skill-os 0.1.2 โ†’ 0.1.5

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/skill-os.js CHANGED
@@ -1,657 +1,216 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { program } = require('commander');
4
- const fs = require('fs');
5
- const path = require('path');
6
- const yaml = require('js-yaml');
7
- const chalk = require('chalk');
8
- const { execSync } = require('child_process');
9
-
10
- // Define specific layer icons matching original python CLI
11
- const layerIcons = {
12
- core: "๐Ÿง ",
13
- system: "๐Ÿ”ง",
14
- runtime: "๐Ÿ“ฆ",
15
- application: "๐Ÿš€",
16
- cloud: "โ˜๏ธ",
17
- package: "๐Ÿ“ฆ",
18
- network: "๐ŸŒ",
19
- storage: "๐Ÿ’พ",
20
- user: "๐Ÿ‘ค",
21
- meta: "๐Ÿ› ๏ธ",
22
- };
23
-
24
- function formatLayerIcon(skillPath) {
25
- const layer = skillPath.split("/")[0];
26
- return layerIcons[layer] || "๐Ÿ“„";
27
- }
28
-
29
- function formatStatus(status) {
30
- switch (status?.toLowerCase()) {
31
- case 'stable': return chalk.green(status);
32
- case 'beta': return chalk.yellow(status);
33
- case 'placeholder': return chalk.dim(status);
34
- default: return status || 'unknown';
35
- }
36
- }
37
-
38
- function getRepoRoot() {
39
- let current = path.dirname(__dirname); // Usually cli is in the repo root
40
- if (fs.existsSync(path.join(current, 'SKILL_INDEX.json'))) {
41
- return current;
42
- }
43
-
44
- if (fs.existsSync(path.join(process.cwd(), 'SKILL_INDEX.json'))) {
45
- return process.cwd();
46
- }
47
- return current;
48
- }
49
-
50
- function loadIndex() {
51
- const repoRoot = getRepoRoot();
52
- const indexPath = path.join(repoRoot, 'SKILL_INDEX.json');
3
+ /**
4
+ * Skill-OS CLI โ€” ๅ…ฅๅฃๆ–‡ไปถ
5
+ *
6
+ * ่ดŸ่ดฃ commander ๅ‘ฝไปคๆณจๅ†Œ๏ผŒไธšๅŠก้€ป่พ‘ๆ‹†ๅˆ†ๅœจ lib/ ไธ‹๏ผš
7
+ * lib/utils.js โ€” ๅ…ฑไบซๅทฅๅ…ท
8
+ * lib/config.js โ€” API ้…็ฝฎ & ๅ‡ญๆฎ
9
+ * lib/local.js โ€” ๆœฌๅœฐๅ‘ฝไปค (create, validate)
10
+ * lib/remote.js โ€” ่ฟœ็จ‹ๅ‘ฝไปค (list, search, info, download, upload)
11
+ * lib/auth.js โ€” ่ฎค่ฏๅ‘ฝไปค (login, logout)
12
+ */
53
13
 
54
- if (!fs.existsSync(indexPath)) {
55
- console.error(chalk.red('Error: SKILL_INDEX.json not found'));
56
- process.exit(1);
57
- }
58
-
59
- const raw = fs.readFileSync(indexPath, 'utf-8');
60
- return JSON.parse(raw);
61
- }
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
- }
14
+ const { program } = require('commander');
89
15
 
90
- const url = `${baseUrl}${endpoint}`;
16
+ const { DEFAULT_API_BASE, DEFAULT_DAILY_API_BASE } = require('./lib/config');
17
+ const { cmdCreate, cmdValidate } = require('./lib/local');
18
+ const { cmdSync } = require('./lib/sync');
19
+ const { cmdList, cmdSearch, cmdInfo, cmdDownload, cmdUpload } = require('./lib/remote');
20
+ const { cmdLogin, cmdLogout } = require('./lib/auth');
91
21
 
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
- }
22
+ // โ”€โ”€ ็‰ˆๆœฌ & ๆ่ฟฐ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
104
23
 
105
- // ----------------------------------------------------------------------
106
- // Commmands Implementations
107
- // ----------------------------------------------------------------------
24
+ program
25
+ .name('skill-os')
26
+ .description('Skill-OS CLI โ€” AI-Native OS ๆŠ€่ƒฝ็ฎก็†ๅนณๅฐ')
27
+ .version('0.1.4');
108
28
 
109
- async function cmdList(options) {
110
- console.log(`\n${chalk.dim('Fetching skills from external registry...')}`);
111
- const index = await fetchFromApi('/skills', options);
29
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
30
+ // ่ฎค่ฏๅ‘ฝไปค
31
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
112
32
 
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
- }
33
+ program.command('login')
34
+ .description('็™ปๅฝ• Skill-OS ๆณจๅ†Œไธญๅฟƒ๏ผˆ้€š่ฟ‡ๆต่งˆๅ™จ BUC SSO ่ฎค่ฏ๏ผ‰')
35
+ .option('--url <url>', 'ๆŒ‡ๅฎšๆœๅŠก็ซฏๅœฐๅ€', DEFAULT_DAILY_API_BASE)
36
+ .action(cmdLogin);
125
37
 
126
- console.log(`\n${chalk.bold('๐Ÿ“š Skill-OS Available Skills')}`);
127
- console.log(`${chalk.dim('โ”€'.repeat(60))}\n`);
38
+ program.command('logout')
39
+ .description('ๆณจ้”€็™ปๅฝ•๏ผŒๆธ…้™คๆœฌๅœฐๅ‡ญๆฎ')
40
+ .option('--url <url>', 'ๆŒ‡ๅฎšๆœๅŠก็ซฏๅœฐๅ€', DEFAULT_DAILY_API_BASE)
41
+ .action(cmdLogout);
128
42
 
129
- const layers = {};
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');
43
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
44
+ // ่ฟœ็จ‹ๆณจๅ†Œไธญๅฟƒๅ‘ฝไปค (Remote)
45
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
134
46
 
135
- if (!layers[layer]) layers[layer] = [];
136
- layers[layer].push({ path: skillPath, info });
137
- }
47
+ program.command('list')
48
+ .alias('ls')
49
+ .description('ๅˆ—ๅ‡บๆณจๅ†Œไธญๅฟƒไธญๆ‰€ๆœ‰ๅฏ็”จ็š„ๆŠ€่ƒฝ')
50
+ .addOption(
51
+ new (require('commander').Option)('--url <url>', 'ๆŒ‡ๅฎšๆณจๅ†Œไธญๅฟƒๅœฐๅ€')
52
+ .default(DEFAULT_DAILY_API_BASE).hideHelp()
53
+ )
54
+ .action(cmdList);
138
55
 
139
- for (const layer of Object.keys(layers).sort()) {
140
- const icon = layerIcons[layer] || "๐Ÿ“„";
141
- console.log(`${chalk.bold(icon + ' ' + layer.toUpperCase())}`);
56
+ program.command('search')
57
+ .alias('s')
58
+ .description('ๅœจๆณจๅ†Œไธญๅฟƒไธญๆœ็ดขๆŠ€่ƒฝ')
59
+ .argument('<ๅ…ณ้”ฎ่ฏ>', 'ๆœ็ดขๅ…ณ้”ฎ่ฏ๏ผŒๅฆ‚ firewallใ€rpm')
60
+ .addOption(
61
+ new (require('commander').Option)('--url <url>', 'ๆŒ‡ๅฎšๆณจๅ†Œไธญๅฟƒๅœฐๅ€')
62
+ .default(DEFAULT_API_BASE).hideHelp()
63
+ )
64
+ .action(cmdSearch);
142
65
 
143
- const sortedSkills = layers[layer].sort((a, b) => a.path.localeCompare(b.path));
66
+ program.command('info')
67
+ .alias('i')
68
+ .description('ๆŸฅ็œ‹่ฟœ็จ‹ๆŠ€่ƒฝ็š„่ฏฆ็ป†ไฟกๆฏ')
69
+ .argument('<ๆŠ€่ƒฝ่ทฏๅพ„>', 'ๆŠ€่ƒฝ่ทฏๅพ„๏ผŒๅฆ‚ system/network/firewall')
70
+ .addOption(
71
+ new (require('commander').Option)('--url <url>', 'ๆŒ‡ๅฎšๆณจๅ†Œไธญๅฟƒๅœฐๅ€')
72
+ .default(DEFAULT_API_BASE).hideHelp()
73
+ )
74
+ .action(cmdInfo);
144
75
 
145
- for (const { path: skillPath, info } of sortedSkills) {
146
- const status = formatStatus(info.status);
147
- const name = info.name || skillPath;
148
- const version = info.version || '?';
149
- const desc = (info.description || '').substring(0, 50);
76
+ program.command('download')
77
+ .alias('dl')
78
+ .description('ไปŽๆณจๅ†Œไธญๅฟƒไธ‹่ฝฝๆŠ€่ƒฝๅŒ…ๅนถ่งฃๅŽ‹')
79
+ .argument('<ๆŠ€่ƒฝ่ทฏๅพ„>', '่ฆไธ‹่ฝฝ็š„ๆŠ€่ƒฝ่ทฏๅพ„๏ผŒๅฆ‚ core/kernel/kernel-info')
80
+ .option('-t, --target <็›ฎๅฝ•>', 'ๆŒ‡ๅฎš่งฃๅŽ‹็›ฎๅฝ•๏ผˆ้ป˜่ฎคไธบๅฝ“ๅ‰็›ฎๅฝ•๏ผ‰')
81
+ .option('--platform <ๅนณๅฐ>', 'ๆŒ‡ๅฎšๅนณๅฐ๏ผŒ่‡ชๅŠจไธ‹่ฝฝๅˆฐ .<ๅนณๅฐ>/skills/ ็›ฎๅฝ•๏ผŒๅฆ‚ --platform qoder')
82
+ .addOption(
83
+ new (require('commander').Option)('--url <url>', 'ๆŒ‡ๅฎšๆณจๅ†Œไธญๅฟƒๅœฐๅ€')
84
+ .default(DEFAULT_API_BASE).hideHelp()
85
+ )
86
+ .action(cmdDownload);
150
87
 
151
- console.log(` ${chalk.cyan(skillPath)}`);
152
- console.log(` ${name} (${version}) [${status}]`);
153
- console.log(` ${chalk.dim(desc + '...')}`);
154
- }
155
- console.log();
156
- }
157
- }
88
+ program.command('upload')
89
+ .alias('publish')
90
+ .description('ๅฐ†ๆœฌๅœฐๆŠ€่ƒฝๅ‘ๅธƒๅˆฐๆณจๅ†Œไธญๅฟƒ')
91
+ .argument('<ๆŠ€่ƒฝ็›ฎๅฝ•>', '่ฆไธŠไผ ็š„ๆŠ€่ƒฝ็›ฎๅฝ•่ทฏๅพ„๏ผŒๅฆ‚ skills/system/network/firewall')
92
+ .option('-u, --update', 'ๆ›ดๆ–ฐๅทฒๆœ‰ๆŠ€่ƒฝ๏ผˆๅŒๅ่ฆ†็›–๏ผ‰๏ผŒ้œ€ๅ…ˆๆ›ดๆ–ฐ SKILL.md ไธญ็š„็‰ˆๆœฌๅท')
93
+ .option('--token <token>', 'ๆ‰‹ๅŠจๆŒ‡ๅฎš่ฎค่ฏ Token๏ผˆ่ฆ†็›–ๅทฒไฟๅญ˜็š„ๅ‡ญๆฎ๏ผ‰')
94
+ .addOption(
95
+ new (require('commander').Option)('--url <url>', 'ๆŒ‡ๅฎšๆณจๅ†Œไธญๅฟƒๅœฐๅ€')
96
+ .default(DEFAULT_DAILY_API_BASE).hideHelp()
97
+ )
98
+ .action(cmdUpload);
158
99
 
159
- async function cmdSearch(query, options) {
160
- console.log(`\n${chalk.dim(`Searching remote registry for '${query}'...`)}`);
100
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
101
+ // ๆœฌๅœฐๅผ€ๅ‘ๅ‘ฝไปค (Local)
102
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
161
103
 
162
- // API Endpoint: /api/v1/search?q={{skill.name}}
163
- const encodedQuery = encodeURIComponent(query);
164
- const response = await fetchFromApi(`/search?q=${encodedQuery}`, options);
104
+ program.command('create')
105
+ .alias('new')
106
+ .description('ๅˆ›ๅปบๆŠ€่ƒฝ่„šๆ‰‹ๆžถ๏ผˆ็”Ÿๆˆ SKILL.md ๆจกๆฟ๏ผ‰')
107
+ .argument('<ๆŠ€่ƒฝ่ทฏๅพ„>', 'ๆŠ€่ƒฝ่ทฏๅพ„๏ผŒๆ ผๅผ๏ผš<ๅฑ‚็บง>/<ๅˆ†็ฑป>/<ๅ็งฐ>๏ผŒๅฆ‚ system/security/cve-repair')
108
+ .option('-f, --force', 'ๅฆ‚ๆžœ็›ฎๅฝ•ๅทฒๅญ˜ๅœจๅˆ™ๅผบๅˆถ่ฆ†็›–')
109
+ .action(cmdCreate);
165
110
 
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 || [];
111
+ program.command('validate')
112
+ .alias('check')
113
+ .description('ๆ ก้ชŒๆŠ€่ƒฝๆ˜ฏๅฆ็ฌฆๅˆ Skill-OS ่ง„่Œƒ')
114
+ .argument('<ๆŠ€่ƒฝ็›ฎๅฝ•>', '่ฆๆ ก้ชŒ็š„ๆŠ€่ƒฝ็›ฎๅฝ•่ทฏๅพ„๏ผŒๅฆ‚ skills/system/security/cve-repair')
115
+ .action(cmdValidate);
169
116
 
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 });
117
+ program.command('sync')
118
+ .description('ๆ‰ซๆ skills/ ็›ฎๅฝ•๏ผŒ่‡ชๅŠจ็”Ÿๆˆ SKILL_INDEX.json')
119
+ .action(cmdSync);
120
+
121
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
122
+ // Shell ๅ‘ฝไปค่กฅๅ…จ
123
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
124
+
125
+ program.command('completion')
126
+ .description('่พ“ๅ‡บ Shell ๅ‘ฝไปค่กฅๅ…จ่„šๆœฌ๏ผˆๆ”ฏๆŒ bash/zsh/fish๏ผ‰')
127
+ .option('--shell <shell>', 'ๆŒ‡ๅฎš Shell ็ฑปๅž‹๏ผšbashใ€zshใ€fish๏ผˆ้ป˜่ฎค่‡ชๅŠจๆฃ€ๆต‹๏ผ‰')
128
+ .action((options) => {
129
+ const shell = options.shell || detectShell();
130
+
131
+ if (shell === 'fish') {
132
+ console.log(generateFishCompletion());
133
+ } else if (shell === 'zsh') {
134
+ console.log(generateZshCompletion());
135
+ } else {
136
+ console.log(generateBashCompletion());
175
137
  }
176
- }
177
138
 
178
- if (matches.length === 0) {
179
- console.log(chalk.yellow(`No skills found matching '${query}'`));
180
- return;
181
- }
139
+ const installHint = {
140
+ bash: "eval \"$(skill-os completion --shell bash)\" # ๆทปๅŠ ๅˆฐ ~/.bashrc",
141
+ zsh: "eval \"$(skill-os completion --shell zsh)\" # ๆทปๅŠ ๅˆฐ ~/.zshrc",
142
+ fish: "skill-os completion --shell fish | source # ๆˆ–ไฟๅญ˜ๅˆฐ ~/.config/fish/completions/skill-os.fish",
143
+ };
182
144
 
183
- console.log(`\n${chalk.bold(`๐Ÿ” Search Results for '${query}'`)}`);
184
- console.log(`${chalk.dim(`Found ${matches.length} skill(s)`)}\n`);
145
+ process.stderr.write(`\n# ๅฎ‰่ฃ…ๆ็คบ (ๆทปๅŠ ๅˆฐ shell ้…็ฝฎๆ–‡ไปถ):\n# ${installHint[shell] || installHint.bash}\n`);
146
+ });
185
147
 
186
- for (const info of matches) {
187
- const skillPath = info.path || info.name; // Fallback to name if path isn't provided separately
188
- const icon = formatLayerIcon(skillPath);
189
- const status = formatStatus(info.status);
190
- const version = info.version || '?';
191
- const desc = info.description || '';
148
+ // โ”€โ”€ ่กฅๅ…จ่„šๆœฌ็”Ÿๆˆ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
192
149
 
193
- console.log(`${icon} ${chalk.cyan(skillPath)}`);
194
- console.log(` ${chalk.bold(info.name)} (${version}) [${status}]`);
195
- console.log(` ${desc}\n`);
196
- }
150
+ function detectShell() {
151
+ const shellEnv = process.env.SHELL || '';
152
+ if (shellEnv.includes('fish')) return 'fish';
153
+ if (shellEnv.includes('zsh')) return 'zsh';
154
+ return 'bash';
197
155
  }
198
156
 
199
- async function cmdInfo(skillPath, options) {
200
- console.log(`\n${chalk.dim(`Fetching details from remote registry...`)}`);
201
-
202
- // API Endpoint: /api/v1/skills/{{skill.path}}/content
203
- const info = await fetchFromApi(`/skills/${skillPath}/content`, options);
204
-
205
- const icon = formatLayerIcon(skillPath);
206
- const status = formatStatus(info.status || info.metadata?.status);
207
- const metadata = info.metadata || info || {}; // Handle if nested or flat
208
-
209
- console.log(`\n${icon} ${chalk.bold(metadata.name || skillPath)}`);
210
- console.log(chalk.dim('โ”€'.repeat(40)));
211
- console.log(` Path: ${chalk.cyan(skillPath)}`);
212
- console.log(` Version: ${metadata.version || 'unknown'}`);
213
- console.log(` Status: ${status}`);
214
- console.log(` Description: ${metadata.description || 'No description'}`);
215
-
216
- if (metadata.dependencies && metadata.dependencies.length > 0) {
217
- console.log(` Dependencies: ${metadata.dependencies.join(', ')}`);
218
- } else {
219
- console.log(` Dependencies: ${chalk.dim('None')}`);
220
- }
221
-
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]')}`);
225
- }
226
- console.log();
157
+ const COMMANDS = [
158
+ { name: 'login', desc: '็™ปๅฝ•ๆณจๅ†Œไธญๅฟƒ' },
159
+ { name: 'logout', desc: 'ๆณจ้”€็™ปๅฝ•' },
160
+ { name: 'list', desc: 'ๅˆ—ๅ‡บๆ‰€ๆœ‰ๆŠ€่ƒฝ' },
161
+ { name: 'search', desc: 'ๆœ็ดขๆŠ€่ƒฝ' },
162
+ { name: 'info', desc: 'ๆŸฅ็œ‹ๆŠ€่ƒฝ่ฏฆๆƒ…' },
163
+ { name: 'download', desc: 'ไธ‹่ฝฝๆŠ€่ƒฝๅŒ…' },
164
+ { name: 'upload', desc: 'ๅ‘ๅธƒๆŠ€่ƒฝๅˆฐๆณจๅ†Œไธญๅฟƒ' },
165
+ { name: 'create', desc: 'ๅˆ›ๅปบๆŠ€่ƒฝ่„šๆ‰‹ๆžถ' },
166
+ { name: 'validate', desc: 'ๆ ก้ชŒๆŠ€่ƒฝ่ง„่Œƒ' },
167
+ { name: 'sync', desc: 'ๅŒๆญฅๆŠ€่ƒฝ็ดขๅผ•' },
168
+ { name: 'completion', desc: '่พ“ๅ‡บๅ‘ฝไปค่กฅๅ…จ่„šๆœฌ' },
169
+ // ๅˆซๅ
170
+ { name: 'ls', desc: 'list ็š„ๅˆซๅ' },
171
+ { name: 's', desc: 'search ็š„ๅˆซๅ' },
172
+ { name: 'i', desc: 'info ็š„ๅˆซๅ' },
173
+ { name: 'dl', desc: 'download ็š„ๅˆซๅ' },
174
+ { name: 'publish', desc: 'upload ็š„ๅˆซๅ' },
175
+ { name: 'new', desc: 'create ็š„ๅˆซๅ' },
176
+ { name: 'check', desc: 'validate ็š„ๅˆซๅ' },
177
+ ];
178
+
179
+ function generateBashCompletion() {
180
+ const cmds = COMMANDS.map(c => c.name).join(' ');
181
+ return `
182
+ # skill-os bash completion
183
+ _skill_os_completions() {
184
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
185
+ if [ "\${#COMP_WORDS[@]}" -eq 2 ]; then
186
+ COMPREPLY=($(compgen -W "${cmds}" -- "\${cur}"))
187
+ fi
227
188
  }
228
-
229
-
230
-
231
- function cmdCreate(skillPath, options) {
232
- const targetDir = path.resolve(skillPath);
233
-
234
- if (fs.existsSync(targetDir)) {
235
- if (!options.force) {
236
- console.error(chalk.red(`Error: Directory already exists: ${targetDir}`));
237
- console.log(chalk.dim("Use --force to overwrite"));
238
- process.exit(1);
239
- }
240
- fs.rmSync(targetDir, { recursive: true, force: true });
241
- }
242
-
243
- fs.mkdirSync(targetDir, { recursive: true });
244
-
245
- const parts = skillPath.split('/');
246
- if (parts.length < 3) {
247
- console.error(chalk.red("Error: Invalid path format"));
248
- console.log(chalk.dim("Expected: <layer>/<category>/<skill>"));
249
- console.log(chalk.dim("Example: system/security/cve-repair"));
250
- process.exit(1);
251
- }
252
-
253
- const layer = parts[0];
254
- const category = parts[1];
255
- const skillName = parts[parts.length - 1];
256
-
257
- const validLayers = ["core", "system", "runtime", "application"];
258
- if (!validLayers.includes(layer)) {
259
- console.log(chalk.yellow(`Warning: Layer '${layer}' not in spec`));
260
- console.log(chalk.dim(`Valid layers: ${validLayers.join(', ')}`));
261
- }
262
-
263
- const titleCaseName = skillName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
264
-
265
- const skillMdContent = `---
266
- name: ${skillName}
267
- version: 0.1.0
268
- description: TODO: Add skill description here
269
- author: Your Name
270
-
271
- layer: ${layer}
272
- category: ${category}
273
- lifecycle: usage # production | maintenance | operations | usage | meta
274
-
275
- # Tags: first tag MUST be the category name
276
- tags:
277
- - ${category}
278
- - TODO
279
-
280
- status: placeholder
281
- dependencies: []
282
-
283
- # Optional: Permissions (for security classification)
284
- # permissions:
285
- # requires_root: false
286
- # dangerous_operations: []
287
- ---
288
-
289
- # ${titleCaseName}
290
-
291
- TODO: Add skill documentation here.
292
-
293
- ## ่ƒฝๅŠ›ๆฆ‚่งˆ
294
-
295
- - TODO: List capabilities
296
-
297
- ## ไฝฟ็”จ็คบไพ‹
298
-
299
- \`\`\`bash
300
- # TODO: Add usage examples
301
- \`\`\`
302
-
303
- ## TODO
304
-
305
- - [ ] Implement core functionality
306
- - [ ] Add scripts if needed
307
- - [ ] Run: skill-os validate ${skillPath}
308
- - [ ] Run: skill-os sync ${skillPath}
309
- `;
310
-
311
- const skillMdPath = path.join(targetDir, "SKILL.md");
312
- fs.writeFileSync(skillMdPath, skillMdContent, 'utf-8');
313
-
314
- console.log(`\n${chalk.green('โœ“ Created skill scaffold:')}`);
315
- console.log(` ${chalk.cyan(targetDir)}`);
316
- console.log(" โ””โ”€โ”€ SKILL.md");
317
-
318
- console.log(`\n${chalk.bold('๐Ÿ“‹ Next Steps:')}`);
319
- console.log(` 1. Edit ${chalk.cyan(skillMdPath)}`);
320
- console.log(` 2. Validate: ${chalk.dim(`skill-os validate ${skillPath}`)}`);
321
- console.log(` 3. Sync: ${chalk.dim(`skill-os sync ${skillPath}`)}`);
189
+ complete -F _skill_os_completions skill-os
190
+ `.trim();
322
191
  }
323
192
 
324
- function cmdDownload(skillPath, options) {
325
- // 1. Determine target directory
326
- const homeDir = require('os').homedir();
327
- let rawTarget = process.cwd();
328
-
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
- }
336
-
337
- const targetDir = path.resolve(rawTarget);
338
- fs.mkdirSync(targetDir, { recursive: true });
339
-
340
- // 2. Determine API source
341
- const serverUrl = getApiBase(options);
342
- const downloadUrl = `${serverUrl}/skills/${skillPath}/download`;
343
-
344
- console.log(`\n${chalk.bold(`๐Ÿ“ฅ Downloading and extracting ${skillPath}...`)}`);
345
- console.log(` Source: ${chalk.cyan(downloadUrl)}`);
346
- console.log(` Target: ${chalk.cyan(targetDir)}\n`);
347
-
348
- try {
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}`)}`);
354
- } catch (e) {
355
- console.error(`\n${chalk.red(`โœ— Failed to download. The API may have returned an error instead of a tarball.`)}`);
356
- process.exit(1);
357
- }
358
- }
359
-
360
- // ----------------------------------------------------------------------
361
- // Publishing / Uploading
362
- // ----------------------------------------------------------------------
363
-
364
- const FormData = require('form-data');
365
-
366
- function parseSkillMdFrontmatter(skillMdPath) {
367
- if (!fs.existsSync(skillMdPath)) return {};
368
- const content = fs.readFileSync(skillMdPath, 'utf8');
369
- const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
370
- if (!match) return {};
371
-
372
- try {
373
- return yaml.load(match[1]) || {};
374
- } catch (e) {
375
- return {};
376
- }
193
+ function generateZshCompletion() {
194
+ const pairs = COMMANDS.map(c => `'${c.name}:${c.desc}'`).join('\n ');
195
+ return `
196
+ # skill-os zsh completion
197
+ _skill_os() {
198
+ local -a commands
199
+ commands=(
200
+ ${pairs}
201
+ )
202
+ _describe 'skill-os commands' commands
377
203
  }
378
-
379
- async function cmdUpload(skillPath, options) {
380
- const skillDir = path.resolve(skillPath);
381
- const skillMdPath = path.join(skillDir, 'SKILL.md');
382
-
383
- console.log(`\n${chalk.bold(`๐Ÿš€ Publishing Skill: ${skillPath}`)}`);
384
- console.log(`${chalk.dim('โ”€'.repeat(50))}\n`);
385
-
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}`));
391
- process.exit(1);
392
- }
393
-
394
- if (!fs.existsSync(skillMdPath)) {
395
- console.log(`${chalk.red('โœ— Fatal: SKILL.md not found')}`);
396
- process.exit(1);
397
- }
398
-
399
- const fm = parseSkillMdFrontmatter(skillMdPath);
400
- if (!fm || Object.keys(fm).length === 0) {
401
- console.log(`${chalk.red('โœ— SKILL.md missing or invalid frontmatter')}`);
402
- process.exit(1);
403
- }
404
-
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}'` });
409
- }
410
-
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);
415
- }
416
-
417
- console.log(chalk.green('โœ“ Local validation passed.'));
418
-
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);
423
-
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
- }
435
-
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));
443
-
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}).`));
449
- }
450
-
451
- const serverUrl = getApiBase(options);
452
- const uploadUrl = `${serverUrl}/skills/upload`;
453
-
454
- const fetchOptions = {
455
- method: 'POST',
456
- body: form,
457
- headers: form.getHeaders()
458
- };
459
-
460
- if (options.token) {
461
- fetchOptions.headers['Authorization'] = `Bearer ${options.token}`;
462
- }
463
-
464
- console.log(chalk.dim(`\n๐Ÿ“ก Uploading to ${uploadUrl}...`));
465
-
466
- let fetchFn = require('node-fetch');
467
- if (typeof fetchFn !== 'function' && fetchFn.default) {
468
- fetchFn = fetchFn.default;
469
- }
470
-
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}`);
480
- }
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.`));
487
- }
488
- process.exit(1);
489
- } finally {
490
- if (fs.existsSync(tarPath)) {
491
- fs.unlinkSync(tarPath);
492
- }
493
- }
494
- }
495
-
496
- // ----------------------------------------------------------------------
497
- // Validation Helpers
498
- // ----------------------------------------------------------------------
499
-
500
- const { Validator } = require('jsonschema');
501
-
502
- const skillSchema = {
503
- id: "/SkillMetadata",
504
- type: "object",
505
- properties: {
506
- name: { type: "string", minLength: 1 },
507
- version: { type: ["string", "number"] },
508
- description: { type: "string" },
509
- layer: { enum: ["core", "system", "runtime", "application"] },
510
- lifecycle: { enum: ["production", "maintenance", "operations", "usage", "meta"] },
511
- category: { type: ["string", "null"] },
512
- tags: { type: "array", items: { type: "string" } },
513
- status: { enum: ["stable", "beta", "placeholder"] },
514
- dependencies: { type: "array", items: { type: "string" } },
515
- permissions: {
516
- type: "object",
517
- properties: {
518
- requires_root: { type: "boolean" },
519
- dangerous_operations: { type: "array", items: { type: "string" } }
520
- }
521
- }
522
- },
523
- required: ["name", "version", "layer", "lifecycle", "status"]
524
- };
525
-
526
- function validateDirectoryStructure(skillDir) {
527
- const errors = [];
528
- if (!fs.existsSync(skillDir)) {
529
- return { isValid: false, errors: ["Directory does not exist"] };
530
- }
531
-
532
- const skillMd = path.join(skillDir, 'SKILL.md');
533
- if (!fs.existsSync(skillMd)) {
534
- errors.push("Missing SKILL.md file");
535
- } else {
536
- const content = fs.readFileSync(skillMd, 'utf8');
537
- if (!content.includes('---')) {
538
- errors.push("SKILL.md missing frontmatter");
539
- }
540
- }
541
-
542
- return { isValid: errors.length === 0, errors };
204
+ compdef _skill_os skill-os
205
+ `.trim();
543
206
  }
544
207
 
545
- function cmdValidate(skillPath) {
546
- const skillDir = path.resolve(skillPath);
547
- const skillMdPath = path.join(skillDir, 'SKILL.md');
548
-
549
- console.log(`\n${chalk.bold(`๐Ÿ” Validating Skill: ${skillPath}`)}`);
550
- console.log(`${chalk.dim('โ”€'.repeat(50))}\n`);
551
-
552
- const { isValid: isDirValid, errors: dirErrors } = validateDirectoryStructure(skillDir);
553
- if (isDirValid) {
554
- console.log(`${chalk.green('โœ“ Directory structure valid')}`);
555
- } else {
556
- console.log(`${chalk.red('โœ— Directory structure errors:')}`);
557
- dirErrors.forEach(err => console.log(` - ${err}`));
558
- }
559
-
560
- if (!fs.existsSync(skillMdPath)) {
561
- console.log(`${chalk.red('โœ— Fatal: SKILL.md not found')}`);
562
- process.exit(1);
563
- }
564
-
565
- const fm = parseSkillMdFrontmatter(skillMdPath);
566
-
567
- // Check if empty frontmatter
568
- if (!fm || Object.keys(fm).length === 0) {
569
- console.log(`${chalk.red('โœ— SKILL.md missing or invalid frontmatter')}`);
570
- process.exit(1);
571
- }
572
-
573
- const v = new Validator();
574
- const result = v.validate(fm, skillSchema);
575
-
576
- // Custom tag validation: first tag MUST be category
577
- if (fm.category && fm.tags && fm.tags.length > 0) {
578
- if (fm.tags[0] !== fm.category) {
579
- result.errors.push({ stack: `tags[0] must equal category name '${fm.category}'` });
580
- }
581
- }
582
-
583
- if (result.valid && result.errors.length === 0) {
584
- console.log(`${chalk.green('โœ“ SKILL.md is spec compliant')}`);
585
- console.log(`\n${chalk.bold('๐Ÿ“‹ Parsed Metadata:')}`);
586
- console.log(` name: ${fm.name}`);
587
- console.log(` version: ${fm.version}`);
588
- console.log(` layer: ${fm.layer}`);
589
- console.log(` lifecycle: ${fm.lifecycle}`);
590
- console.log(` category: ${fm.category || '(missing)'}`);
591
- console.log(` tags: ${JSON.stringify(fm.tags || [])}`);
592
- if (fm.permissions) {
593
- console.log(` requires_root: ${fm.permissions.requires_root}`);
594
- }
595
- } else {
596
- console.log(`${chalk.red('โœ— SKILL.md validation errors:')}`);
597
- result.errors.forEach(err => console.log(` - ${err.stack.replace('instance.', '')}`));
598
- process.exit(1);
599
- }
600
-
601
- console.log(`\n${chalk.green('โœ… Validation passed!')}\n`);
208
+ function generateFishCompletion() {
209
+ return COMMANDS.map(c =>
210
+ `complete -c skill-os -n "__fish_use_subcommand" -a "${c.name}" -d "${c.desc}"`
211
+ ).join('\n');
602
212
  }
603
213
 
604
- // ----------------------------------------------------------------------
605
- // CLI Setup
606
- // ----------------------------------------------------------------------
607
-
608
- program
609
- .name('skill-os')
610
- .description('Skill-OS CLI');
611
-
612
- program.command('list')
613
- .description('List all available skills from the remote registry')
614
- .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
615
- .action(cmdList);
616
-
617
- program.command('search')
618
- .description('Search for skills in the remote registry')
619
- .argument('<query>', 'Search query')
620
- .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
621
- .action(cmdSearch);
622
-
623
- program.command('info')
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)
627
- .action(cmdInfo);
628
-
629
- program.command('create')
630
- .description('Create a new skill scaffold locally')
631
- .argument('<path>', 'Skill path to create (e.g., system/migration)')
632
- .option('-f, --force', 'Force overwrite if exists', false)
633
- .action(cmdCreate);
634
-
635
- program.command('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)')
639
- .option('--platform <platform>', 'Specify the platform (e.g., qoder will download to .qoder/skills/)')
640
- .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
641
- .action(cmdDownload);
642
-
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);
651
-
652
- program.command('validate')
653
- .description('Validate a skill against spec')
654
- .argument('<path>', 'Skill path to validate (e.g., system/security/cve-repair)')
655
- .action(cmdValidate);
214
+ // โ”€โ”€ ๅฏๅŠจ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
656
215
 
657
216
  program.parse(process.argv);