@nucleoicons/skills 0.1.2 → 0.1.4
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 +13 -5
- package/dist/api.d.ts +11 -2
- package/dist/api.js +48 -7
- package/dist/index.js +26 -5
- package/dist/install.d.ts +2 -0
- package/dist/install.js +61 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
CLI to install and update **Nucleo** MCP + icon skills on your machine.
|
|
4
4
|
|
|
5
|
+
Once installed, your AI coding tool can help you **use Nucleo icons in code**. Typical uses:
|
|
6
|
+
|
|
7
|
+
- Search icons by name or idea across your licensed families (core, ui, sharp, micro, pixel)
|
|
8
|
+
- Pick an icon and get the right React component or SVG for your project
|
|
9
|
+
- Customize icons when inserting them (color, secondary color, stroke width, duo opacity, corners)
|
|
10
|
+
|
|
11
|
+
Ask your editor naturally, for example: “find a settings icon in Nucleo” or “insert the home icon as a React component.”
|
|
12
|
+
|
|
5
13
|
Requires **Node.js 20+**.
|
|
6
14
|
|
|
7
15
|
## Install
|
|
@@ -39,8 +47,8 @@ After updating, reload MCP in your editor again.
|
|
|
39
47
|
|
|
40
48
|
## Editors
|
|
41
49
|
|
|
42
|
-
| Tool
|
|
43
|
-
|
|
44
|
-
| Cursor
|
|
45
|
-
| Claude Code | `claude`
|
|
46
|
-
| Codex
|
|
50
|
+
| Tool | `--tools` value |
|
|
51
|
+
| ----------- | --------------- |
|
|
52
|
+
| Cursor | `cursor` |
|
|
53
|
+
| Claude Code | `claude` |
|
|
54
|
+
| Codex | `codex` |
|
package/dist/api.d.ts
CHANGED
|
@@ -32,6 +32,10 @@ export interface SkillsReleaseData {
|
|
|
32
32
|
files: SkillsReleaseFile[];
|
|
33
33
|
message?: string;
|
|
34
34
|
}
|
|
35
|
+
export interface DownloadProgress {
|
|
36
|
+
transferred: number;
|
|
37
|
+
total: number | null;
|
|
38
|
+
}
|
|
35
39
|
/**
|
|
36
40
|
* POST /skills/login
|
|
37
41
|
* Body: { email, password }
|
|
@@ -43,5 +47,10 @@ export declare function skillsLogin(email: string, password: string): Promise<Sk
|
|
|
43
47
|
* Response: { data: { version, families, files, downloadReady } }
|
|
44
48
|
*/
|
|
45
49
|
export declare function fetchSkillsRelease(token: string, target: PackageTarget): Promise<SkillsReleaseData>;
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
declare function formatBytes(bytes: number): string;
|
|
51
|
+
/** Download a remote file (signed R2 URL) to disk, with optional progress. */
|
|
52
|
+
export declare function downloadUrlToFile(url: string, destPath: string, onProgress?: (progress: DownloadProgress) => void): Promise<{
|
|
53
|
+
bytes: number;
|
|
54
|
+
total: number | null;
|
|
55
|
+
}>;
|
|
56
|
+
export { formatBytes };
|
package/dist/api.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createWriteStream } from "node:fs";
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
|
+
import { Transform } from "node:stream";
|
|
4
5
|
import { pipeline } from "node:stream/promises";
|
|
5
6
|
import { Readable } from "node:stream";
|
|
6
7
|
import { getApiBaseUrl } from "./config.js";
|
|
@@ -84,18 +85,58 @@ export async function fetchSkillsRelease(token, target) {
|
|
|
84
85
|
}
|
|
85
86
|
return json.data;
|
|
86
87
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
function formatBytes(bytes) {
|
|
89
|
+
if (bytes < 1024) {
|
|
90
|
+
return `${bytes} B`;
|
|
91
|
+
}
|
|
92
|
+
if (bytes < 1024 * 1024) {
|
|
93
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
94
|
+
}
|
|
95
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
96
|
+
}
|
|
97
|
+
/** Download a remote file (signed R2 URL) to disk, with optional progress. */
|
|
98
|
+
export async function downloadUrlToFile(url, destPath, onProgress) {
|
|
99
|
+
let res;
|
|
100
|
+
try {
|
|
101
|
+
res = await fetch(url);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
105
|
+
throw new ApiError(`Download failed (network): ${detail}`);
|
|
106
|
+
}
|
|
90
107
|
if (!res.ok) {
|
|
91
|
-
throw new ApiError(`Download failed (${res.status})
|
|
108
|
+
throw new ApiError(`Download failed (${res.status}). The link may have expired — run setup again.`, res.status);
|
|
92
109
|
}
|
|
93
110
|
if (!res.body) {
|
|
94
111
|
throw new ApiError("Download failed: empty response body.");
|
|
95
112
|
}
|
|
113
|
+
const totalHeader = res.headers.get("content-length");
|
|
114
|
+
const total = totalHeader ? Number(totalHeader) : null;
|
|
115
|
+
let transferred = 0;
|
|
116
|
+
let lastLogged = 0;
|
|
96
117
|
await mkdir(dirname(destPath), { recursive: true });
|
|
97
118
|
const fileStream = createWriteStream(destPath);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
119
|
+
const progressTap = new Transform({
|
|
120
|
+
transform(chunk, _encoding, callback) {
|
|
121
|
+
transferred += chunk.length;
|
|
122
|
+
if (onProgress) {
|
|
123
|
+
onProgress({ transferred, total: Number.isFinite(total) ? total : null });
|
|
124
|
+
}
|
|
125
|
+
// Throttle console-friendly updates to ~every 2MB
|
|
126
|
+
if (transferred - lastLogged >= 2 * 1024 * 1024 || transferred === total) {
|
|
127
|
+
lastLogged = transferred;
|
|
128
|
+
}
|
|
129
|
+
callback(null, chunk);
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
try {
|
|
133
|
+
const nodeReadable = Readable.fromWeb(res.body);
|
|
134
|
+
await pipeline(nodeReadable, progressTap, fileStream);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
138
|
+
throw new ApiError(`Download interrupted after ${formatBytes(transferred)}${total ? ` of ${formatBytes(total)}` : ""}: ${detail}`);
|
|
139
|
+
}
|
|
140
|
+
return { bytes: transferred, total: Number.isFinite(total) ? total : null };
|
|
101
141
|
}
|
|
142
|
+
export { formatBytes };
|
package/dist/index.js
CHANGED
|
@@ -23,11 +23,32 @@ function parseTools(value) {
|
|
|
23
23
|
.map((t) => t.trim().toLowerCase())
|
|
24
24
|
.filter(Boolean);
|
|
25
25
|
}
|
|
26
|
+
let verbose = false;
|
|
27
|
+
function printError(error) {
|
|
28
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
29
|
+
console.error(`\nError: ${message}`);
|
|
30
|
+
if (verbose && error instanceof Error && error.stack) {
|
|
31
|
+
console.error(error.stack);
|
|
32
|
+
}
|
|
33
|
+
console.error("");
|
|
34
|
+
}
|
|
35
|
+
process.on("unhandledRejection", (reason) => {
|
|
36
|
+
printError(reason);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
});
|
|
39
|
+
process.on("uncaughtException", (error) => {
|
|
40
|
+
printError(error);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
});
|
|
26
43
|
const program = new Command();
|
|
27
44
|
program
|
|
28
45
|
.name("nucleo-skills")
|
|
29
46
|
.description("Install and update Nucleo Pro MCP + icon skills")
|
|
30
|
-
.version(packageVersion())
|
|
47
|
+
.version(packageVersion())
|
|
48
|
+
.option("-v, --verbose", "Show full error details")
|
|
49
|
+
.hook("preAction", (thisCommand) => {
|
|
50
|
+
verbose = Boolean(thisCommand.opts().verbose);
|
|
51
|
+
});
|
|
31
52
|
program
|
|
32
53
|
.command("setup")
|
|
33
54
|
.description("Sign in, download your licensed package, configure your editor")
|
|
@@ -52,7 +73,7 @@ program
|
|
|
52
73
|
});
|
|
53
74
|
}
|
|
54
75
|
catch (error) {
|
|
55
|
-
|
|
76
|
+
printError(error);
|
|
56
77
|
process.exitCode = 1;
|
|
57
78
|
}
|
|
58
79
|
});
|
|
@@ -73,7 +94,7 @@ program
|
|
|
73
94
|
});
|
|
74
95
|
}
|
|
75
96
|
catch (error) {
|
|
76
|
-
|
|
97
|
+
printError(error);
|
|
77
98
|
process.exitCode = 1;
|
|
78
99
|
}
|
|
79
100
|
});
|
|
@@ -85,7 +106,7 @@ program
|
|
|
85
106
|
await runStatus();
|
|
86
107
|
}
|
|
87
108
|
catch (error) {
|
|
88
|
-
|
|
109
|
+
printError(error);
|
|
89
110
|
process.exitCode = 1;
|
|
90
111
|
}
|
|
91
112
|
});
|
|
@@ -97,7 +118,7 @@ program
|
|
|
97
118
|
await runLogout();
|
|
98
119
|
}
|
|
99
120
|
catch (error) {
|
|
100
|
-
|
|
121
|
+
printError(error);
|
|
101
122
|
process.exitCode = 1;
|
|
102
123
|
}
|
|
103
124
|
});
|
package/dist/install.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export declare function readInstallManifest(root?: string): Promise<InstallManif
|
|
|
4
4
|
export declare function cleanInstallRoot(root?: string): Promise<void>;
|
|
5
5
|
/**
|
|
6
6
|
* Download all release files (signed URLs) and merge into the install root.
|
|
7
|
+
* Downloads complete first, then the install folder is replaced — so a failed
|
|
8
|
+
* download does not wipe a previous working install.
|
|
7
9
|
*/
|
|
8
10
|
export declare function installFromRelease(release: SkillsReleaseData, target: PackageTarget, root?: string): Promise<void>;
|
|
9
11
|
/**
|
package/dist/install.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { execFile } from "node:child_process";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import extract from "extract-zip";
|
|
7
|
-
import { downloadUrlToFile } from "./api.js";
|
|
7
|
+
import { downloadUrlToFile, formatBytes } from "./api.js";
|
|
8
8
|
import { installRoot, nucleoHomeDir } from "./config.js";
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
export async function readInstallManifest(root = installRoot()) {
|
|
@@ -39,26 +39,65 @@ async function extractZipMerge(zipPath, dest) {
|
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
41
|
* Download all release files (signed URLs) and merge into the install root.
|
|
42
|
+
* Downloads complete first, then the install folder is replaced — so a failed
|
|
43
|
+
* download does not wipe a previous working install.
|
|
42
44
|
*/
|
|
43
45
|
export async function installFromRelease(release, target, root = installRoot()) {
|
|
44
46
|
if (!release.downloadReady) {
|
|
45
47
|
throw new Error(release.message ||
|
|
46
|
-
"Downloads are not ready on the server yet
|
|
48
|
+
"Downloads are not ready on the server yet. Please try again later.");
|
|
47
49
|
}
|
|
48
50
|
const missingUrl = release.files.filter((f) => !f.url);
|
|
49
51
|
if (missingUrl.length > 0) {
|
|
50
52
|
throw new Error(`Release files missing download urls: ${missingUrl.map((f) => f.id).join(", ")}`);
|
|
51
53
|
}
|
|
52
|
-
await cleanInstallRoot(root);
|
|
53
54
|
const downloadsDir = join(nucleoHomeDir(), "downloads");
|
|
54
55
|
await mkdir(downloadsDir, { recursive: true });
|
|
56
|
+
const downloaded = [];
|
|
55
57
|
try {
|
|
58
|
+
// 1) Download everything first (keeps existing install intact on failure)
|
|
56
59
|
for (const file of release.files) {
|
|
57
60
|
const zipPath = join(downloadsDir, `${file.id}-${release.version}.zip`);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
process.stdout.write(` ↓ ${file.id}`);
|
|
62
|
+
let lastLine = "";
|
|
63
|
+
try {
|
|
64
|
+
const result = await downloadUrlToFile(file.url, zipPath, ({ transferred, total }) => {
|
|
65
|
+
const label = total
|
|
66
|
+
? ` ↓ ${file.id} ${formatBytes(transferred)} / ${formatBytes(total)}`
|
|
67
|
+
: ` ↓ ${file.id} ${formatBytes(transferred)}`;
|
|
68
|
+
// overwrite current line
|
|
69
|
+
process.stdout.write(`\r${label}${" ".repeat(Math.max(0, lastLine.length - label.length))}`);
|
|
70
|
+
lastLine = label;
|
|
71
|
+
});
|
|
72
|
+
const sizeLabel = result.total
|
|
73
|
+
? `${formatBytes(result.bytes)} / ${formatBytes(result.total)}`
|
|
74
|
+
: formatBytes(result.bytes);
|
|
75
|
+
process.stdout.write(`\r ↓ ${file.id} ${sizeLabel} ✓\n`);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
process.stdout.write("\n");
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
const info = await stat(zipPath);
|
|
82
|
+
if (info.size <= 0) {
|
|
83
|
+
throw new Error(`Downloaded file is empty: ${file.id}`);
|
|
84
|
+
}
|
|
85
|
+
downloaded.push({ id: file.id, zipPath });
|
|
86
|
+
}
|
|
87
|
+
// 2) Only now replace the install root
|
|
88
|
+
console.log(" Extracting…");
|
|
89
|
+
await cleanInstallRoot(root);
|
|
90
|
+
for (const file of downloaded) {
|
|
91
|
+
console.log(` ▸ ${file.id}`);
|
|
92
|
+
try {
|
|
93
|
+
await extractZipMerge(file.zipPath, root);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
97
|
+
throw new Error(`Failed extracting ${file.id} (${formatBytes((await stat(file.zipPath)).size)}). ${detail}\n` +
|
|
98
|
+
`If this keeps happening, free disk space or try again with:\n` +
|
|
99
|
+
` NODE_OPTIONS=--max-old-space-size=8192 npx @nucleoicons/skills setup …`);
|
|
100
|
+
}
|
|
62
101
|
}
|
|
63
102
|
}
|
|
64
103
|
finally {
|
|
@@ -116,11 +155,19 @@ export async function buildMcp(root = installRoot()) {
|
|
|
116
155
|
}
|
|
117
156
|
const distEntry = join(mcpDir, "dist", "index.js");
|
|
118
157
|
const hasDist = existsSync(distEntry);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
158
|
+
console.log(" npm install (mcp)…");
|
|
159
|
+
try {
|
|
160
|
+
await execFileAsync("npm", ["install", "--omit=dev"], {
|
|
161
|
+
cwd: mcpDir,
|
|
162
|
+
env: process.env,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
167
|
+
throw new Error(`MCP dependency install failed: ${detail}`);
|
|
168
|
+
}
|
|
123
169
|
if (!hasDist || existsSync(join(mcpDir, "src"))) {
|
|
170
|
+
console.log(" npm run build (mcp)…");
|
|
124
171
|
try {
|
|
125
172
|
await execFileAsync("npm", ["run", "build"], {
|
|
126
173
|
cwd: mcpDir,
|
|
@@ -129,7 +176,8 @@ export async function buildMcp(root = installRoot()) {
|
|
|
129
176
|
}
|
|
130
177
|
catch (error) {
|
|
131
178
|
if (!existsSync(distEntry)) {
|
|
132
|
-
|
|
179
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
180
|
+
throw new Error(`MCP build failed: ${detail}`);
|
|
133
181
|
}
|
|
134
182
|
}
|
|
135
183
|
}
|