skill-os 0.1.0 โ†’ 0.1.2

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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/skill-os.js +226 -391
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skill-os",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Skill-OS CLI for managing OS skills",
5
5
  "main": "skill-os.js",
6
6
  "bin": {
@@ -19,7 +19,7 @@
19
19
  "dependencies": {
20
20
  "chalk": "^4.1.2",
21
21
  "commander": "^13.1.0",
22
- "inquirer": "^8.2.6",
22
+ "form-data": "^4.0.5",
23
23
  "js-yaml": "^4.1.0",
24
24
  "jsonschema": "^1.4.1",
25
25
  "node-fetch": "^2.7.0"
@@ -27,4 +27,4 @@
27
27
  "engines": {
28
28
  "node": ">=14.0.0"
29
29
  }
30
- }
30
+ }
package/skill-os.js CHANGED
@@ -5,7 +5,6 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const yaml = require('js-yaml');
7
7
  const chalk = require('chalk');
8
- const inquirer = require('inquirer');
9
8
  const { execSync } = require('child_process');
10
9
 
11
10
  // Define specific layer icons matching original python CLI
@@ -61,20 +60,78 @@ function loadIndex() {
61
60
  return JSON.parse(raw);
62
61
  }
63
62
 
63
+ // ----------------------------------------------------------------------
64
+ // API Configuration
65
+ // ----------------------------------------------------------------------
66
+
67
+ const DEFAULT_API_BASE = "https://oscopilot.alibaba-inc.com/skills/api/v1";
68
+
69
+ function getApiBase(options) {
70
+ let url = options?.url || process.env.SKILL_OS_REGISTRY || DEFAULT_API_BASE;
71
+ return url.replace(/\/$/, "");
72
+ }
73
+
74
+ async function fetchFromApi(endpoint, options = {}) {
75
+ // Only require node-fetch dynamically when needed
76
+ let fetchFn = require('node-fetch');
77
+ if (typeof fetchFn !== 'function' && fetchFn.default) {
78
+ fetchFn = fetchFn.default;
79
+ }
80
+
81
+ let baseUrl;
82
+ // Extract url option if passed inside options object
83
+ if (options && options.url) {
84
+ baseUrl = getApiBase({ url: options.url });
85
+ delete options.url;
86
+ } else {
87
+ baseUrl = getApiBase({});
88
+ }
89
+
90
+ const url = `${baseUrl}${endpoint}`;
91
+
92
+ try {
93
+ const response = await fetchFn(url, options);
94
+ if (!response.ok) {
95
+ throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
96
+ }
97
+ return await response.json();
98
+ } catch (error) {
99
+ console.error(chalk.red(`\nโœ— Error connecting to remote registry at ${url}`));
100
+ console.error(chalk.dim(error.message));
101
+ process.exit(1);
102
+ }
103
+ }
104
+
64
105
  // ----------------------------------------------------------------------
65
106
  // Commmands Implementations
66
107
  // ----------------------------------------------------------------------
67
108
 
68
- function cmdList() {
69
- const index = loadIndex();
70
- const skills = index.skills || {};
109
+ async function cmdList(options) {
110
+ console.log(`\n${chalk.dim('Fetching skills from external registry...')}`);
111
+ const index = await fetchFromApi('/skills', options);
112
+
113
+ // API returns an array of skills, or an object with a data/skills array property
114
+ let skillsList = [];
115
+ if (Array.isArray(index)) {
116
+ skillsList = index;
117
+ } else if (Array.isArray(index.data)) {
118
+ skillsList = index.data;
119
+ } else if (Array.isArray(index.skills)) {
120
+ skillsList = index.skills;
121
+ } else if (typeof index === 'object') {
122
+ // Fallback if it is an object map
123
+ skillsList = Object.entries(index.skills || index).map(([k, v]) => ({ path: k, ...v }));
124
+ }
71
125
 
72
126
  console.log(`\n${chalk.bold('๐Ÿ“š Skill-OS Available Skills')}`);
73
127
  console.log(`${chalk.dim('โ”€'.repeat(60))}\n`);
74
128
 
75
129
  const layers = {};
76
- for (const [skillPath, info] of Object.entries(skills)) {
77
- const layer = skillPath.split('/')[0];
130
+ for (const info of skillsList) {
131
+ // Fallback path resolution. If the API returns 'path', use it. Otherwise, construct it or use name.
132
+ const skillPath = info.path || (info.layer ? `${info.layer}/${info.category || 'misc'}/${info.name}` : info.name || 'unknown');
133
+ const layer = info.layer || (skillPath.includes('/') ? skillPath.split('/')[0] : 'misc');
134
+
78
135
  if (!layers[layer]) layers[layer] = [];
79
136
  layers[layer].push({ path: skillPath, info });
80
137
  }
@@ -99,16 +156,22 @@ function cmdList() {
99
156
  }
100
157
  }
101
158
 
102
- function cmdSearch(query) {
103
- const lowercaseQuery = query.toLowerCase();
104
- const index = loadIndex();
105
- const skills = index.skills || {};
159
+ async function cmdSearch(query, options) {
160
+ console.log(`\n${chalk.dim(`Searching remote registry for '${query}'...`)}`);
161
+
162
+ // API Endpoint: /api/v1/search?q={{skill.name}}
163
+ const encodedQuery = encodeURIComponent(query);
164
+ const response = await fetchFromApi(`/search?q=${encodedQuery}`, options);
106
165
 
107
- const matches = [];
108
- for (const [skillPath, info] of Object.entries(skills)) {
109
- const searchable = `${skillPath} ${info.name || ''} ${info.description || ''}`.toLowerCase();
110
- if (searchable.includes(lowercaseQuery)) {
111
- matches.push({ path: skillPath, info });
166
+ // The API likely returns an array or an object map. Normalize to an array of objects.
167
+ let matches = [];
168
+ const rawResults = response.data || response.results || response.skills || response || [];
169
+
170
+ if (Array.isArray(rawResults)) {
171
+ matches = rawResults;
172
+ } else if (typeof rawResults === 'object') {
173
+ for (const [skillPath, info] of Object.entries(rawResults)) {
174
+ matches.push({ path: skillPath, ...info });
112
175
  }
113
176
  }
114
177
 
@@ -120,124 +183,53 @@ function cmdSearch(query) {
120
183
  console.log(`\n${chalk.bold(`๐Ÿ” Search Results for '${query}'`)}`);
121
184
  console.log(`${chalk.dim(`Found ${matches.length} skill(s)`)}\n`);
122
185
 
123
- for (const { path: skillPath, info } of matches) {
186
+ for (const info of matches) {
187
+ const skillPath = info.path || info.name; // Fallback to name if path isn't provided separately
124
188
  const icon = formatLayerIcon(skillPath);
125
189
  const status = formatStatus(info.status);
126
- const name = info.name || skillPath;
127
190
  const version = info.version || '?';
128
191
  const desc = info.description || '';
129
192
 
130
193
  console.log(`${icon} ${chalk.cyan(skillPath)}`);
131
- console.log(` ${chalk.bold(name)} (${version}) [${status}]`);
194
+ console.log(` ${chalk.bold(info.name)} (${version}) [${status}]`);
132
195
  console.log(` ${desc}\n`);
133
196
  }
134
197
  }
135
198
 
136
- function cmdInfo(skillPath) {
137
- const index = loadIndex();
138
- const skills = index.skills || {};
199
+ async function cmdInfo(skillPath, options) {
200
+ console.log(`\n${chalk.dim(`Fetching details from remote registry...`)}`);
139
201
 
140
- if (!skills[skillPath]) {
141
- console.error(chalk.red(`Error: Skill '${skillPath}' not found`));
142
- console.log(chalk.dim("Use 'skill-os list' to see available skills"));
143
- process.exit(1);
144
- }
202
+ // API Endpoint: /api/v1/skills/{{skill.path}}/content
203
+ const info = await fetchFromApi(`/skills/${skillPath}/content`, options);
145
204
 
146
- const info = skills[skillPath];
147
205
  const icon = formatLayerIcon(skillPath);
148
- const status = formatStatus(info.status);
206
+ const status = formatStatus(info.status || info.metadata?.status);
207
+ const metadata = info.metadata || info || {}; // Handle if nested or flat
149
208
 
150
- console.log(`\n${icon} ${chalk.bold(info.name || skillPath)}`);
209
+ console.log(`\n${icon} ${chalk.bold(metadata.name || skillPath)}`);
151
210
  console.log(chalk.dim('โ”€'.repeat(40)));
152
211
  console.log(` Path: ${chalk.cyan(skillPath)}`);
153
- console.log(` Version: ${info.version || 'unknown'}`);
212
+ console.log(` Version: ${metadata.version || 'unknown'}`);
154
213
  console.log(` Status: ${status}`);
155
- console.log(` Description: ${info.description || 'No description'}`);
214
+ console.log(` Description: ${metadata.description || 'No description'}`);
156
215
 
157
- if (info.dependencies && info.dependencies.length > 0) {
158
- console.log(` Dependencies: ${info.dependencies.join(', ')}`);
216
+ if (metadata.dependencies && metadata.dependencies.length > 0) {
217
+ console.log(` Dependencies: ${metadata.dependencies.join(', ')}`);
159
218
  } else {
160
219
  console.log(` Dependencies: ${chalk.dim('None')}`);
161
220
  }
162
221
 
163
- const repoRoot = getRepoRoot();
164
- const skillMdPath = path.join(repoRoot, skillPath, 'SKILL.md');
165
- if (fs.existsSync(skillMdPath)) {
166
- console.log(`\n ${chalk.dim(`SKILL.md: ${skillMdPath}`)}`);
222
+ // If the API returns the actual markdown content, display a snippet or note
223
+ if (info.content || info.markdown) {
224
+ console.log(`\n ${chalk.dim('[Remote Document Content Available]')}`);
167
225
  }
168
226
  console.log();
169
227
  }
170
228
 
171
- function cmdInstall(skillPath, options) {
172
- const index = loadIndex();
173
- const skills = index.skills || {};
174
-
175
- // Expand ~ relative to HOME
176
- const homeDir = require('os').homedir();
177
- const rawTarget = options.target.startsWith('~/') ?
178
- path.join(homeDir, options.target.slice(2)) :
179
- path.resolve(options.target);
180
-
181
- const targetDir = path.resolve(rawTarget);
182
-
183
- if (!skills[skillPath]) {
184
- console.error(chalk.red(`Error: Skill '${skillPath}' not found`));
185
- console.log(chalk.dim("Use 'skill-os list' to see available skills"));
186
- process.exit(1);
187
- }
188
-
189
- const info = skills[skillPath];
190
- const repoRoot = getRepoRoot();
191
- const sourceDir = path.join(repoRoot, skillPath);
192
-
193
- if (!fs.existsSync(sourceDir)) {
194
- console.error(chalk.red(`Error: Skill directory not found: ${sourceDir}`));
195
- process.exit(1);
196
- }
197
-
198
- const skillName = path.basename(skillPath);
199
- const installTarget = path.join(targetDir, skillName);
200
-
201
- if (fs.existsSync(installTarget)) {
202
- if (options.force) {
203
- console.log(chalk.yellow(`Removing existing: ${installTarget}`));
204
- fs.rmSync(installTarget, { recursive: true, force: true });
205
- } else {
206
- console.error(chalk.red(`Error: Target already exists: ${installTarget}`));
207
- console.log(chalk.dim("Use --force to overwrite"));
208
- process.exit(1);
209
- }
210
- }
211
-
212
- fs.mkdirSync(targetDir, { recursive: true });
213
229
 
214
- console.log(`\n${chalk.bold(`๐Ÿ“ฅ Installing ${info.name || skillPath}...`)}`);
215
- console.log(` Source: ${chalk.cyan(sourceDir)}`);
216
- console.log(` Target: ${chalk.cyan(installTarget)}`);
217
-
218
- try {
219
- // Simple recursive copy
220
- fs.cpSync(sourceDir, installTarget, { recursive: true });
221
- console.log(`\n${chalk.green('โœ“ Successfully installed!')}`);
222
-
223
- console.log(`\n${chalk.bold('๐Ÿ“‹ Usage:')}`);
224
- console.log(` SKILL.md: ${installTarget}/SKILL.md`);
225
-
226
- const deps = info.dependencies || [];
227
- if (deps.length > 0) {
228
- console.log(`\n${chalk.yellow('โš ๏ธ Dependencies required:')}`);
229
- deps.forEach(dep => console.log(` - ${dep}`));
230
- console.log(`\n Install with: ${chalk.dim(`pip install ${deps.join(' ')}`)}`);
231
- }
232
- } catch (e) {
233
- console.error(chalk.red(`Error during installation: ${e.message}`));
234
- process.exit(1);
235
- }
236
- }
237
230
 
238
231
  function cmdCreate(skillPath, options) {
239
- const repoRoot = getRepoRoot();
240
- const targetDir = path.join(repoRoot, 'skills', skillPath);
232
+ const targetDir = path.resolve(skillPath);
241
233
 
242
234
  if (fs.existsSync(targetDir)) {
243
235
  if (!options.force) {
@@ -329,36 +321,48 @@ TODO: Add skill documentation here.
329
321
  console.log(` 3. Sync: ${chalk.dim(`skill-os sync ${skillPath}`)}`);
330
322
  }
331
323
 
332
- function cmdDownload(skillName, options) {
333
- const targetDir = options.platform
334
- ? path.join(process.cwd(), `.${options.platform}`, "skills")
335
- : process.cwd();
324
+ function cmdDownload(skillPath, options) {
325
+ // 1. Determine target directory
326
+ const homeDir = require('os').homedir();
327
+ let rawTarget = process.cwd();
336
328
 
337
- fs.mkdirSync(targetDir, { recursive: true });
329
+ if (options.target) {
330
+ rawTarget = options.target.startsWith('~/') ?
331
+ path.join(homeDir, options.target.slice(2)) :
332
+ path.resolve(options.target);
333
+ } else if (options.platform) {
334
+ rawTarget = path.join(process.cwd(), `.${options.platform}`, "skills");
335
+ }
338
336
 
339
- let serverUrl = options.url || process.env.SKILL_OS_REGISTRY || "https://example.com/skills";
340
- serverUrl = serverUrl.replace(/\/$/, ""); // Trim trailing slash
337
+ const targetDir = path.resolve(rawTarget);
338
+ fs.mkdirSync(targetDir, { recursive: true });
341
339
 
342
- const downloadUrl = `${serverUrl}/${skillName}.tar.gz`;
340
+ // 2. Determine API source
341
+ const serverUrl = getApiBase(options);
342
+ const downloadUrl = `${serverUrl}/skills/${skillPath}/download`;
343
343
 
344
- console.log(`\n${chalk.bold(`๐Ÿ“ฅ Downloading ${skillName}...`)}`);
344
+ console.log(`\n${chalk.bold(`๐Ÿ“ฅ Downloading and extracting ${skillPath}...`)}`);
345
345
  console.log(` Source: ${chalk.cyan(downloadUrl)}`);
346
346
  console.log(` Target: ${chalk.cyan(targetDir)}\n`);
347
347
 
348
348
  try {
349
- // Using curl synchronously for simplicity, similar to the bash version
350
- execSync(`curl -L -O ${downloadUrl}`, { cwd: targetDir, stdio: 'inherit' });
351
- console.log(`\n${chalk.green(`โœ“ Successfully downloaded ${skillName} to ${targetDir}`)}`);
349
+ // Stream the curl output directly into tar to extract the directory structure
350
+ // -s: silent curl, -L: follow redirects
351
+ // tar -xzf -: extract gzipped tar from stdin
352
+ execSync(`curl -s -L "${downloadUrl}" | tar -xzf -`, { cwd: targetDir, stdio: 'inherit' });
353
+ console.log(`\n${chalk.green(`โœ“ Successfully downloaded and extracted to: ${targetDir}`)}`);
352
354
  } catch (e) {
353
- console.error(`\n${chalk.red(`โœ— Failed to download ${skillName}: command returned error code ${e.status}`)}`);
355
+ console.error(`\n${chalk.red(`โœ— Failed to download. The API may have returned an error instead of a tarball.`)}`);
354
356
  process.exit(1);
355
357
  }
356
358
  }
357
359
 
358
360
  // ----------------------------------------------------------------------
359
- // YAML / Sync Helpers
361
+ // Publishing / Uploading
360
362
  // ----------------------------------------------------------------------
361
363
 
364
+ const FormData = require('form-data');
365
+
362
366
  function parseSkillMdFrontmatter(skillMdPath) {
363
367
  if (!fs.existsSync(skillMdPath)) return {};
364
368
  const content = fs.readFileSync(skillMdPath, 'utf8');
@@ -372,284 +376,120 @@ function parseSkillMdFrontmatter(skillMdPath) {
372
376
  }
373
377
  }
374
378
 
375
- function loadYamlIndex() {
376
- const repoRoot = getRepoRoot();
377
- const yamlPath = path.join(repoRoot, 'SKILL_INDEX.yaml');
378
- if (!fs.existsSync(yamlPath)) {
379
- console.error(chalk.red('Error: SKILL_INDEX.yaml not found'));
380
- process.exit(1);
381
- }
382
- const raw = fs.readFileSync(yamlPath, 'utf8');
383
- return { data: yaml.load(raw), yamlPath };
384
- }
385
-
386
- function syncToJson(repoRoot) {
387
- const yamlPath = path.join(repoRoot, 'SKILL_INDEX.yaml');
388
- const jsonPath = path.join(repoRoot, 'SKILL_INDEX.json');
389
-
390
- const data = yaml.load(fs.readFileSync(yamlPath, 'utf8'));
391
- data.generated_at = new Date().toISOString();
392
-
393
- fs.writeFileSync(jsonPath, JSON.stringify(data, null, 2), 'utf8');
394
- }
395
-
396
- function appendSkillToYaml(skillPath, skillEntry, yamlPath) {
397
- let content = fs.readFileSync(yamlPath, 'utf8');
398
- const escapedPath = skillPath.replace(/[.*+?^$\\{}()|[\\]\\\\]/g, '\\\\$&');
399
-
400
- // JS Regex to match existing block (simplified approximation matching python's pattern)
401
- // Find " skill/path:" and everything indented under it until the next line that isn't indented by at least 4 spaces
402
- const pattern = new RegExp(`(^|\\n) ${escapedPath}:\\n(?: .*\\n)*`);
403
-
404
- let newBlock = `
405
- ${skillPath}:
406
- name: ${skillEntry.name}
407
- version: "${skillEntry.version}"
408
- description: ${skillEntry.description}
409
- status: ${skillEntry.status}
410
- layer: ${skillEntry.layer}
411
- lifecycle: ${skillEntry.lifecycle}
412
- category: ${skillEntry.category}
413
- tags:
414
- `;
415
- const tags = skillEntry.tags || [];
416
- if (tags.length) {
417
- tags.forEach(t => newBlock += ` - ${t}\n`);
418
- } else {
419
- newBlock += ` tags: []\n`;
420
- }
421
-
422
- const deps = skillEntry.dependencies || [];
423
- if (deps.length) {
424
- newBlock += ` dependencies:\n`;
425
- deps.forEach(d => newBlock += ` - ${d}\n`);
426
- } else {
427
- newBlock += ` dependencies: []\n`;
428
- }
429
-
430
- if (pattern.test(content)) {
431
- content = content.replace(pattern, `$1${newBlock.trimStart()}`);
432
- } else {
433
- content = content.trimEnd() + '\n' + newBlock;
434
- }
435
-
436
- fs.writeFileSync(yamlPath, content, 'utf8');
437
- }
379
+ async function cmdUpload(skillPath, options) {
380
+ const skillDir = path.resolve(skillPath);
381
+ const skillMdPath = path.join(skillDir, 'SKILL.md');
438
382
 
439
- async function cmdSync(skillPath) {
440
- const repoRoot = getRepoRoot();
441
- const skillDir = path.join(repoRoot, 'skills', skillPath);
383
+ console.log(`\n${chalk.bold(`๐Ÿš€ Publishing Skill: ${skillPath}`)}`);
384
+ console.log(`${chalk.dim('โ”€'.repeat(50))}\n`);
442
385
 
443
- if (!fs.existsSync(skillDir)) {
444
- console.error(chalk.red(`Error: Skill directory not found: ${skillDir}`));
445
- console.log(chalk.dim(`Create it first with: skill-os create ${skillPath}`));
386
+ // 1. Validation (Requires hoisted functions)
387
+ const { isValid: isDirValid, errors: dirErrors } = validateDirectoryStructure(skillDir);
388
+ if (!isDirValid) {
389
+ console.error(chalk.red('โœ— Validation failed: Directory structure errors:'));
390
+ dirErrors.forEach(err => console.error(` - ${err}`));
446
391
  process.exit(1);
447
392
  }
448
393
 
449
- const skillMdPath = path.join(skillDir, 'SKILL.md');
450
394
  if (!fs.existsSync(skillMdPath)) {
451
- console.error(chalk.red(`Error: SKILL.md not found in ${skillDir}`));
395
+ console.log(`${chalk.red('โœ— Fatal: SKILL.md not found')}`);
452
396
  process.exit(1);
453
397
  }
454
398
 
455
- console.log(`\n${chalk.bold('๐Ÿ“ Skill-OS Sync')}`);
456
- console.log(chalk.dim('โ”€'.repeat(50)));
457
- console.log(`Skill path: ${chalk.cyan(skillPath)}\n`);
458
-
459
399
  const fm = parseSkillMdFrontmatter(skillMdPath);
460
- const { data: indexData, yamlPath } = loadYamlIndex();
461
- const skills = indexData.skills || {};
462
- const existing = skills[skillPath] || {};
463
- const isUpdate = !!existing.name;
464
-
465
- if (isUpdate) {
466
- console.log(`${chalk.yellow('โš  Skill already registered, updating...')}\n`);
467
- } else {
468
- console.log(`${chalk.green('โœจ Registering new skill...')}\n`);
400
+ if (!fm || Object.keys(fm).length === 0) {
401
+ console.log(`${chalk.red('โœ— SKILL.md missing or invalid frontmatter')}`);
402
+ process.exit(1);
469
403
  }
470
404
 
471
- console.log(`${chalk.bold('Please confirm or enter skill metadata:')}\n`);
472
-
473
- const pathParts = skillPath.split('/');
474
- const inferredCategory = pathParts.length >= 2 ? pathParts[1] : '';
475
-
476
- const questions = [
477
- { name: 'name', message: ' name', default: existing.name || fm.name || pathParts[pathParts.length - 1] },
478
- { name: 'description', message: ' description', default: existing.description || fm.description || '' },
479
- { name: 'version', message: ' version', default: existing.version || fm.version || '1.0.0' },
480
- { name: 'status', message: ` status ${chalk.dim('(stable, beta, placeholder)')}`, default: existing.status || fm.status || 'beta' },
481
- { name: 'dependencies', message: ` dependencies ${chalk.dim('(comma-separated)')}`, default: (existing.dependencies || fm.dependencies || []).join(', ') }
482
- ];
483
-
484
- const answers = await inquirer.prompt(questions);
485
-
486
- const depsArr = answers.dependencies.split(',').map(d => d.trim()).filter(Boolean);
487
- const category = existing.category || fm.category || inferredCategory;
488
- let tags = existing.tags || fm.tags || [];
489
-
490
- if (category && (!tags.length || tags[0] !== category)) {
491
- tags = [category, ...tags.filter(t => t !== category)];
405
+ const v = new Validator();
406
+ const result = v.validate(fm, skillSchema);
407
+ if (fm.category && fm.tags && fm.tags.length > 0 && fm.tags[0] !== fm.category) {
408
+ result.errors.push({ stack: `tags[0] must equal category name '${fm.category}'` });
492
409
  }
493
410
 
494
- const skillEntry = {
495
- name: answers.name,
496
- version: answers.version,
497
- description: answers.description,
498
- status: answers.status,
499
- layer: existing.layer || fm.layer || pathParts[0],
500
- lifecycle: existing.lifecycle || fm.lifecycle || 'usage',
501
- category: category,
502
- tags: tags,
503
- dependencies: depsArr
504
- };
505
-
506
- console.log(`\n${chalk.bold('๐Ÿ“‹ Summary:')}`);
507
- console.log(` ${chalk.cyan(skillPath)}:`);
508
- for (const [k, v] of Object.entries(skillEntry)) {
509
- console.log(` ${k}: ${Array.isArray(v) ? (v.length ? JSON.stringify(v) : '[]') : v}`);
411
+ if (!result.valid || result.errors.length > 0) {
412
+ console.error(chalk.red('โœ— Validation failed: SKILL.md has errors:'));
413
+ result.errors.forEach(err => console.error(` - ${err.stack.replace('instance.', '')}`));
414
+ process.exit(1);
510
415
  }
511
- console.log();
512
416
 
513
- const { confirm } = await inquirer.prompt([{
514
- type: 'confirm',
515
- name: 'confirm',
516
- message: 'Confirm and save?',
517
- default: true
518
- }]);
417
+ console.log(chalk.green('โœ“ Local validation passed.'));
519
418
 
520
- if (!confirm) {
521
- console.log(chalk.yellow('Cancelled'));
522
- process.exit(0);
523
- }
419
+ // 2. Prepare tarball
420
+ const tarFilename = `${fm.name}-${fm.version}.tar.gz`;
421
+ const tmpDir = require('os').tmpdir();
422
+ const tarPath = path.join(tmpDir, tarFilename);
524
423
 
525
- console.log(`\n${chalk.dim('Saving SKILL_INDEX.yaml...')}`);
526
- appendSkillToYaml(skillPath, skillEntry, yamlPath);
527
-
528
- console.log(chalk.dim('Syncing to SKILL_INDEX.json...'));
529
- syncToJson(repoRoot);
424
+ console.log(chalk.dim(`๐Ÿ“ฆ Creating package archive...`));
425
+ try {
426
+ // -C changes to directory, . archives contents
427
+ // COPYFILE_DISABLE=1 prevents macOS tar from including ._* extended attribute files
428
+ execSync(`tar -czf "${tarPath}" -C "${skillDir}" .`, {
429
+ env: { ...process.env, COPYFILE_DISABLE: '1' }
430
+ });
431
+ } catch (e) {
432
+ console.error(chalk.red('โœ— Failed to create tar archive.'));
433
+ process.exit(1);
434
+ }
530
435
 
531
- console.log(`\n${chalk.green(`โœ… Skill ${isUpdate ? 'updated' : 'registered'} successfully!`)}`);
532
- console.log(`\n${chalk.bold('๐Ÿ“‹ Next steps:')}`);
533
- console.log(" git add SKILL_INDEX.yaml SKILL_INDEX.json");
534
- console.log(` git commit -m 'feat: ${isUpdate ? 'update' : 'add'} ${skillPath} skill'`);
535
- }
436
+ // 3. Prepare Form Data
437
+ const form = new FormData();
438
+ form.append('package', fs.createReadStream(tarPath), {
439
+ filename: tarFilename,
440
+ contentType: 'application/gzip'
441
+ });
442
+ form.append('metadata', JSON.stringify(fm));
536
443
 
537
- function findSkillMds(dir, fileList = []) {
538
- const files = fs.readdirSync(dir);
539
- for (const file of files) {
540
- const stat = fs.statSync(path.join(dir, file));
541
- if (stat.isDirectory()) {
542
- findSkillMds(path.join(dir, file), fileList);
543
- } else if (file === 'SKILL.md') {
544
- fileList.push(path.join(dir, file));
545
- }
444
+ if (options.update) {
445
+ form.append('is_update', 'true');
446
+ console.log(chalk.yellow(`โš  Uploading as an UPDATE (Version: ${fm.version}). Ensure the version number has been bumped!`));
447
+ } else {
448
+ console.log(chalk.cyan(`โœจ Uploading as a NEW skill (Version: ${fm.version}).`));
546
449
  }
547
- return fileList;
548
- }
549
450
 
550
- function cmdSyncAll(options) {
551
- const repoRoot = getRepoRoot();
552
- const skillsDir = path.join(repoRoot, 'skills');
451
+ const serverUrl = getApiBase(options);
452
+ const uploadUrl = `${serverUrl}/skills/upload`;
553
453
 
554
- console.log(`\n${chalk.bold('๐Ÿ”„ Skill-OS Sync All')}`);
555
- console.log(chalk.dim('โ”€'.repeat(50)));
556
- console.log(`Scanning: ${chalk.cyan(skillsDir)}\n`);
454
+ const fetchOptions = {
455
+ method: 'POST',
456
+ body: form,
457
+ headers: form.getHeaders()
458
+ };
557
459
 
558
- const skillMds = findSkillMds(skillsDir);
559
- if (skillMds.length === 0) {
560
- console.log(chalk.yellow('No SKILL.md files found'));
561
- return;
460
+ if (options.token) {
461
+ fetchOptions.headers['Authorization'] = `Bearer ${options.token}`;
562
462
  }
563
463
 
564
- console.log(`Found ${chalk.green(skillMds.length)} skills\n`);
565
-
566
- const newSkills = {};
567
- const errors = [];
464
+ console.log(chalk.dim(`\n๐Ÿ“ก Uploading to ${uploadUrl}...`));
568
465
 
569
- skillMds.sort().forEach(skillMd => {
570
- const relPath = path.relative(skillsDir, path.dirname(skillMd));
571
- const fm = parseSkillMdFrontmatter(skillMd);
466
+ let fetchFn = require('node-fetch');
467
+ if (typeof fetchFn !== 'function' && fetchFn.default) {
468
+ fetchFn = fetchFn.default;
469
+ }
572
470
 
573
- if (!fm || Object.keys(fm).length === 0) {
574
- errors.push(` โš  ${relPath}: Failed to parse frontmatter`);
575
- return;
471
+ try {
472
+ const response = await fetchFn(uploadUrl, fetchOptions);
473
+ if (!response.ok) {
474
+ let errorMsg = response.statusText;
475
+ try {
476
+ const errBody = await response.json();
477
+ if (errBody.error || errBody.message) errorMsg = errBody.error || errBody.message;
478
+ } catch (e) { }
479
+ throw new Error(`HTTP ${response.status}: ${errorMsg}`);
576
480
  }
577
-
578
- let status = fm.status || 'beta';
579
- let icon = 'โœ“';
580
- if (status === 'placeholder') {
581
- if (!options.includePlaceholder) {
582
- status = 'beta'; // Default placeholder -> beta
583
- }
584
- icon = '๐Ÿ”ธ';
481
+ console.log(`\n${chalk.green('โœ… Publish successful!')}`);
482
+ } catch (error) {
483
+ console.error(chalk.red(`\nโœ— Publish failed.`));
484
+ console.error(chalk.red(` ${error.message}`));
485
+ if (!options.update && error.message.includes('already exists')) {
486
+ console.log(chalk.yellow(`\n๐Ÿ’ก If you intended to update an existing skill, use the --update flag.`));
585
487
  }
586
-
587
- const name = fm.name || path.basename(path.dirname(skillMd));
588
- const layer = fm.layer || relPath.split('/')[0];
589
- const category = fm.category || (relPath.includes('/') ? relPath.split('/')[1] : '');
590
- let tags = fm.tags || [];
591
-
592
- if (tags && category && tags[0] !== category) {
593
- tags = [category, ...tags.filter(t => t !== category)];
488
+ process.exit(1);
489
+ } finally {
490
+ if (fs.existsSync(tarPath)) {
491
+ fs.unlinkSync(tarPath);
594
492
  }
595
-
596
- let description = fm.description || '';
597
- if (description.length > 200) description = description.substring(0, 200);
598
-
599
- newSkills[relPath] = {
600
- name,
601
- version: fm.version || '0.1.0',
602
- description,
603
- status,
604
- layer,
605
- lifecycle: fm.lifecycle || 'usage',
606
- category,
607
- tags: tags.slice(0, 5),
608
- dependencies: fm.dependencies || []
609
- };
610
-
611
- console.log(` ${icon} ${chalk.cyan(relPath)} (${layer}/${newSkills[relPath].lifecycle})`);
612
- });
613
-
614
- if (errors.length) {
615
- console.log(`\n${chalk.yellow('Warnings:')}`);
616
- errors.forEach(e => console.log(e));
617
- }
618
-
619
- const { data: indexData, yamlPath } = loadYamlIndex();
620
- indexData.skills = newSkills;
621
- indexData.version = "3.0";
622
-
623
- const yamlDump = yaml.dump(indexData, {
624
- sortKeys: false,
625
- noCompatMode: true
626
- });
627
-
628
- const header = `# Skill-OS ๆŠ€่ƒฝ็ดขๅผ•
629
- # ====================
630
- # Generated: ${new Date().toISOString()}
631
- # Layers: core | system | runtime | application | meta
632
- # Lifecycle: production | maintenance | operations | usage | meta
633
-
634
- `;
635
-
636
- fs.writeFileSync(yamlPath, header + yamlDump, 'utf8');
637
- console.log(`\n${chalk.green(`โœ“ Updated ${path.basename(yamlPath)}`)}`);
638
-
639
- syncToJson(repoRoot);
640
- console.log(`${chalk.green(`โœ“ Updated SKILL_INDEX.json`)}`);
641
-
642
- console.log(`\n${chalk.bold('๐Ÿ“Š Summary:')}`);
643
- console.log(` Total skills: ${Object.keys(newSkills).length}`);
644
-
645
- const layerCounts = {};
646
- for (const info of Object.values(newSkills)) {
647
- const layer = info.layer || 'unknown';
648
- layerCounts[layer] = (layerCounts[layer] || 0) + 1;
649
- }
650
-
651
- for (const layer of Object.keys(layerCounts).sort()) {
652
- console.log(` ${formatLayerIcon(layer + '/')} ${layer}: ${layerCounts[layer]}`);
653
493
  }
654
494
  }
655
495
 
@@ -703,8 +543,7 @@ function validateDirectoryStructure(skillDir) {
703
543
  }
704
544
 
705
545
  function cmdValidate(skillPath) {
706
- const repoRoot = getRepoRoot();
707
- const skillDir = path.join(repoRoot, 'skills', skillPath);
546
+ const skillDir = path.resolve(skillPath);
708
547
  const skillMdPath = path.join(skillDir, 'SKILL.md');
709
548
 
710
549
  console.log(`\n${chalk.bold(`๐Ÿ” Validating Skill: ${skillPath}`)}`);
@@ -771,48 +610,44 @@ program
771
610
  .description('Skill-OS CLI');
772
611
 
773
612
  program.command('list')
774
- .description('List all available skills')
613
+ .description('List all available skills from the remote registry')
614
+ .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
775
615
  .action(cmdList);
776
616
 
777
617
  program.command('search')
778
- .description('Search for skills')
618
+ .description('Search for skills in the remote registry')
779
619
  .argument('<query>', 'Search query')
620
+ .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
780
621
  .action(cmdSearch);
781
622
 
782
623
  program.command('info')
783
- .description('Show skill details')
784
- .argument('<path>', 'Skill path (e.g., package/package/rpm_search)')
624
+ .description('Show skill details from the remote registry')
625
+ .argument('<path>', 'Skill path (e.g., package/rpm_search)')
626
+ .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
785
627
  .action(cmdInfo);
786
628
 
787
- program.command('install')
788
- .description('Install a skill to target directory')
789
- .argument('<path>', 'Skill path to install')
790
- .option('-t, --target <dir>', 'Target directory', '~/.skills')
791
- .option('-f, --force', 'Force overwrite if exists', false)
792
- .action(cmdInstall);
793
-
794
629
  program.command('create')
795
- .description('Create a new skill scaffold')
630
+ .description('Create a new skill scaffold locally')
796
631
  .argument('<path>', 'Skill path to create (e.g., system/migration)')
797
632
  .option('-f, --force', 'Force overwrite if exists', false)
798
633
  .action(cmdCreate);
799
634
 
800
635
  program.command('download')
801
- .description('Download a skill package')
802
- .argument('<skill_name>', 'Name of the skill to download')
636
+ .description('Download and extract a skill package from the remote registry')
637
+ .argument('<skill_path>', 'Path of the skill to download (e.g., core/kernel/kernel-info)')
638
+ .option('-t, --target <dir>', 'Target directory to extract into (defaults to current dir)')
803
639
  .option('--platform <platform>', 'Specify the platform (e.g., qoder will download to .qoder/skills/)')
804
- .option('--url <url>', 'Specify the base URL for the registry (defaults to SKILL_OS_REGISTRY env var)')
640
+ .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
805
641
  .action(cmdDownload);
806
642
 
807
- program.command('sync')
808
- .description('Register or update a skill in the index with interactive prompts')
809
- .argument('<path>', 'Skill path to sync (e.g., cloud/alicloud-api)')
810
- .action(cmdSync);
811
-
812
- program.command('sync-all')
813
- .description('Regenerate index from all SKILL.md files (one-click sync)')
814
- .option('--include-placeholder', 'Include placeholder skills in output', false)
815
- .action(cmdSyncAll);
643
+ program.command('upload')
644
+ .alias('publish')
645
+ .description('Upload a local skill to the remote registry')
646
+ .argument('<path>', 'Skill path to upload (e.g., system/migration)')
647
+ .option('-u, --update', 'Publish as an update to an existing skill', false)
648
+ .option('--token <token>', 'Authentication token for the remote registry')
649
+ .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
650
+ .action(cmdUpload);
816
651
 
817
652
  program.command('validate')
818
653
  .description('Validate a skill against spec')