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.
- package/package.json +3 -3
- 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.
|
|
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
|
-
"
|
|
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
|
-
|
|
70
|
-
const
|
|
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
|
|
77
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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(
|
|
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: ${
|
|
212
|
+
console.log(` Version: ${metadata.version || 'unknown'}`);
|
|
154
213
|
console.log(` Status: ${status}`);
|
|
155
|
-
console.log(` Description: ${
|
|
214
|
+
console.log(` Description: ${metadata.description || 'No description'}`);
|
|
156
215
|
|
|
157
|
-
if (
|
|
158
|
-
console.log(` Dependencies: ${
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
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(
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
324
|
+
function cmdDownload(skillPath, options) {
|
|
325
|
+
// 1. Determine target directory
|
|
326
|
+
const homeDir = require('os').homedir();
|
|
327
|
+
let rawTarget = process.cwd();
|
|
336
328
|
|
|
337
|
-
|
|
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
|
-
|
|
340
|
-
|
|
337
|
+
const targetDir = path.resolve(rawTarget);
|
|
338
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
341
339
|
|
|
342
|
-
|
|
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 ${
|
|
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
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
376
|
-
const
|
|
377
|
-
const
|
|
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
|
-
|
|
440
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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.
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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(
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
551
|
-
const
|
|
552
|
-
const skillsDir = path.join(repoRoot, 'skills');
|
|
451
|
+
const serverUrl = getApiBase(options);
|
|
452
|
+
const uploadUrl = `${serverUrl}/skills/upload`;
|
|
553
453
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
454
|
+
const fetchOptions = {
|
|
455
|
+
method: 'POST',
|
|
456
|
+
body: form,
|
|
457
|
+
headers: form.getHeaders()
|
|
458
|
+
};
|
|
557
459
|
|
|
558
|
-
|
|
559
|
-
|
|
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(
|
|
565
|
-
|
|
566
|
-
const newSkills = {};
|
|
567
|
-
const errors = [];
|
|
464
|
+
console.log(chalk.dim(`\n๐ก Uploading to ${uploadUrl}...`));
|
|
568
465
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
466
|
+
let fetchFn = require('node-fetch');
|
|
467
|
+
if (typeof fetchFn !== 'function' && fetchFn.default) {
|
|
468
|
+
fetchFn = fetchFn.default;
|
|
469
|
+
}
|
|
572
470
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
|
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/
|
|
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('<
|
|
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>', '
|
|
640
|
+
.option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
|
|
805
641
|
.action(cmdDownload);
|
|
806
642
|
|
|
807
|
-
program.command('
|
|
808
|
-
.
|
|
809
|
-
.
|
|
810
|
-
.
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
.
|
|
814
|
-
.
|
|
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')
|