skill-os 0.1.3 โ†’ 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/skill-os.js CHANGED
@@ -1,667 +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');
53
-
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
- }
89
-
90
- const url = `${baseUrl}${endpoint}`;
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
+ */
91
13
 
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
-
105
- // ----------------------------------------------------------------------
106
- // Commmands Implementations
107
- // ----------------------------------------------------------------------
108
-
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
- }
125
-
126
- console.log(`\n${chalk.bold('๐Ÿ“š Skill-OS Available Skills')}`);
127
- console.log(`${chalk.dim('โ”€'.repeat(60))}\n`);
128
-
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');
134
-
135
- if (!layers[layer]) layers[layer] = [];
136
- layers[layer].push({ path: skillPath, info });
137
- }
138
-
139
- for (const layer of Object.keys(layers).sort()) {
140
- const icon = layerIcons[layer] || "๐Ÿ“„";
141
- console.log(`${chalk.bold(icon + ' ' + layer.toUpperCase())}`);
142
-
143
- const sortedSkills = layers[layer].sort((a, b) => a.path.localeCompare(b.path));
144
-
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);
150
-
151
- console.log(` ${chalk.cyan(skillPath)}`);
152
- console.log(` ${name} (${version}) [${status}]`);
153
- console.log(` ${chalk.dim(desc + '...')}`);
154
- }
155
- console.log();
156
- }
157
- }
158
-
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);
14
+ const { program } = require('commander');
165
15
 
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 || [];
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');
169
21
 
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 });
175
- }
176
- }
22
+ // โ”€โ”€ ็‰ˆๆœฌ & ๆ่ฟฐ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
177
23
 
178
- if (matches.length === 0) {
179
- console.log(chalk.yellow(`No skills found matching '${query}'`));
180
- return;
181
- }
24
+ program
25
+ .name('skill-os')
26
+ .description('Skill-OS CLI โ€” AI-Native OS ๆŠ€่ƒฝ็ฎก็†ๅนณๅฐ')
27
+ .version('0.1.4');
182
28
 
183
- console.log(`\n${chalk.bold(`๐Ÿ” Search Results for '${query}'`)}`);
184
- console.log(`${chalk.dim(`Found ${matches.length} skill(s)`)}\n`);
29
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
30
+ // ่ฎค่ฏๅ‘ฝไปค
31
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
185
32
 
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 || '';
33
+ program.command('login')
34
+ .description('็™ปๅฝ• Skill-OS ๆณจๅ†Œไธญๅฟƒ๏ผˆ้€š่ฟ‡ๆต่งˆๅ™จ BUC SSO ่ฎค่ฏ๏ผ‰')
35
+ .option('--url <url>', 'ๆŒ‡ๅฎšๆœๅŠก็ซฏๅœฐๅ€', DEFAULT_DAILY_API_BASE)
36
+ .action(cmdLogin);
192
37
 
193
- console.log(`${icon} ${chalk.cyan(skillPath)}`);
194
- console.log(` ${chalk.bold(info.name)} (${version}) [${status}]`);
195
- console.log(` ${desc}\n`);
196
- }
197
- }
38
+ program.command('logout')
39
+ .description('ๆณจ้”€็™ปๅฝ•๏ผŒๆธ…้™คๆœฌๅœฐๅ‡ญๆฎ')
40
+ .option('--url <url>', 'ๆŒ‡ๅฎšๆœๅŠก็ซฏๅœฐๅ€', DEFAULT_DAILY_API_BASE)
41
+ .action(cmdLogout);
198
42
 
199
- async function cmdInfo(skillPath, options) {
200
- console.log(`\n${chalk.dim(`Fetching details from remote registry...`)}`);
43
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
44
+ // ่ฟœ็จ‹ๆณจๅ†Œไธญๅฟƒๅ‘ฝไปค (Remote)
45
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
201
46
 
202
- // API Endpoint: /api/v1/skills/{{skill.path}}/content
203
- const info = await fetchFromApi(`/skills/${skillPath}/content`, options);
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);
204
55
 
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
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);
208
65
 
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'}`);
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);
215
75
 
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
- }
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);
221
87
 
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();
227
- }
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);
228
99
 
100
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
101
+ // ๆœฌๅœฐๅผ€ๅ‘ๅ‘ฝไปค (Local)
102
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
229
103
 
104
+ program.command('create')
105
+ .alias('new')
106
+ .description('ๅˆ›ๅปบๆŠ€่ƒฝ่„šๆ‰‹ๆžถ๏ผˆ็”Ÿๆˆ SKILL.md ๆจกๆฟ๏ผ‰')
107
+ .argument('<ๆŠ€่ƒฝ่ทฏๅพ„>', 'ๆŠ€่ƒฝ่ทฏๅพ„๏ผŒๆ ผๅผ๏ผš<ๅฑ‚็บง>/<ๅˆ†็ฑป>/<ๅ็งฐ>๏ผŒๅฆ‚ system/security/cve-repair')
108
+ .option('-f, --force', 'ๅฆ‚ๆžœ็›ฎๅฝ•ๅทฒๅญ˜ๅœจๅˆ™ๅผบๅˆถ่ฆ†็›–')
109
+ .action(cmdCreate);
230
110
 
231
- function cmdCreate(skillPath, options) {
232
- const targetDir = path.resolve(skillPath);
111
+ program.command('validate')
112
+ .alias('check')
113
+ .description('ๆ ก้ชŒๆŠ€่ƒฝๆ˜ฏๅฆ็ฌฆๅˆ Skill-OS ่ง„่Œƒ')
114
+ .argument('<ๆŠ€่ƒฝ็›ฎๅฝ•>', '่ฆๆ ก้ชŒ็š„ๆŠ€่ƒฝ็›ฎๅฝ•่ทฏๅพ„๏ผŒๅฆ‚ skills/system/security/cve-repair')
115
+ .action(cmdValidate);
233
116
 
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);
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());
239
137
  }
240
- fs.rmSync(targetDir, { recursive: true, force: true });
241
- }
242
-
243
- fs.mkdirSync(targetDir, { recursive: true });
244
138
 
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
- }
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
+ };
252
144
 
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');
145
+ process.stderr.write(`\n# ๅฎ‰่ฃ…ๆ็คบ (ๆทปๅŠ ๅˆฐ shell ้…็ฝฎๆ–‡ไปถ):\n# ${installHint[shell] || installHint.bash}\n`);
146
+ });
313
147
 
314
- console.log(`\n${chalk.green('โœ“ Created skill scaffold:')}`);
315
- console.log(` ${chalk.cyan(targetDir)}`);
316
- console.log(" โ””โ”€โ”€ SKILL.md");
148
+ // โ”€โ”€ ่กฅๅ…จ่„šๆœฌ็”Ÿๆˆ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
317
149
 
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}`)}`);
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';
322
155
  }
323
156
 
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
- const tmpZip = path.join(require('os').tmpdir(), `skill-${Date.now()}.zip`);
350
-
351
- // Download the zip file to OS tmp dir
352
- execSync(`curl -s -L -o "${tmpZip}" "${downloadUrl}"`);
353
-
354
- // Extract the zip to target directory
355
- // -q: quiet, -o: overwrite
356
- execSync(`unzip -q -o "${tmpZip}" -d "${targetDir}"`);
357
-
358
- // Cleanup temp file
359
- if (fs.existsSync(tmpZip)) {
360
- fs.unlinkSync(tmpZip);
361
- }
362
-
363
- console.log(`\n${chalk.green(`โœ“ Successfully downloaded and extracted to: ${targetDir}`)}`);
364
- } catch (e) {
365
- console.error(`\n${chalk.red(`โœ— Failed to download. The API may have returned an error, or 'unzip' is not installed.`)}`);
366
- process.exit(1);
367
- }
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
368
188
  }
369
-
370
- // ----------------------------------------------------------------------
371
- // Publishing / Uploading
372
- // ----------------------------------------------------------------------
373
-
374
- const FormData = require('form-data');
375
-
376
- function parseSkillMdFrontmatter(skillMdPath) {
377
- if (!fs.existsSync(skillMdPath)) return {};
378
- const content = fs.readFileSync(skillMdPath, 'utf8');
379
- const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
380
- if (!match) return {};
381
-
382
- try {
383
- return yaml.load(match[1]) || {};
384
- } catch (e) {
385
- return {};
386
- }
189
+ complete -F _skill_os_completions skill-os
190
+ `.trim();
387
191
  }
388
192
 
389
- async function cmdUpload(skillPath, options) {
390
- const skillDir = path.resolve(skillPath);
391
- const skillMdPath = path.join(skillDir, 'SKILL.md');
392
-
393
- console.log(`\n${chalk.bold(`๐Ÿš€ Publishing Skill: ${skillPath}`)}`);
394
- console.log(`${chalk.dim('โ”€'.repeat(50))}\n`);
395
-
396
- // 1. Validation (Requires hoisted functions)
397
- const { isValid: isDirValid, errors: dirErrors } = validateDirectoryStructure(skillDir);
398
- if (!isDirValid) {
399
- console.error(chalk.red('โœ— Validation failed: Directory structure errors:'));
400
- dirErrors.forEach(err => console.error(` - ${err}`));
401
- process.exit(1);
402
- }
403
-
404
- if (!fs.existsSync(skillMdPath)) {
405
- console.log(`${chalk.red('โœ— Fatal: SKILL.md not found')}`);
406
- process.exit(1);
407
- }
408
-
409
- const fm = parseSkillMdFrontmatter(skillMdPath);
410
- if (!fm || Object.keys(fm).length === 0) {
411
- console.log(`${chalk.red('โœ— SKILL.md missing or invalid frontmatter')}`);
412
- process.exit(1);
413
- }
414
-
415
- const v = new Validator();
416
- const result = v.validate(fm, skillSchema);
417
- if (fm.category && fm.tags && fm.tags.length > 0 && fm.tags[0] !== fm.category) {
418
- result.errors.push({ stack: `tags[0] must equal category name '${fm.category}'` });
419
- }
420
-
421
- if (!result.valid || result.errors.length > 0) {
422
- console.error(chalk.red('โœ— Validation failed: SKILL.md has errors:'));
423
- result.errors.forEach(err => console.error(` - ${err.stack.replace('instance.', '')}`));
424
- process.exit(1);
425
- }
426
-
427
- console.log(chalk.green('โœ“ Local validation passed.'));
428
-
429
- // 2. Prepare tarball
430
- const tarFilename = `${fm.name}-${fm.version}.tar.gz`;
431
- const tmpDir = require('os').tmpdir();
432
- const tarPath = path.join(tmpDir, tarFilename);
433
-
434
- console.log(chalk.dim(`๐Ÿ“ฆ Creating package archive...`));
435
- try {
436
- // -C changes to directory, . archives contents
437
- // COPYFILE_DISABLE=1 prevents macOS tar from including ._* extended attribute files
438
- execSync(`tar -czf "${tarPath}" -C "${skillDir}" .`, {
439
- env: { ...process.env, COPYFILE_DISABLE: '1' }
440
- });
441
- } catch (e) {
442
- console.error(chalk.red('โœ— Failed to create tar archive.'));
443
- process.exit(1);
444
- }
445
-
446
- // 3. Prepare Form Data
447
- const form = new FormData();
448
- form.append('package', fs.createReadStream(tarPath), {
449
- filename: tarFilename,
450
- contentType: 'application/gzip'
451
- });
452
- form.append('metadata', JSON.stringify(fm));
453
-
454
- if (options.update) {
455
- form.append('is_update', 'true');
456
- console.log(chalk.yellow(`โš  Uploading as an UPDATE (Version: ${fm.version}). Ensure the version number has been bumped!`));
457
- } else {
458
- console.log(chalk.cyan(`โœจ Uploading as a NEW skill (Version: ${fm.version}).`));
459
- }
460
-
461
- const serverUrl = getApiBase(options);
462
- const uploadUrl = `${serverUrl}/skills/upload`;
463
-
464
- const fetchOptions = {
465
- method: 'POST',
466
- body: form,
467
- headers: form.getHeaders()
468
- };
469
-
470
- if (options.token) {
471
- fetchOptions.headers['Authorization'] = `Bearer ${options.token}`;
472
- }
473
-
474
- console.log(chalk.dim(`\n๐Ÿ“ก Uploading to ${uploadUrl}...`));
475
-
476
- let fetchFn = require('node-fetch');
477
- if (typeof fetchFn !== 'function' && fetchFn.default) {
478
- fetchFn = fetchFn.default;
479
- }
480
-
481
- try {
482
- const response = await fetchFn(uploadUrl, fetchOptions);
483
- if (!response.ok) {
484
- let errorMsg = response.statusText;
485
- try {
486
- const errBody = await response.json();
487
- if (errBody.error || errBody.message) errorMsg = errBody.error || errBody.message;
488
- } catch (e) { }
489
- throw new Error(`HTTP ${response.status}: ${errorMsg}`);
490
- }
491
- console.log(`\n${chalk.green('โœ… Publish successful!')}`);
492
- } catch (error) {
493
- console.error(chalk.red(`\nโœ— Publish failed.`));
494
- console.error(chalk.red(` ${error.message}`));
495
- if (!options.update && error.message.includes('already exists')) {
496
- console.log(chalk.yellow(`\n๐Ÿ’ก If you intended to update an existing skill, use the --update flag.`));
497
- }
498
- process.exit(1);
499
- } finally {
500
- if (fs.existsSync(tarPath)) {
501
- fs.unlinkSync(tarPath);
502
- }
503
- }
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
504
203
  }
505
-
506
- // ----------------------------------------------------------------------
507
- // Validation Helpers
508
- // ----------------------------------------------------------------------
509
-
510
- const { Validator } = require('jsonschema');
511
-
512
- const skillSchema = {
513
- id: "/SkillMetadata",
514
- type: "object",
515
- properties: {
516
- name: { type: "string", minLength: 1 },
517
- version: { type: ["string", "number"] },
518
- description: { type: "string" },
519
- layer: { enum: ["core", "system", "runtime", "application"] },
520
- lifecycle: { enum: ["production", "maintenance", "operations", "usage", "meta"] },
521
- category: { type: ["string", "null"] },
522
- tags: { type: "array", items: { type: "string" } },
523
- status: { enum: ["stable", "beta", "placeholder"] },
524
- dependencies: { type: "array", items: { type: "string" } },
525
- permissions: {
526
- type: "object",
527
- properties: {
528
- requires_root: { type: "boolean" },
529
- dangerous_operations: { type: "array", items: { type: "string" } }
530
- }
531
- }
532
- },
533
- required: ["name", "version", "layer", "lifecycle", "status"]
534
- };
535
-
536
- function validateDirectoryStructure(skillDir) {
537
- const errors = [];
538
- if (!fs.existsSync(skillDir)) {
539
- return { isValid: false, errors: ["Directory does not exist"] };
540
- }
541
-
542
- const skillMd = path.join(skillDir, 'SKILL.md');
543
- if (!fs.existsSync(skillMd)) {
544
- errors.push("Missing SKILL.md file");
545
- } else {
546
- const content = fs.readFileSync(skillMd, 'utf8');
547
- if (!content.includes('---')) {
548
- errors.push("SKILL.md missing frontmatter");
549
- }
550
- }
551
-
552
- return { isValid: errors.length === 0, errors };
204
+ compdef _skill_os skill-os
205
+ `.trim();
553
206
  }
554
207
 
555
- function cmdValidate(skillPath) {
556
- const skillDir = path.resolve(skillPath);
557
- const skillMdPath = path.join(skillDir, 'SKILL.md');
558
-
559
- console.log(`\n${chalk.bold(`๐Ÿ” Validating Skill: ${skillPath}`)}`);
560
- console.log(`${chalk.dim('โ”€'.repeat(50))}\n`);
561
-
562
- const { isValid: isDirValid, errors: dirErrors } = validateDirectoryStructure(skillDir);
563
- if (isDirValid) {
564
- console.log(`${chalk.green('โœ“ Directory structure valid')}`);
565
- } else {
566
- console.log(`${chalk.red('โœ— Directory structure errors:')}`);
567
- dirErrors.forEach(err => console.log(` - ${err}`));
568
- }
569
-
570
- if (!fs.existsSync(skillMdPath)) {
571
- console.log(`${chalk.red('โœ— Fatal: SKILL.md not found')}`);
572
- process.exit(1);
573
- }
574
-
575
- const fm = parseSkillMdFrontmatter(skillMdPath);
576
-
577
- // Check if empty frontmatter
578
- if (!fm || Object.keys(fm).length === 0) {
579
- console.log(`${chalk.red('โœ— SKILL.md missing or invalid frontmatter')}`);
580
- process.exit(1);
581
- }
582
-
583
- const v = new Validator();
584
- const result = v.validate(fm, skillSchema);
585
-
586
- // Custom tag validation: first tag MUST be category
587
- if (fm.category && fm.tags && fm.tags.length > 0) {
588
- if (fm.tags[0] !== fm.category) {
589
- result.errors.push({ stack: `tags[0] must equal category name '${fm.category}'` });
590
- }
591
- }
592
-
593
- if (result.valid && result.errors.length === 0) {
594
- console.log(`${chalk.green('โœ“ SKILL.md is spec compliant')}`);
595
- console.log(`\n${chalk.bold('๐Ÿ“‹ Parsed Metadata:')}`);
596
- console.log(` name: ${fm.name}`);
597
- console.log(` version: ${fm.version}`);
598
- console.log(` layer: ${fm.layer}`);
599
- console.log(` lifecycle: ${fm.lifecycle}`);
600
- console.log(` category: ${fm.category || '(missing)'}`);
601
- console.log(` tags: ${JSON.stringify(fm.tags || [])}`);
602
- if (fm.permissions) {
603
- console.log(` requires_root: ${fm.permissions.requires_root}`);
604
- }
605
- } else {
606
- console.log(`${chalk.red('โœ— SKILL.md validation errors:')}`);
607
- result.errors.forEach(err => console.log(` - ${err.stack.replace('instance.', '')}`));
608
- process.exit(1);
609
- }
610
-
611
- 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');
612
212
  }
613
213
 
614
- // ----------------------------------------------------------------------
615
- // CLI Setup
616
- // ----------------------------------------------------------------------
617
-
618
- program
619
- .name('skill-os')
620
- .description('Skill-OS CLI');
621
-
622
- program.command('list')
623
- .description('List all available skills from the remote registry')
624
- .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
625
- .action(cmdList);
626
-
627
- program.command('search')
628
- .description('Search for skills in the remote registry')
629
- .argument('<query>', 'Search query')
630
- .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
631
- .action(cmdSearch);
632
-
633
- program.command('info')
634
- .description('Show skill details from the remote registry')
635
- .argument('<path>', 'Skill path (e.g., package/rpm_search)')
636
- .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
637
- .action(cmdInfo);
638
-
639
- program.command('create')
640
- .description('Create a new skill scaffold locally')
641
- .argument('<path>', 'Skill path to create (e.g., system/migration)')
642
- .option('-f, --force', 'Force overwrite if exists', false)
643
- .action(cmdCreate);
644
-
645
- program.command('download')
646
- .description('Download and extract a skill package from the remote registry')
647
- .argument('<skill_path>', 'Path of the skill to download (e.g., core/kernel/kernel-info)')
648
- .option('-t, --target <dir>', 'Target directory to extract into (defaults to current dir)')
649
- .option('--platform <platform>', 'Specify the platform (e.g., qoder will download to .qoder/skills/)')
650
- .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
651
- .action(cmdDownload);
652
-
653
- program.command('upload')
654
- .alias('publish')
655
- .description('Upload a local skill to the remote registry')
656
- .argument('<path>', 'Skill path to upload (e.g., system/migration)')
657
- .option('-u, --update', 'Publish as an update to an existing skill', false)
658
- .option('--token <token>', 'Authentication token for the remote registry')
659
- .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
660
- .action(cmdUpload);
661
-
662
- program.command('validate')
663
- .description('Validate a skill against spec')
664
- .argument('<path>', 'Skill path to validate (e.g., system/security/cve-repair)')
665
- .action(cmdValidate);
214
+ // โ”€โ”€ ๅฏๅŠจ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
666
215
 
667
216
  program.parse(process.argv);