nvwa-skill-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.
Files changed (3) hide show
  1. package/README.md +103 -0
  2. package/bin/cli.js +531 -0
  3. package/package.json +27 -0
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # nvwa-skill-cli
2
+
3
+ 一个可发布到 npm 的最小 CLI。既支持把 zip 解压到指定目录,也支持从 8188 下载后委托 `skills` 做真正的 agent 安装。
4
+
5
+ ## 功能
6
+
7
+ - 支持 `install <zip-url-or-file>`
8
+ - 支持 `install-skillhub <skillId> [version] --base-url <8188>`
9
+ - 自动下载 zip
10
+ - 自动解压
11
+ - 自动查找 `SKILL.md`
12
+ - 默认优先使用 `SKILL.md` 中的 `name` 作为安装目录名
13
+ - `install-skillhub` 会调用 `skills add`,把 skill 真正安装到 Codex/其他 agent 的 skills 目录
14
+
15
+ ## 本地调试
16
+
17
+ 在当前目录执行:
18
+
19
+ ```bash
20
+ npm install
21
+ node bin/cli.js --help
22
+ node bin/cli.js install ./demo-skill.zip --target ./skills
23
+ node bin/cli.js install-skillhub 2062784556544692224 --base-url http://localhost:8188 -g
24
+ node bin/cli.js install-skillhub 2062784556544692224 1.0.0 --base-url http://localhost:8188 --agent codex -y
25
+ ```
26
+
27
+ 也可以用 `npx` 调试本地包:
28
+
29
+ ```bash
30
+ npx . --help
31
+ npx . install ./demo-skill.zip --target ./skills
32
+ npx . install-skillhub 2062784556544692224 --base-url http://localhost:8188 -g
33
+ npx . install-skillhub 2062784556544692224 1.0.0 --base-url http://localhost:8188 --agent codex -y
34
+ ```
35
+
36
+ ## 命令
37
+
38
+ ```bash
39
+ npx nvwa-skill-cli install <zip-url-or-file> [options]
40
+ npx nvwa-skill-cli install-skillhub <skillId> [version] [options]
41
+ ```
42
+
43
+ ### 参数
44
+
45
+ - `install` 命令参数
46
+ - `--target <dir>`:安装根目录,默认 `./installed-skills`
47
+ - `--name <dirName>`:覆盖安装目录名
48
+ - `--force`:目标目录已存在时覆盖
49
+ - `install-skillhub` 命令参数
50
+ - `--base-url <url>`:`install-skillhub` 使用的 8188 地址,默认 `http://localhost:8188`
51
+ - `--agent <agent>`:要安装到的 agent,默认 `codex`
52
+ - `-g, --global`:安装到全局目录;对于 Codex 即 `~/.codex/skills`
53
+ - `--copy`:调用 `skills add` 时使用复制模式,不走 symlink
54
+ - `-y, --yes`:跳过 `skills` 的交互确认
55
+ - `version`:可选;不传时默认下载技能当前版本
56
+
57
+ ### 示例
58
+
59
+ ```bash
60
+ npx nvwa-skill-cli install https://example.com/demo-skill.zip
61
+ npx nvwa-skill-cli install ./demo-skill.zip --target ./skills
62
+ npx nvwa-skill-cli install https://example.com/demo-skill.zip --name self-improvement
63
+ npx nvwa-skill-cli install-skillhub 2062784556544692224 --base-url http://localhost:8188 -g
64
+ npx nvwa-skill-cli install-skillhub 2062784556544692224 1.0.0 --base-url http://localhost:8188 --agent codex -y
65
+
66
+ ## 安装结果
67
+
68
+ - `install`:只是解压到指定目录,方便检查 zip 内容
69
+ - `install-skillhub`:下载后调用 `skills add <临时目录>`,会真正安装到 agent 的 skills 目录
70
+ - 对 Codex 而言:
71
+ - 项目级安装默认落到 `./.agents/skills`
72
+ - 全局安装 `-g` 落到 `~/.codex/skills`
73
+ ```
74
+
75
+ ## 发布到 npm
76
+
77
+ 1. 修改 `package.json` 中的 `name`,确认 npm 上未被占用
78
+ 2. 按需要调整 `version`
79
+ 3. 登录 npm:
80
+
81
+ ```bash
82
+ npm login
83
+ ```
84
+
85
+ 4. 发布:
86
+
87
+ ```bash
88
+ npm publish
89
+ ```
90
+
91
+ 如果后续需要 scoped package,可以改成:
92
+
93
+ ```json
94
+ {
95
+ "name": "@your-scope/nvwa-skill-cli"
96
+ }
97
+ ```
98
+
99
+ 然后发布:
100
+
101
+ ```bash
102
+ npm publish --access public
103
+ ```
package/bin/cli.js ADDED
@@ -0,0 +1,531 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const fsp = require("fs/promises");
5
+ const http = require("http");
6
+ const https = require("https");
7
+ const os = require("os");
8
+ const path = require("path");
9
+ const { spawn } = require("child_process");
10
+ const { pipeline } = require("stream/promises");
11
+ const AdmZip = require("adm-zip");
12
+
13
+ const HELP_TEXT = `
14
+ Usage:
15
+ nvwa-skill install <zip-url-or-file> [options]
16
+ nvwa-skill install-skillhub <skillId> [version] [options]
17
+ nvwa-skill help
18
+
19
+ Commands:
20
+ install Download or read a zip, extract it, verify SKILL.md, and install it
21
+ install-skillhub Download a skill package from 8188 anonymous endpoints and install it
22
+ help Show help
23
+
24
+ Options:
25
+ --target <dir> Installation root directory (default: ./installed-skills)
26
+ --name <dirName> Override installed directory name
27
+ --base-url <url> 8188 base URL for install-skillhub (default: http://localhost:8188)
28
+ --agent <agent> Target agent for install-skillhub (default: codex)
29
+ -g, --global Install to global agent directory
30
+ --copy Copy files instead of symlinking when using skills
31
+ -y, --yes Skip skills confirmation prompts
32
+ --force Overwrite existing target directory
33
+ -h, --help Show help
34
+
35
+ Examples:
36
+ npx nvwa-skill-cli install https://example.com/demo-skill.zip
37
+ npx nvwa-skill-cli install ./demo-skill.zip --target ./skills
38
+ npx nvwa-skill-cli install https://example.com/demo-skill.zip --name self-improvement
39
+ npx nvwa-skill-cli install-skillhub 2062784556544692224 --base-url http://localhost:8188 -g
40
+ npx nvwa-skill-cli install-skillhub 2062784556544692224 1.0.0 --base-url http://localhost:8188 --agent codex -y
41
+ `.trim();
42
+
43
+ async function main() {
44
+ const args = process.argv.slice(2);
45
+ const command = args[0];
46
+
47
+ if (!command || command === "help" || command === "-h" || command === "--help") {
48
+ console.log(HELP_TEXT);
49
+ return;
50
+ }
51
+
52
+ if (command !== "install" && command !== "install-skillhub") {
53
+ fail(`Unknown command: ${command}`);
54
+ }
55
+
56
+ if (command === "install") {
57
+ const parsed = parseInstallArgs(args.slice(1));
58
+ if (!parsed.source) {
59
+ fail("Missing zip URL or local zip path.");
60
+ }
61
+ await installFromSource(parsed);
62
+ return;
63
+ }
64
+
65
+ const skillHubParsed = parseInstallSkillHubArgs(args.slice(1));
66
+ await installFromSkillHub(skillHubParsed);
67
+ }
68
+
69
+ async function installFromSource(parsed) {
70
+ const targetRoot = path.resolve(parsed.target || path.join(process.cwd(), "installed-skills"));
71
+ const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "nvwa-skill-"));
72
+
73
+ try {
74
+ const zipPath = await resolveZipSource(parsed.source, tempRoot);
75
+ const extractDir = path.join(tempRoot, "extract");
76
+ await extractZip(zipPath, extractDir);
77
+
78
+ const skillRoots = await findSkillRoots(extractDir);
79
+ if (skillRoots.length === 0) {
80
+ fail("No SKILL.md found in extracted package.");
81
+ }
82
+ if (skillRoots.length > 1) {
83
+ fail(`Multiple SKILL.md roots found: ${skillRoots.join(", ")}`);
84
+ }
85
+
86
+ const skillRoot = skillRoots[0];
87
+ const detectedSkillName = await detectSkillName(skillRoot);
88
+ const installName = parsed.name || detectedSkillName || path.basename(skillRoot);
89
+ const installDir = path.join(targetRoot, installName);
90
+
91
+ await ensureInstallTarget(installDir, parsed.force);
92
+ await fsp.mkdir(targetRoot, { recursive: true });
93
+ await copyDirectory(skillRoot, installDir);
94
+
95
+ const result = {
96
+ source: parsed.source,
97
+ skillRoot,
98
+ installDir,
99
+ installedName: installName
100
+ };
101
+
102
+ console.log("Skill installed successfully.");
103
+ console.log(JSON.stringify(result, null, 2));
104
+ } finally {
105
+ await safeRemove(tempRoot);
106
+ }
107
+ }
108
+
109
+ function parseInstallArgs(args) {
110
+ const result = {
111
+ source: "",
112
+ target: "",
113
+ name: "",
114
+ force: false
115
+ };
116
+
117
+ for (let index = 0; index < args.length; index += 1) {
118
+ const arg = args[index];
119
+ if (!arg) {
120
+ continue;
121
+ }
122
+ if (!result.source && !arg.startsWith("-")) {
123
+ result.source = arg;
124
+ continue;
125
+ }
126
+ if (arg === "--target") {
127
+ result.target = requireNextValue(args, index, "--target");
128
+ index += 1;
129
+ continue;
130
+ }
131
+ if (arg === "--name") {
132
+ result.name = requireNextValue(args, index, "--name");
133
+ index += 1;
134
+ continue;
135
+ }
136
+ if (arg === "--force") {
137
+ result.force = true;
138
+ continue;
139
+ }
140
+ if (arg === "-h" || arg === "--help") {
141
+ console.log(HELP_TEXT);
142
+ process.exit(0);
143
+ }
144
+ fail(`Unknown option: ${arg}`);
145
+ }
146
+
147
+ return result;
148
+ }
149
+
150
+ function parseInstallSkillHubArgs(args) {
151
+ const result = {
152
+ skillId: "",
153
+ version: "",
154
+ baseUrl: "http://localhost:8188",
155
+ agent: "codex",
156
+ global: false,
157
+ copy: false,
158
+ yes: false
159
+ };
160
+
161
+ for (let index = 0; index < args.length; index += 1) {
162
+ const arg = args[index];
163
+ if (!arg) {
164
+ continue;
165
+ }
166
+ if (!result.skillId && !arg.startsWith("-")) {
167
+ result.skillId = arg;
168
+ continue;
169
+ }
170
+ if (!result.version && !arg.startsWith("-")) {
171
+ result.version = arg;
172
+ continue;
173
+ }
174
+ if (arg === "--agent") {
175
+ result.agent = requireNextValue(args, index, "--agent");
176
+ index += 1;
177
+ continue;
178
+ }
179
+ if (arg === "--base-url") {
180
+ result.baseUrl = requireNextValue(args, index, "--base-url");
181
+ index += 1;
182
+ continue;
183
+ }
184
+ if (arg === "-g" || arg === "--global") {
185
+ result.global = true;
186
+ continue;
187
+ }
188
+ if (arg === "--copy") {
189
+ result.copy = true;
190
+ continue;
191
+ }
192
+ if (arg === "-y" || arg === "--yes") {
193
+ result.yes = true;
194
+ continue;
195
+ }
196
+ if (arg === "-h" || arg === "--help") {
197
+ console.log(HELP_TEXT);
198
+ process.exit(0);
199
+ }
200
+ fail(`Unknown option: ${arg}`);
201
+ }
202
+
203
+ if (!result.skillId) {
204
+ fail("Missing skillId for install-skillhub.");
205
+ }
206
+ return result;
207
+ }
208
+
209
+ function requireNextValue(args, index, optionName) {
210
+ const value = args[index + 1];
211
+ if (!value || value.startsWith("-")) {
212
+ fail(`Missing value for ${optionName}`);
213
+ }
214
+ return value;
215
+ }
216
+
217
+ async function resolveZipSource(source, tempRoot) {
218
+ if (isHttpUrl(source)) {
219
+ const zipPath = path.join(tempRoot, "download.zip");
220
+ await downloadFile(source, zipPath, 0);
221
+ return zipPath;
222
+ }
223
+
224
+ const localPath = path.resolve(source);
225
+ const stat = await fsp.stat(localPath).catch(() => null);
226
+ if (!stat || !stat.isFile()) {
227
+ fail(`Local zip file not found: ${localPath}`);
228
+ }
229
+ return localPath;
230
+ }
231
+
232
+ async function installFromSkillHub(parsed) {
233
+ const normalizedBaseUrl = trimTrailingSlash(parsed.baseUrl);
234
+ const basicDetailUrl = `${normalizedBaseUrl}/api/v1/skill/basic?skillId=${encodeURIComponent(parsed.skillId)}`;
235
+ const downloadUrl = parsed.version
236
+ ? `${normalizedBaseUrl}/api/v1/skill/version/download?skillId=${encodeURIComponent(parsed.skillId)}&version=${encodeURIComponent(parsed.version)}`
237
+ : `${normalizedBaseUrl}/api/v1/skill/download?skillId=${encodeURIComponent(parsed.skillId)}`;
238
+ const basicDetail = await getJson(basicDetailUrl);
239
+ const detailData = basicDetail && basicDetail.data ? basicDetail.data : null;
240
+ if (!detailData || !detailData.name) {
241
+ fail(`Failed to resolve skill basic detail from ${basicDetailUrl}`);
242
+ }
243
+
244
+ const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "nvwa-skillhub-"));
245
+ try {
246
+ const zipPath = await resolveZipSource(downloadUrl, tempRoot);
247
+ const extractDir = path.join(tempRoot, "extract");
248
+ await extractZip(zipPath, extractDir);
249
+
250
+ const skillRoots = await findSkillRoots(extractDir);
251
+ if (skillRoots.length === 0) {
252
+ fail("No SKILL.md found in extracted package.");
253
+ }
254
+ if (skillRoots.length > 1) {
255
+ fail(`Multiple SKILL.md roots found: ${skillRoots.join(", ")}`);
256
+ }
257
+
258
+ const skillRoot = skillRoots[0];
259
+ const detectedSkillName = await detectSkillName(skillRoot);
260
+ const installName = sanitizeDirectoryName(String(detectedSkillName || detailData.name || path.basename(skillRoot)));
261
+ const installResult = await runSkillsAdd(skillRoot, {
262
+ agent: parsed.agent,
263
+ global: parsed.global,
264
+ copy: parsed.copy,
265
+ yes: parsed.yes
266
+ });
267
+ const codexSyncDir = await syncCodexGlobalSkillIfNeeded(installName, parsed);
268
+
269
+ console.log("Skill installed via skills successfully.");
270
+ console.log(JSON.stringify({
271
+ source: downloadUrl,
272
+ skillId: parsed.skillId,
273
+ version: parsed.version || String(detailData.version || ""),
274
+ installedName: installName,
275
+ agent: parsed.agent,
276
+ global: parsed.global,
277
+ mode: parsed.copy ? "copy" : "symlink",
278
+ skillsCommand: installResult.command,
279
+ codexInstallDir: codexSyncDir
280
+ }, null, 2));
281
+ } finally {
282
+ await safeRemove(tempRoot);
283
+ }
284
+ }
285
+
286
+ async function runSkillsAdd(skillRoot, options) {
287
+ const skillsPackageJson = require.resolve("skills/package.json");
288
+ const skillsCliPath = path.join(path.dirname(skillsPackageJson), "bin", "cli.mjs");
289
+ const commandArgs = [skillsCliPath, "add", skillRoot, "--agent", options.agent];
290
+ if (options.global) {
291
+ commandArgs.push("--global");
292
+ }
293
+ if (options.copy) {
294
+ commandArgs.push("--copy");
295
+ }
296
+ if (options.yes) {
297
+ commandArgs.push("--yes");
298
+ }
299
+
300
+ await new Promise((resolve, reject) => {
301
+ const child = spawn(process.execPath, commandArgs, {
302
+ stdio: "inherit"
303
+ });
304
+ child.on("error", reject);
305
+ child.on("exit", (code) => {
306
+ if (code === 0) {
307
+ resolve();
308
+ return;
309
+ }
310
+ reject(new Error(`skills add exited with code ${code}`));
311
+ });
312
+ });
313
+
314
+ return {
315
+ command: [process.execPath, ...commandArgs].join(" ")
316
+ };
317
+ }
318
+
319
+ async function syncCodexGlobalSkillIfNeeded(skillName, options) {
320
+ if (options.agent !== "codex" || !options.global) {
321
+ return "";
322
+ }
323
+
324
+ const homeDir = os.homedir();
325
+ const codexHome = process.env.CODEX_HOME && process.env.CODEX_HOME.trim()
326
+ ? process.env.CODEX_HOME.trim()
327
+ : path.join(homeDir, ".codex");
328
+ const canonicalSkillDir = path.join(homeDir, ".agents", "skills", skillName);
329
+ const codexSkillsDir = path.join(codexHome, "skills");
330
+ const codexSkillDir = path.join(codexSkillsDir, skillName);
331
+ const canonicalStat = await fsp.stat(canonicalSkillDir).catch(() => null);
332
+ if (!canonicalStat || !canonicalStat.isDirectory()) {
333
+ return "";
334
+ }
335
+
336
+ await fsp.mkdir(codexSkillsDir, { recursive: true });
337
+ await safeRemove(codexSkillDir);
338
+ await copyDirectory(canonicalSkillDir, codexSkillDir);
339
+ return codexSkillDir;
340
+ }
341
+
342
+ function isHttpUrl(value) {
343
+ return value.startsWith("http://") || value.startsWith("https://");
344
+ }
345
+
346
+ function trimTrailingSlash(value) {
347
+ return String(value || "").replace(/\/+$/, "");
348
+ }
349
+
350
+ async function downloadFile(url, destination, redirectCount) {
351
+ if (redirectCount > 5) {
352
+ fail(`Too many redirects while downloading: ${url}`);
353
+ }
354
+
355
+ const client = url.startsWith("https://") ? https : http;
356
+ const response = await new Promise((resolve, reject) => {
357
+ client.get(url, (res) => resolve(res)).on("error", reject);
358
+ });
359
+
360
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
361
+ response.resume();
362
+ const redirectUrl = new URL(response.headers.location, url).toString();
363
+ await downloadFile(redirectUrl, destination, redirectCount + 1);
364
+ return;
365
+ }
366
+
367
+ if (response.statusCode !== 200) {
368
+ response.resume();
369
+ fail(`Download failed with status ${response.statusCode}: ${url}`);
370
+ }
371
+
372
+ await pipeline(response, fs.createWriteStream(destination));
373
+ }
374
+
375
+ async function getJson(url) {
376
+ const body = await getText(url, 0);
377
+ try {
378
+ return JSON.parse(body);
379
+ } catch (error) {
380
+ fail(`Invalid JSON response from ${url}: ${error.message}`);
381
+ }
382
+ }
383
+
384
+ async function getText(url, redirectCount) {
385
+ if (redirectCount > 5) {
386
+ fail(`Too many redirects while requesting: ${url}`);
387
+ }
388
+ const client = url.startsWith("https://") ? https : http;
389
+ const response = await new Promise((resolve, reject) => {
390
+ client.get(url, (res) => resolve(res)).on("error", reject);
391
+ });
392
+
393
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
394
+ const redirectUrl = new URL(response.headers.location, url).toString();
395
+ response.resume();
396
+ return getText(redirectUrl, redirectCount + 1);
397
+ }
398
+ if (response.statusCode !== 200) {
399
+ const errorBody = await readResponseBody(response);
400
+ fail(`Request failed with status ${response.statusCode}: ${url}${errorBody ? `, body=${errorBody}` : ""}`);
401
+ }
402
+ return readResponseBody(response);
403
+ }
404
+
405
+ async function readResponseBody(response) {
406
+ let content = "";
407
+ for await (const chunk of response) {
408
+ content += chunk.toString("utf8");
409
+ }
410
+ return content;
411
+ }
412
+
413
+ async function extractZip(zipPath, extractDir) {
414
+ await fsp.mkdir(extractDir, { recursive: true });
415
+ const zip = new AdmZip(zipPath);
416
+ zip.extractAllTo(extractDir, true);
417
+ }
418
+
419
+ async function findSkillRoots(rootDir) {
420
+ const results = [];
421
+ await walk(rootDir, async (entryPath, dirent) => {
422
+ if (dirent.isFile() && dirent.name === "SKILL.md") {
423
+ results.push(path.dirname(entryPath));
424
+ }
425
+ });
426
+ return results;
427
+ }
428
+
429
+ async function walk(rootDir, visitor) {
430
+ const entries = await fsp.readdir(rootDir, { withFileTypes: true });
431
+ for (const entry of entries) {
432
+ const fullPath = path.join(rootDir, entry.name);
433
+ await visitor(fullPath, entry);
434
+ if (entry.isDirectory()) {
435
+ await walk(fullPath, visitor);
436
+ }
437
+ }
438
+ }
439
+
440
+ async function detectSkillName(skillRoot) {
441
+ const skillFilePath = path.join(skillRoot, "SKILL.md");
442
+ const exists = await fsp.stat(skillFilePath).catch(() => null);
443
+ if (!exists || !exists.isFile()) {
444
+ return "";
445
+ }
446
+ const content = await fsp.readFile(skillFilePath, "utf8");
447
+ return parseSkillName(content);
448
+ }
449
+
450
+ function parseSkillName(content) {
451
+ if (!content || !content.startsWith("---")) {
452
+ return "";
453
+ }
454
+ const lines = content.split(/\r?\n/);
455
+ let frontMatterEnded = false;
456
+ for (let index = 1; index < lines.length; index += 1) {
457
+ const line = lines[index];
458
+ if (line.trim() === "---") {
459
+ frontMatterEnded = true;
460
+ break;
461
+ }
462
+ const match = line.match(/^name\s*:\s*(.+)\s*$/);
463
+ if (match) {
464
+ return sanitizeDirectoryName(stripWrappingQuotes(match[1].trim()));
465
+ }
466
+ }
467
+ if (!frontMatterEnded) {
468
+ return "";
469
+ }
470
+ return "";
471
+ }
472
+
473
+ function stripWrappingQuotes(value) {
474
+ if (!value) {
475
+ return "";
476
+ }
477
+ if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) {
478
+ return value.slice(1, -1).trim();
479
+ }
480
+ return value;
481
+ }
482
+
483
+ function sanitizeDirectoryName(value) {
484
+ if (!value) {
485
+ return "";
486
+ }
487
+ return value
488
+ .replace(/[<>:"/\\|?*\u0000-\u001F]/g, "-")
489
+ .replace(/\s+/g, "-")
490
+ .replace(/-+/g, "-")
491
+ .replace(/^-|-$/g, "");
492
+ }
493
+
494
+ async function ensureInstallTarget(installDir, force) {
495
+ const exists = await fsp.stat(installDir).catch(() => null);
496
+ if (!exists) {
497
+ return;
498
+ }
499
+ if (!force) {
500
+ fail(`Target directory already exists: ${installDir}. Use --force to overwrite.`);
501
+ }
502
+ await safeRemove(installDir);
503
+ }
504
+
505
+ async function copyDirectory(sourceDir, targetDir) {
506
+ await fsp.mkdir(targetDir, { recursive: true });
507
+ const entries = await fsp.readdir(sourceDir, { withFileTypes: true });
508
+ for (const entry of entries) {
509
+ const sourcePath = path.join(sourceDir, entry.name);
510
+ const targetPath = path.join(targetDir, entry.name);
511
+ if (entry.isDirectory()) {
512
+ await copyDirectory(sourcePath, targetPath);
513
+ } else if (entry.isFile()) {
514
+ await fsp.copyFile(sourcePath, targetPath);
515
+ }
516
+ }
517
+ }
518
+
519
+ async function safeRemove(targetPath) {
520
+ await fsp.rm(targetPath, { recursive: true, force: true }).catch(() => undefined);
521
+ }
522
+
523
+ function fail(message) {
524
+ console.error(`Error: ${message}`);
525
+ process.exit(1);
526
+ }
527
+
528
+ main().catch((error) => {
529
+ const message = error && error.message ? error.message : String(error);
530
+ fail(message);
531
+ });
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "nvwa-skill-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI for installing skill packages from zip URLs or local zip files.",
5
+ "bin": {
6
+ "nvwa-skill": "bin/cli.js"
7
+ },
8
+ "files": [
9
+ "bin",
10
+ "README.md"
11
+ ],
12
+ "keywords": [
13
+ "skill",
14
+ "cli",
15
+ "zip",
16
+ "installer"
17
+ ],
18
+ "license": "MIT",
19
+ "type": "commonjs",
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "dependencies": {
24
+ "adm-zip": "^0.5.16",
25
+ "skills": "^1.5.10"
26
+ }
27
+ }