@dmall-inc/skills-cli 0.1.0
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/README.md +46 -0
- package/bin/skills-cli.js +424 -0
- package/package.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @dmall-inc/skills-cli
|
|
2
|
+
|
|
3
|
+
基于平台技能引用 URL 安装技能到本地。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i -g @dmall-inc/skills-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
或临时执行:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @dmall-inc/skills-cli add https://<host>/skills/@owner/<name> --token <wat_xxx>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 用法
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
skills-cli add <url> [--token <wat_xxx>] [--target-dir <dir>] [--force] [--version <v>]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- `url` 必须是:`https://<host>/skills/@owner/<name>`
|
|
24
|
+
- token 优先级:`--token` > `DMALL_SKILLS_TOKEN`
|
|
25
|
+
- 未传 `--target-dir` 时,默认安装到:
|
|
26
|
+
- `$CODEX_SKILLS_DIR/<owner>--<name>`(若设置)
|
|
27
|
+
- 否则 `~/.codex/skills/<owner>--<name>`
|
|
28
|
+
|
|
29
|
+
## 安装流程
|
|
30
|
+
|
|
31
|
+
1. 解析 URL 得到 `owner + name`
|
|
32
|
+
2. 调用 `GET /api/open/skills/resolve`
|
|
33
|
+
3. 调用 `GET /api/open/skills/{id}/download`
|
|
34
|
+
4. 解压并安装到本地目录
|
|
35
|
+
5. 校验目标目录内存在 `SKILL.md`
|
|
36
|
+
|
|
37
|
+
## 常见问题
|
|
38
|
+
|
|
39
|
+
- 报错 `Missing token`
|
|
40
|
+
- 传 `--token` 或设置 `DMALL_SKILLS_TOKEN`
|
|
41
|
+
- 报错 `HTTP 401/403`
|
|
42
|
+
- token 无效、无权限,或技能对当前 workspace 不可见
|
|
43
|
+
- 报错 `Invalid install URL path`
|
|
44
|
+
- URL 不是 `/skills/@owner/<name>` 结构
|
|
45
|
+
- 报错 ``unzip` command not found`
|
|
46
|
+
- 先在本机安装 `unzip`
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const fsp = require('node:fs/promises');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { execFile } = require('node:child_process');
|
|
8
|
+
const { promisify } = require('node:util');
|
|
9
|
+
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 20_000;
|
|
12
|
+
|
|
13
|
+
function printHelp() {
|
|
14
|
+
console.log(`@dmall-inc/skills-cli\n\nUsage:\n skills-cli add <https://<host>/skills/@owner/name> [--token <wat_xxx>] [--target-dir <dir>] [--force] [--version <v>] [--timeout-ms <ms>] [--verbose]\n\nToken priority:\n 1) --token\n 2) DMALL_SKILLS_TOKEN\n\nTimeout priority:\n 1) --timeout-ms\n 2) DMALL_SKILLS_TIMEOUT_MS\n 3) default 20000ms\n\nExamples:\n skills-cli add https://example.com/skills/@team/report-generator --token wat_xxx\n DMALL_SKILLS_TOKEN=wat_xxx skills-cli add https://example.com/skills/@team/report-generator\n skills-cli add https://example.com/skills/@team/report-generator --verbose --timeout-ms 30000\n`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fail(message, code = 1) {
|
|
18
|
+
console.error(`[skills-cli] ${message}`);
|
|
19
|
+
process.exit(code);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseNonEmptyOptionValue(rest, i, optionName) {
|
|
23
|
+
const value = rest[i + 1];
|
|
24
|
+
if (value == null || String(value).trim() === '' || String(value).startsWith('--')) {
|
|
25
|
+
fail(`Option ${optionName} requires a value.`);
|
|
26
|
+
}
|
|
27
|
+
return String(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parsePositiveInteger(raw, optionName) {
|
|
31
|
+
const n = Number(raw);
|
|
32
|
+
if (!Number.isFinite(n) || n <= 0 || Math.floor(n) !== n) {
|
|
33
|
+
fail(`Option ${optionName} must be a positive integer.`);
|
|
34
|
+
}
|
|
35
|
+
return n;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseArgs(argv) {
|
|
39
|
+
const [, , command, ...rest] = argv;
|
|
40
|
+
if (!command || command === '-h' || command === '--help') {
|
|
41
|
+
return { command: 'help' };
|
|
42
|
+
}
|
|
43
|
+
if (command !== 'add') {
|
|
44
|
+
fail(`Unsupported command: ${command}`);
|
|
45
|
+
}
|
|
46
|
+
const options = {
|
|
47
|
+
token: undefined,
|
|
48
|
+
targetDir: undefined,
|
|
49
|
+
force: false,
|
|
50
|
+
version: undefined,
|
|
51
|
+
timeoutMs: undefined,
|
|
52
|
+
verbose: false,
|
|
53
|
+
};
|
|
54
|
+
const positional = [];
|
|
55
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
56
|
+
const item = rest[i];
|
|
57
|
+
if (item === '--token') {
|
|
58
|
+
options.token = parseNonEmptyOptionValue(rest, i, '--token');
|
|
59
|
+
i += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (item === '--target-dir') {
|
|
63
|
+
options.targetDir = parseNonEmptyOptionValue(rest, i, '--target-dir');
|
|
64
|
+
i += 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (item === '--version') {
|
|
68
|
+
options.version = parseNonEmptyOptionValue(rest, i, '--version');
|
|
69
|
+
i += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (item === '--timeout-ms') {
|
|
73
|
+
const raw = parseNonEmptyOptionValue(rest, i, '--timeout-ms');
|
|
74
|
+
options.timeoutMs = parsePositiveInteger(raw, '--timeout-ms');
|
|
75
|
+
i += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (item === '--force') {
|
|
79
|
+
options.force = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (item === '--verbose') {
|
|
83
|
+
options.verbose = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (item.startsWith('--')) {
|
|
87
|
+
fail(`Unknown option: ${item}`);
|
|
88
|
+
}
|
|
89
|
+
positional.push(item);
|
|
90
|
+
}
|
|
91
|
+
if (positional.length !== 1) {
|
|
92
|
+
fail('`add` requires exactly one URL argument.');
|
|
93
|
+
}
|
|
94
|
+
return { command, url: positional[0], options };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseInstallUrl(rawUrl) {
|
|
98
|
+
let url;
|
|
99
|
+
try {
|
|
100
|
+
url = new URL(rawUrl);
|
|
101
|
+
} catch {
|
|
102
|
+
fail('Invalid URL. Expected format: https://<host>/skills/@owner/<name>');
|
|
103
|
+
}
|
|
104
|
+
if (url.protocol !== 'https:') {
|
|
105
|
+
fail('Only https:// URLs are supported.');
|
|
106
|
+
}
|
|
107
|
+
const match = url.pathname.match(/^\/skills\/@([a-zA-Z0-9._-]+)\/([a-z0-9](?:[a-z0-9-]{1,62}[a-z0-9])?)\/?$/);
|
|
108
|
+
if (!match) {
|
|
109
|
+
fail('Invalid install URL path. Expected: /skills/@owner/<name>');
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
hostOrigin: `${url.protocol}//${url.host}`,
|
|
113
|
+
owner: match[1],
|
|
114
|
+
name: match[2],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveToken(optionsToken) {
|
|
119
|
+
const t = (optionsToken || process.env.DMALL_SKILLS_TOKEN || '').trim();
|
|
120
|
+
if (!t) {
|
|
121
|
+
fail('Missing token. Pass --token or set DMALL_SKILLS_TOKEN.');
|
|
122
|
+
}
|
|
123
|
+
return t;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resolveTimeoutMs(optionsTimeoutMs) {
|
|
127
|
+
if (Number.isFinite(optionsTimeoutMs)) {
|
|
128
|
+
return optionsTimeoutMs;
|
|
129
|
+
}
|
|
130
|
+
const envTimeoutRaw = (process.env.DMALL_SKILLS_TIMEOUT_MS || '').trim();
|
|
131
|
+
if (!envTimeoutRaw) {
|
|
132
|
+
return DEFAULT_TIMEOUT_MS;
|
|
133
|
+
}
|
|
134
|
+
return parsePositiveInteger(envTimeoutRaw, 'DMALL_SKILLS_TIMEOUT_MS');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function debugLog(enabled, message) {
|
|
138
|
+
if (enabled) {
|
|
139
|
+
console.log(`[skills-cli][debug] ${message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function buildHttpFailureMessage(prefix, status, bodyPreview) {
|
|
144
|
+
const hints = [];
|
|
145
|
+
if (status === 401 || status === 403) {
|
|
146
|
+
hints.push('token 无效、过期,或对当前 workspace 无权限');
|
|
147
|
+
}
|
|
148
|
+
if (status === 404) {
|
|
149
|
+
hints.push('owner/name 不存在,或技能对当前 token 不可见');
|
|
150
|
+
}
|
|
151
|
+
const hintText = hints.length > 0 ? `;可能原因:${hints.join(';')}` : '';
|
|
152
|
+
return `${prefix}: HTTP ${status}${hintText}${bodyPreview ? `;response=${bodyPreview}` : ''}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeTextPreview(text, maxLen = 500) {
|
|
156
|
+
if (!text) return '';
|
|
157
|
+
const oneLine = String(text).replace(/\s+/g, ' ').trim();
|
|
158
|
+
if (oneLine.length <= maxLen) return oneLine;
|
|
159
|
+
return `${oneLine.slice(0, maxLen)}... (truncated, total=${oneLine.length})`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function extractErrorMessageFromEnvelope(text) {
|
|
163
|
+
if (!text) return null;
|
|
164
|
+
try {
|
|
165
|
+
const json = JSON.parse(text);
|
|
166
|
+
const msg = typeof json?.message === 'string' ? json.message.trim() : '';
|
|
167
|
+
const errCode = typeof json?.errorCode === 'string' ? json.errorCode.trim() : '';
|
|
168
|
+
if (!msg && !errCode) return null;
|
|
169
|
+
return `${msg || '请求失败'}${errCode ? ` (${errCode})` : ''}`;
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function requestWithTimeout(url, init, timeoutMs) {
|
|
176
|
+
const controller = new AbortController();
|
|
177
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
178
|
+
try {
|
|
179
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (error && error.name === 'AbortError') {
|
|
182
|
+
throw new Error(`Request timeout after ${timeoutMs}ms: ${url}`);
|
|
183
|
+
}
|
|
184
|
+
throw error;
|
|
185
|
+
} finally {
|
|
186
|
+
clearTimeout(timer);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function requestJson(url, token, timeoutMs, verbose) {
|
|
191
|
+
debugLog(verbose, `GET ${url}`);
|
|
192
|
+
const response = await requestWithTimeout(
|
|
193
|
+
url,
|
|
194
|
+
{
|
|
195
|
+
method: 'GET',
|
|
196
|
+
headers: {
|
|
197
|
+
Authorization: `Bearer ${token}`,
|
|
198
|
+
Accept: 'application/json',
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
timeoutMs
|
|
202
|
+
);
|
|
203
|
+
const text = await response.text();
|
|
204
|
+
if (!response.ok) {
|
|
205
|
+
const envelopeError = extractErrorMessageFromEnvelope(text);
|
|
206
|
+
if (envelopeError) {
|
|
207
|
+
throw new Error(`${envelopeError} (HTTP ${response.status})`);
|
|
208
|
+
}
|
|
209
|
+
throw new Error(buildHttpFailureMessage('Request failed', response.status, normalizeTextPreview(text)));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let json;
|
|
213
|
+
try {
|
|
214
|
+
json = JSON.parse(text);
|
|
215
|
+
} catch {
|
|
216
|
+
throw new Error(`Invalid JSON response: ${normalizeTextPreview(text, 300)}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const code = Number(json?.code);
|
|
220
|
+
if (!Number.isFinite(code) || code !== 200) {
|
|
221
|
+
const msg = typeof json?.message === 'string' ? json.message : 'Request failed';
|
|
222
|
+
const errCode = json?.errorCode ? ` (${json.errorCode})` : '';
|
|
223
|
+
throw new Error(`${msg}${errCode}`);
|
|
224
|
+
}
|
|
225
|
+
return json.data;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isLikelyZip(buffer) {
|
|
229
|
+
return (
|
|
230
|
+
buffer &&
|
|
231
|
+
buffer.length >= 4 &&
|
|
232
|
+
buffer[0] === 0x50 &&
|
|
233
|
+
buffer[1] === 0x4b &&
|
|
234
|
+
(buffer[2] === 0x03 || buffer[2] === 0x05 || buffer[2] === 0x07) &&
|
|
235
|
+
(buffer[3] === 0x04 || buffer[3] === 0x06 || buffer[3] === 0x08)
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function downloadZip(url, token, timeoutMs, verbose) {
|
|
240
|
+
debugLog(verbose, `GET ${url}`);
|
|
241
|
+
const response = await requestWithTimeout(
|
|
242
|
+
url,
|
|
243
|
+
{
|
|
244
|
+
method: 'GET',
|
|
245
|
+
headers: {
|
|
246
|
+
Authorization: `Bearer ${token}`,
|
|
247
|
+
Accept: 'application/zip',
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
timeoutMs
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
if (!response.ok) {
|
|
254
|
+
const text = await response.text();
|
|
255
|
+
const envelopeError = extractErrorMessageFromEnvelope(text);
|
|
256
|
+
if (envelopeError) {
|
|
257
|
+
throw new Error(`Download failed: ${envelopeError} (HTTP ${response.status})`);
|
|
258
|
+
}
|
|
259
|
+
throw new Error(buildHttpFailureMessage('Download failed', response.status, normalizeTextPreview(text)));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
263
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
264
|
+
if (!isLikelyZip(buffer)) {
|
|
265
|
+
throw new Error('Download failed: response is not a valid zip payload.');
|
|
266
|
+
}
|
|
267
|
+
return buffer;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function unzipToDir(zipPath, destDir) {
|
|
271
|
+
await fsp.mkdir(destDir, { recursive: true });
|
|
272
|
+
try {
|
|
273
|
+
await execFileAsync('unzip', ['-q', zipPath, '-d', destDir]);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (error && /ENOENT/.test(String(error))) {
|
|
276
|
+
throw new Error('`unzip` command not found. Please install unzip and retry.');
|
|
277
|
+
}
|
|
278
|
+
const stderr = error && error.stderr ? String(error.stderr) : String(error);
|
|
279
|
+
throw new Error(`Unzip failed: ${normalizeTextPreview(stderr)}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function findSkillMdFiles(rootDir) {
|
|
284
|
+
const found = [];
|
|
285
|
+
async function walk(current) {
|
|
286
|
+
const entries = await fsp.readdir(current, { withFileTypes: true });
|
|
287
|
+
for (const entry of entries) {
|
|
288
|
+
const full = path.join(current, entry.name);
|
|
289
|
+
if (entry.isDirectory()) {
|
|
290
|
+
await walk(full);
|
|
291
|
+
} else if (entry.isFile() && entry.name === 'SKILL.md') {
|
|
292
|
+
found.push(full);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
await walk(rootDir);
|
|
297
|
+
return found;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function chooseSkillRoot(extractDir, skillMdFiles) {
|
|
301
|
+
if (skillMdFiles.length === 0) {
|
|
302
|
+
throw new Error('Extracted package does not contain SKILL.md');
|
|
303
|
+
}
|
|
304
|
+
const directRootSkillMd = skillMdFiles.find((file) => path.dirname(file) === extractDir);
|
|
305
|
+
const selected = directRootSkillMd || [...skillMdFiles].sort((a, b) => a.length - b.length)[0];
|
|
306
|
+
return { root: path.dirname(selected), skillMd: selected };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function copyDirAtomically(src, dest, force) {
|
|
310
|
+
const exists = fs.existsSync(dest);
|
|
311
|
+
if (exists && !force) {
|
|
312
|
+
throw new Error(`Target already exists: ${dest}. Use --force to overwrite.`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const parentDir = path.dirname(dest);
|
|
316
|
+
await fsp.mkdir(parentDir, { recursive: true });
|
|
317
|
+
|
|
318
|
+
const stagingDir = path.join(parentDir, `.${path.basename(dest)}.tmp-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`);
|
|
319
|
+
await fsp.cp(src, stagingDir, { recursive: true });
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
if (exists && force) {
|
|
323
|
+
await fsp.rm(dest, { recursive: true, force: true });
|
|
324
|
+
}
|
|
325
|
+
await fsp.rename(stagingDir, dest);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
await fsp.rm(stagingDir, { recursive: true, force: true });
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function resolveTargetDir(owner, name, explicitDir) {
|
|
333
|
+
if (explicitDir && explicitDir.trim()) {
|
|
334
|
+
return path.resolve(explicitDir.trim());
|
|
335
|
+
}
|
|
336
|
+
const codexHome = process.env.CODEX_HOME
|
|
337
|
+
? path.resolve(process.env.CODEX_HOME)
|
|
338
|
+
: path.join(os.homedir(), '.codex');
|
|
339
|
+
const skillsDir = process.env.CODEX_SKILLS_DIR
|
|
340
|
+
? path.resolve(process.env.CODEX_SKILLS_DIR)
|
|
341
|
+
: path.join(codexHome, 'skills');
|
|
342
|
+
return path.join(skillsDir, `${owner}--${name}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function addCommand(rawUrl, options) {
|
|
346
|
+
const parsed = parseInstallUrl(rawUrl);
|
|
347
|
+
const token = resolveToken(options.token);
|
|
348
|
+
const timeoutMs = resolveTimeoutMs(options.timeoutMs);
|
|
349
|
+
|
|
350
|
+
const resolveUrl = new URL('/api/open/skills/resolve', parsed.hostOrigin);
|
|
351
|
+
resolveUrl.searchParams.set('owner', parsed.owner);
|
|
352
|
+
resolveUrl.searchParams.set('name', parsed.name);
|
|
353
|
+
|
|
354
|
+
console.log(`[skills-cli] Resolving ${parsed.owner}/${parsed.name} ...`);
|
|
355
|
+
const resolved = await requestJson(resolveUrl.toString(), token, timeoutMs, options.verbose);
|
|
356
|
+
const skillId = String(resolved?.id || '').trim();
|
|
357
|
+
if (!skillId) {
|
|
358
|
+
throw new Error('Resolve response missing skill id');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const resolvedOwner = String(resolved?.owner || '').trim();
|
|
362
|
+
const resolvedName = String(resolved?.name || '').trim();
|
|
363
|
+
if ((resolvedOwner && resolvedOwner !== parsed.owner) || (resolvedName && resolvedName !== parsed.name)) {
|
|
364
|
+
debugLog(options.verbose, `Resolve canonical ref: owner=${resolvedOwner || parsed.owner}, name=${resolvedName || parsed.name}`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const downloadUrl = new URL(`/api/open/skills/${encodeURIComponent(skillId)}/download`, parsed.hostOrigin);
|
|
368
|
+
if (options.version && options.version.trim()) {
|
|
369
|
+
downloadUrl.searchParams.set('version', options.version.trim());
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
console.log(`[skills-cli] Downloading skill ${skillId} ...`);
|
|
373
|
+
const zipBuffer = await downloadZip(downloadUrl.toString(), token, timeoutMs, options.verbose);
|
|
374
|
+
|
|
375
|
+
const tempDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'skills-cli-'));
|
|
376
|
+
const zipPath = path.join(tempDir, 'skill.zip');
|
|
377
|
+
const extractDir = path.join(tempDir, 'extract');
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
await fsp.writeFile(zipPath, zipBuffer);
|
|
381
|
+
await unzipToDir(zipPath, extractDir);
|
|
382
|
+
|
|
383
|
+
const skillMdFiles = await findSkillMdFiles(extractDir);
|
|
384
|
+
const selected = chooseSkillRoot(extractDir, skillMdFiles);
|
|
385
|
+
const targetDir = resolveTargetDir(parsed.owner, parsed.name, options.targetDir);
|
|
386
|
+
|
|
387
|
+
console.log(`[skills-cli] Installing to ${targetDir} ...`);
|
|
388
|
+
await copyDirAtomically(selected.root, targetDir, options.force);
|
|
389
|
+
|
|
390
|
+
const installedSkillMd = path.join(targetDir, path.relative(selected.root, selected.skillMd));
|
|
391
|
+
if (!fs.existsSync(installedSkillMd)) {
|
|
392
|
+
throw new Error(`Installed package validation failed: SKILL.md not found in ${targetDir}`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
console.log('[skills-cli] Install success.');
|
|
396
|
+
console.log(`[skills-cli] Path: ${targetDir}`);
|
|
397
|
+
if (resolved?.publishedVersion) {
|
|
398
|
+
console.log(`[skills-cli] Published version: ${resolved.publishedVersion}`);
|
|
399
|
+
}
|
|
400
|
+
} finally {
|
|
401
|
+
await fsp.rm(tempDir, { recursive: true, force: true });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function main() {
|
|
406
|
+
if (typeof fetch !== 'function') {
|
|
407
|
+
fail('Global fetch is unavailable. Please use Node.js >= 18.');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const parsed = parseArgs(process.argv);
|
|
411
|
+
if (parsed.command === 'help') {
|
|
412
|
+
printHelp();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (parsed.command === 'add') {
|
|
416
|
+
await addCommand(parsed.url, parsed.options);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
fail(`Unsupported command: ${parsed.command}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
main().catch((error) => {
|
|
423
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
424
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dmall-inc/skills-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install Dmall platform skills from @owner/name URL",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"skills-cli": "bin/skills-cli.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"lint": "node --check bin/skills-cli.js",
|
|
15
|
+
"start": "node bin/skills-cli.js"
|
|
16
|
+
}
|
|
17
|
+
}
|