@nucleoicons/skills 0.1.4 → 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/README.md +7 -7
- package/dist/adapters/agents.d.ts +12 -4
- package/dist/adapters/agents.js +73 -31
- package/dist/install.d.ts +6 -0
- package/dist/install.js +83 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,9 +25,9 @@ npx @nucleoicons/skills setup --target react --tools cursor
|
|
|
25
25
|
npx @nucleoicons/skills setup --target vanilla --tools cursor,claude
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
Setup installs the package under your user home (`~/.nucleo/skills`) and configures the editors you choose.
|
|
28
|
+
Setup installs the package under your user home (`~/.nucleo/skills`) and configures the editors you choose (skills + MCP, no manual config step).
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Reload MCP in your editor, then ask something like:
|
|
31
31
|
|
|
32
32
|
```text
|
|
33
33
|
What Nucleo icon families do I have?
|
|
@@ -47,8 +47,8 @@ After updating, reload MCP in your editor again.
|
|
|
47
47
|
|
|
48
48
|
## Editors
|
|
49
49
|
|
|
50
|
-
| Tool | `--tools` value |
|
|
51
|
-
| ----------- | --------------- |
|
|
52
|
-
| Cursor | `cursor` |
|
|
53
|
-
| Claude Code | `claude` |
|
|
54
|
-
| Codex | `codex` |
|
|
50
|
+
| Tool | `--tools` value | Auto-configured |
|
|
51
|
+
| ----------- | --------------- | --------------- |
|
|
52
|
+
| Cursor | `cursor` | `~/.cursor/mcp.json` + `~/.cursor/skills/` |
|
|
53
|
+
| Claude Code | `claude` | `~/.claude.json` + `~/.claude/skills/` |
|
|
54
|
+
| Codex | `codex` | `~/.codex/config.toml` + `~/.agents/skills/` |
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import { type AgentTool } from "../config.js";
|
|
2
|
+
/** Remove an existing `[mcp_servers.<name>]` table (+ nested tables), then append `block`. */
|
|
3
|
+
export declare function upsertTomlMcpServer(toml: string, serverName: string, block: string): string;
|
|
2
4
|
/** Merge nucleo-icons into ~/.cursor/mcp.json and copy skills to ~/.cursor/skills */
|
|
3
5
|
export declare function configureCursor(root?: string): Promise<{
|
|
4
6
|
mcpPath: string;
|
|
7
|
+
skillsPath: string;
|
|
5
8
|
skills: string[];
|
|
6
9
|
}>;
|
|
7
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Copy skills to ~/.claude/skills and merge MCP into ~/.claude.json
|
|
12
|
+
* (user-scope mcpServers — same file `claude mcp add --scope user` writes).
|
|
13
|
+
*/
|
|
8
14
|
export declare function configureClaude(root?: string): Promise<{
|
|
15
|
+
mcpPath: string;
|
|
16
|
+
skillsPath: string;
|
|
9
17
|
skills: string[];
|
|
10
|
-
hint: string;
|
|
11
18
|
}>;
|
|
12
|
-
/** Copy skills to ~/.agents/skills and
|
|
19
|
+
/** Copy skills to ~/.agents/skills and merge MCP into ~/.codex/config.toml */
|
|
13
20
|
export declare function configureCodex(root?: string): Promise<{
|
|
21
|
+
mcpPath: string;
|
|
22
|
+
skillsPath: string;
|
|
14
23
|
skills: string[];
|
|
15
|
-
hint: string;
|
|
16
24
|
}>;
|
|
17
25
|
export declare function configureTool(tool: AgentTool, root?: string): Promise<void>;
|
package/dist/adapters/agents.js
CHANGED
|
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { installRoot } from "../config.js";
|
|
6
6
|
import { mcpEntryPath, skillsSourceDir } from "../install.js";
|
|
7
|
+
const MCP_SERVER_NAME = "nucleo-icons";
|
|
7
8
|
async function readJsonFile(path, fallback) {
|
|
8
9
|
if (!existsSync(path)) {
|
|
9
10
|
return fallback;
|
|
@@ -40,60 +41,101 @@ function nucleoMcpServerConfig(root = installRoot()) {
|
|
|
40
41
|
},
|
|
41
42
|
};
|
|
42
43
|
}
|
|
44
|
+
function escapeTomlString(value) {
|
|
45
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
46
|
+
}
|
|
47
|
+
/** Remove an existing `[mcp_servers.<name>]` table (+ nested tables), then append `block`. */
|
|
48
|
+
export function upsertTomlMcpServer(toml, serverName, block) {
|
|
49
|
+
const prefix = `mcp_servers.${serverName}`;
|
|
50
|
+
const lines = toml.length > 0 ? toml.split(/\r?\n/) : [];
|
|
51
|
+
const kept = [];
|
|
52
|
+
let skipping = false;
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
const header = line.match(/^\[([^\]]+)\]\s*$/);
|
|
55
|
+
if (header) {
|
|
56
|
+
const name = header[1];
|
|
57
|
+
skipping = name === prefix || name.startsWith(`${prefix}.`);
|
|
58
|
+
}
|
|
59
|
+
if (!skipping) {
|
|
60
|
+
kept.push(line);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
let result = kept.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
64
|
+
if (result.length > 0) {
|
|
65
|
+
result += "\n\n";
|
|
66
|
+
}
|
|
67
|
+
return `${result}${block.trimEnd()}\n`;
|
|
68
|
+
}
|
|
69
|
+
function nucleoCodexTomlBlock(root = installRoot()) {
|
|
70
|
+
const entry = escapeTomlString(mcpEntryPath(root));
|
|
71
|
+
const skillsRoot = escapeTomlString(root);
|
|
72
|
+
return [
|
|
73
|
+
`[mcp_servers.${MCP_SERVER_NAME}]`,
|
|
74
|
+
'command = "node"',
|
|
75
|
+
`args = ["${entry}"]`,
|
|
76
|
+
"",
|
|
77
|
+
`[mcp_servers.${MCP_SERVER_NAME}.env]`,
|
|
78
|
+
`NUCLEO_SKILLS_ROOT = "${skillsRoot}"`,
|
|
79
|
+
].join("\n");
|
|
80
|
+
}
|
|
43
81
|
/** Merge nucleo-icons into ~/.cursor/mcp.json and copy skills to ~/.cursor/skills */
|
|
44
82
|
export async function configureCursor(root = installRoot()) {
|
|
45
83
|
const cursorDir = join(homedir(), ".cursor");
|
|
46
84
|
const mcpPath = join(cursorDir, "mcp.json");
|
|
85
|
+
const skillsPath = join(cursorDir, "skills");
|
|
47
86
|
await mkdir(cursorDir, { recursive: true });
|
|
48
87
|
const mcp = await readJsonFile(mcpPath, { mcpServers: {} });
|
|
49
88
|
mcp.mcpServers = mcp.mcpServers ?? {};
|
|
50
|
-
mcp.mcpServers[
|
|
89
|
+
mcp.mcpServers[MCP_SERVER_NAME] = nucleoMcpServerConfig(root);
|
|
51
90
|
await writeFile(mcpPath, `${JSON.stringify(mcp, null, 2)}\n`, "utf8");
|
|
52
|
-
const skills = await copySkillFolders(
|
|
53
|
-
return { mcpPath, skills };
|
|
91
|
+
const skills = await copySkillFolders(skillsPath, root);
|
|
92
|
+
return { mcpPath, skillsPath, skills };
|
|
54
93
|
}
|
|
55
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Copy skills to ~/.claude/skills and merge MCP into ~/.claude.json
|
|
96
|
+
* (user-scope mcpServers — same file `claude mcp add --scope user` writes).
|
|
97
|
+
*/
|
|
56
98
|
export async function configureClaude(root = installRoot()) {
|
|
57
99
|
const claudeDir = join(homedir(), ".claude");
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
100
|
+
const mcpPath = join(homedir(), ".claude.json");
|
|
101
|
+
const skillsPath = join(claudeDir, "skills");
|
|
102
|
+
await mkdir(claudeDir, { recursive: true });
|
|
103
|
+
const config = await readJsonFile(mcpPath, {});
|
|
104
|
+
config.mcpServers = config.mcpServers ?? {};
|
|
105
|
+
config.mcpServers[MCP_SERVER_NAME] = {
|
|
106
|
+
type: "stdio",
|
|
107
|
+
...nucleoMcpServerConfig(root),
|
|
108
|
+
};
|
|
109
|
+
await writeFile(mcpPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
110
|
+
const skills = await copySkillFolders(skillsPath, root);
|
|
111
|
+
return { mcpPath, skillsPath, skills };
|
|
69
112
|
}
|
|
70
|
-
/** Copy skills to ~/.agents/skills and
|
|
113
|
+
/** Copy skills to ~/.agents/skills and merge MCP into ~/.codex/config.toml */
|
|
71
114
|
export async function configureCodex(root = installRoot()) {
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return { skills, hint };
|
|
115
|
+
const codexDir = join(homedir(), ".codex");
|
|
116
|
+
const mcpPath = join(codexDir, "config.toml");
|
|
117
|
+
const skillsPath = join(homedir(), ".agents", "skills");
|
|
118
|
+
await mkdir(codexDir, { recursive: true });
|
|
119
|
+
const existing = existsSync(mcpPath) ? await readFile(mcpPath, "utf8") : "";
|
|
120
|
+
const next = upsertTomlMcpServer(existing, MCP_SERVER_NAME, nucleoCodexTomlBlock(root));
|
|
121
|
+
await writeFile(mcpPath, next, "utf8");
|
|
122
|
+
const skills = await copySkillFolders(skillsPath, root);
|
|
123
|
+
return { mcpPath, skillsPath, skills };
|
|
82
124
|
}
|
|
83
125
|
export async function configureTool(tool, root = installRoot()) {
|
|
84
126
|
if (tool === "cursor") {
|
|
85
127
|
const result = await configureCursor(root);
|
|
86
128
|
console.log(` Cursor MCP → ${result.mcpPath}`);
|
|
87
|
-
console.log(` Cursor skills →
|
|
129
|
+
console.log(` Cursor skills → ${result.skillsPath} (${result.skills.join(", ")})`);
|
|
88
130
|
return;
|
|
89
131
|
}
|
|
90
132
|
if (tool === "claude") {
|
|
91
133
|
const result = await configureClaude(root);
|
|
92
|
-
console.log(` Claude
|
|
93
|
-
console.log(result.
|
|
134
|
+
console.log(` Claude MCP → ${result.mcpPath}`);
|
|
135
|
+
console.log(` Claude skills → ${result.skillsPath} (${result.skills.join(", ")})`);
|
|
94
136
|
return;
|
|
95
137
|
}
|
|
96
138
|
const result = await configureCodex(root);
|
|
97
|
-
console.log(` Codex
|
|
98
|
-
console.log(result.
|
|
139
|
+
console.log(` Codex MCP → ${result.mcpPath}`);
|
|
140
|
+
console.log(` Codex skills → ${result.skillsPath} (${result.skills.join(", ")})`);
|
|
99
141
|
}
|
package/dist/install.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { type SkillsReleaseData } from "./api.js";
|
|
2
2
|
import { type InstallManifest, type PackageTarget } from "./config.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a real npm binary. Prefer the one next to the running Node
|
|
5
|
+
* (`process.execPath`) so we don't depend on PATH / shell aliases
|
|
6
|
+
* (common cause of `spawn npm ENOENT` even when `npm -v` works in zsh).
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveNpmBin(): string;
|
|
3
9
|
export declare function readInstallManifest(root?: string): Promise<InstallManifest | null>;
|
|
4
10
|
export declare function cleanInstallRoot(root?: string): Promise<void>;
|
|
5
11
|
/**
|
package/dist/install.js
CHANGED
|
@@ -1,12 +1,47 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
2
|
+
import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, 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
7
|
import { downloadUrlToFile, formatBytes } from "./api.js";
|
|
8
8
|
import { installRoot, nucleoHomeDir } from "./config.js";
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a real npm binary. Prefer the one next to the running Node
|
|
12
|
+
* (`process.execPath`) so we don't depend on PATH / shell aliases
|
|
13
|
+
* (common cause of `spawn npm ENOENT` even when `npm -v` works in zsh).
|
|
14
|
+
*/
|
|
15
|
+
export function resolveNpmBin() {
|
|
16
|
+
const binDir = dirname(process.execPath);
|
|
17
|
+
const candidates = process.platform === "win32"
|
|
18
|
+
? [join(binDir, "npm.cmd"), join(binDir, "npm.exe"), "npm.cmd", "npm"]
|
|
19
|
+
: [join(binDir, "npm"), "npm"];
|
|
20
|
+
for (const candidate of candidates) {
|
|
21
|
+
if (candidate === "npm" || candidate === "npm.cmd" || existsSync(candidate)) {
|
|
22
|
+
return candidate;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
26
|
+
}
|
|
27
|
+
async function runNpm(args, cwd) {
|
|
28
|
+
const npmBin = resolveNpmBin();
|
|
29
|
+
try {
|
|
30
|
+
await execFileAsync(npmBin, args, {
|
|
31
|
+
cwd,
|
|
32
|
+
env: process.env,
|
|
33
|
+
// Windows .cmd shims need a shell; Unix absolute paths do not.
|
|
34
|
+
shell: process.platform === "win32",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
39
|
+
throw new Error(`${detail}\n` +
|
|
40
|
+
`Tried to run: ${npmBin} ${args.join(" ")}\n` +
|
|
41
|
+
`If npm is installed, ensure it sits next to node (${process.execPath}), or run:\n` +
|
|
42
|
+
` cd ${cwd} && npm ${args.join(" ")}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
10
45
|
export async function readInstallManifest(root = installRoot()) {
|
|
11
46
|
const path = join(root, "manifest.json");
|
|
12
47
|
if (!existsSync(path)) {
|
|
@@ -23,18 +58,49 @@ export async function cleanInstallRoot(root = installRoot()) {
|
|
|
23
58
|
await rm(root, { recursive: true, force: true });
|
|
24
59
|
await mkdir(root, { recursive: true });
|
|
25
60
|
}
|
|
26
|
-
|
|
61
|
+
async function canRunUnzip() {
|
|
62
|
+
try {
|
|
63
|
+
await execFileAsync("unzip", ["-v"], { timeout: 5_000 });
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Extract one zip into dest (merge). Prefer system `unzip` — extract-zip + a
|
|
72
|
+
* full-tree cp of ~40k files is slow and has been hanging/exiting mid-install.
|
|
73
|
+
*/
|
|
27
74
|
async function extractZipMerge(zipPath, dest) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
75
|
+
await mkdir(dest, { recursive: true });
|
|
76
|
+
const heartbeat = setInterval(() => {
|
|
77
|
+
process.stdout.write(".");
|
|
78
|
+
}, 2_000);
|
|
31
79
|
try {
|
|
32
|
-
await
|
|
33
|
-
|
|
34
|
-
|
|
80
|
+
if (await canRunUnzip()) {
|
|
81
|
+
// -o overwrite, -q quiet (we print our own progress)
|
|
82
|
+
await execFileAsync("unzip", ["-o", "-q", zipPath, "-d", dest], {
|
|
83
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// Fallback: extract directly into dest (no tmp + cp)
|
|
88
|
+
await extract(zipPath, { dir: dest });
|
|
89
|
+
}
|
|
35
90
|
}
|
|
36
91
|
finally {
|
|
37
|
-
|
|
92
|
+
clearInterval(heartbeat);
|
|
93
|
+
process.stdout.write("\n");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Fail loudly if extract left an empty/incomplete install. */
|
|
97
|
+
async function assertInstallLooksComplete(root) {
|
|
98
|
+
const required = ["mcp", "skills"];
|
|
99
|
+
const missing = required.filter((name) => !existsSync(join(root, name)));
|
|
100
|
+
if (missing.length > 0) {
|
|
101
|
+
const listing = existsSync(root) ? (await readdir(root)).join(", ") || "(empty)" : "(missing)";
|
|
102
|
+
throw new Error(`Install looks incomplete at ${root}. Missing: ${missing.join(", ")}. ` +
|
|
103
|
+
`Contents: ${listing}`);
|
|
38
104
|
}
|
|
39
105
|
}
|
|
40
106
|
/**
|
|
@@ -88,17 +154,20 @@ export async function installFromRelease(release, target, root = installRoot())
|
|
|
88
154
|
console.log(" Extracting…");
|
|
89
155
|
await cleanInstallRoot(root);
|
|
90
156
|
for (const file of downloaded) {
|
|
91
|
-
|
|
157
|
+
const zipInfo = await stat(file.zipPath);
|
|
158
|
+
process.stdout.write(` ▸ ${file.id} (${formatBytes(zipInfo.size)}) `);
|
|
92
159
|
try {
|
|
93
160
|
await extractZipMerge(file.zipPath, root);
|
|
161
|
+
console.log(` ✓ ${file.id}`);
|
|
94
162
|
}
|
|
95
163
|
catch (error) {
|
|
96
164
|
const detail = error instanceof Error ? error.message : String(error);
|
|
97
|
-
throw new Error(`Failed extracting ${file.id} (${formatBytes(
|
|
165
|
+
throw new Error(`Failed extracting ${file.id} (${formatBytes(zipInfo.size)}). ${detail}\n` +
|
|
98
166
|
`If this keeps happening, free disk space or try again with:\n` +
|
|
99
167
|
` NODE_OPTIONS=--max-old-space-size=8192 npx @nucleoicons/skills setup …`);
|
|
100
168
|
}
|
|
101
169
|
}
|
|
170
|
+
await assertInstallLooksComplete(root);
|
|
102
171
|
}
|
|
103
172
|
finally {
|
|
104
173
|
await rm(downloadsDir, { recursive: true, force: true }).catch(() => undefined);
|
|
@@ -157,10 +226,7 @@ export async function buildMcp(root = installRoot()) {
|
|
|
157
226
|
const hasDist = existsSync(distEntry);
|
|
158
227
|
console.log(" npm install (mcp)…");
|
|
159
228
|
try {
|
|
160
|
-
await
|
|
161
|
-
cwd: mcpDir,
|
|
162
|
-
env: process.env,
|
|
163
|
-
});
|
|
229
|
+
await runNpm(["install", "--omit=dev"], mcpDir);
|
|
164
230
|
}
|
|
165
231
|
catch (error) {
|
|
166
232
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -169,10 +235,7 @@ export async function buildMcp(root = installRoot()) {
|
|
|
169
235
|
if (!hasDist || existsSync(join(mcpDir, "src"))) {
|
|
170
236
|
console.log(" npm run build (mcp)…");
|
|
171
237
|
try {
|
|
172
|
-
await
|
|
173
|
-
cwd: mcpDir,
|
|
174
|
-
env: process.env,
|
|
175
|
-
});
|
|
238
|
+
await runNpm(["run", "build"], mcpDir);
|
|
176
239
|
}
|
|
177
240
|
catch (error) {
|
|
178
241
|
if (!existsSync(distEntry)) {
|