@nucleoicons/skills 0.1.5 → 0.1.7

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 CHANGED
@@ -4,7 +4,7 @@ CLI to install and update **Nucleo** MCP + icon skills on your machine.
4
4
 
5
5
  Once installed, your AI coding tool can help you **use Nucleo icons in code**. Typical uses:
6
6
 
7
- - Search icons by name or idea across your licensed families (core, ui, sharp, micro, pixel)
7
+ - Search icons by name or idea across your installed families (Pro and/or free)
8
8
  - Pick an icon and get the right React component or SVG for your project
9
9
  - Customize icons when inserting them (color, secondary color, stroke width, duo opacity, corners)
10
10
 
@@ -14,15 +14,23 @@ Requires **Node.js 20+**.
14
14
 
15
15
  ## Install
16
16
 
17
+ **Pro** (Nucleo account — licensed families):
18
+
17
19
  ```bash
18
20
  npx @nucleoicons/skills setup --target react --tools cursor
19
21
  ```
20
22
 
23
+ **Free** (no account — glass, arcade, isometric, flags, cursors, social-media, credit-cards):
24
+
25
+ ```bash
26
+ npx @nucleoicons/skills setup --free --target react --tools cursor
27
+ ```
28
+
21
29
  **Required for setup:** `--target` (`react` or `vanilla`) and `--tools` (`cursor`, `claude`, `codex`).
22
30
 
23
31
  ```bash
24
32
  npx @nucleoicons/skills setup --target react --tools cursor
25
- npx @nucleoicons/skills setup --target vanilla --tools cursor,claude
33
+ npx @nucleoicons/skills setup --free --target vanilla --tools cursor,claude
26
34
  ```
27
35
 
28
36
  Setup installs the package under your user home (`~/.nucleo/skills`) and configures the editors you choose (skills + MCP, no manual config step).
@@ -35,6 +43,8 @@ What Nucleo icon families do I have?
35
43
 
36
44
  ## Update
37
45
 
46
+ Same command for free and Pro — the CLI remembers which package you installed (`--free` is not needed again):
47
+
38
48
  ```bash
39
49
  npx @nucleoicons/skills update
40
50
  ```
package/dist/api.d.ts CHANGED
@@ -27,6 +27,7 @@ export interface SkillsReleaseFile {
27
27
  export interface SkillsReleaseData {
28
28
  version: string;
29
29
  target: PackageTarget | string;
30
+ tier?: "free" | "pro" | string;
30
31
  families: string[];
31
32
  downloadReady: boolean;
32
33
  files: SkillsReleaseFile[];
@@ -47,6 +48,10 @@ export declare function skillsLogin(email: string, password: string): Promise<Sk
47
48
  * Response: { data: { version, families, files, downloadReady } }
48
49
  */
49
50
  export declare function fetchSkillsRelease(token: string, target: PackageTarget): Promise<SkillsReleaseData>;
51
+ /**
52
+ * GET /skills/release-free?target=react|vanilla (no auth)
53
+ */
54
+ export declare function fetchSkillsFreeRelease(target: PackageTarget): Promise<SkillsReleaseData>;
50
55
  declare function formatBytes(bytes: number): string;
51
56
  /** Download a remote file (signed R2 URL) to disk, with optional progress. */
52
57
  export declare function downloadUrlToFile(url: string, destPath: string, onProgress?: (progress: DownloadProgress) => void): Promise<{
package/dist/api.js CHANGED
@@ -83,7 +83,26 @@ export async function fetchSkillsRelease(token, target) {
83
83
  if (!json.data?.version || !Array.isArray(json.data.files)) {
84
84
  throw new ApiError("Release response missing version/files.", res.status, body);
85
85
  }
86
- return json.data;
86
+ return { ...json.data, tier: json.data.tier ?? "pro" };
87
+ }
88
+ /**
89
+ * GET /skills/release-free?target=react|vanilla (no auth)
90
+ */
91
+ export async function fetchSkillsFreeRelease(target) {
92
+ const url = new URL(`${getApiBaseUrl()}/skills/release-free`);
93
+ url.searchParams.set("target", target);
94
+ const res = await fetch(url, {
95
+ headers: { Accept: "application/json" },
96
+ });
97
+ const body = await readErrorBody(res);
98
+ if (!res.ok) {
99
+ throw new ApiError(parseApiErrorMessage(body, `Could not fetch free release (${res.status}).`), res.status, body);
100
+ }
101
+ const json = JSON.parse(body);
102
+ if (!json.data?.version || !Array.isArray(json.data.files)) {
103
+ throw new ApiError("Free release response missing version/files.", res.status, body);
104
+ }
105
+ return { ...json.data, tier: json.data.tier ?? "free" };
87
106
  }
88
107
  function formatBytes(bytes) {
89
108
  if (bytes < 1024) {
@@ -6,5 +6,7 @@ export interface SetupOptions {
6
6
  tools?: AgentTool[];
7
7
  email?: string;
8
8
  yes?: boolean;
9
+ /** Free icons package — no Nucleo login */
10
+ free?: boolean;
9
11
  }
10
12
  export declare function runSetup(options?: SetupOptions): Promise<void>;
@@ -1,11 +1,11 @@
1
- import { mkdir } from "node:fs/promises";
2
1
  import { confirm, select, checkbox } from "@inquirer/prompts";
3
- import { ApiError, fetchSkillsRelease } from "../api.js";
2
+ import { mkdir } from "node:fs/promises";
3
+ import { ApiError, fetchSkillsFreeRelease, fetchSkillsRelease } from "../api.js";
4
4
  import { installRoot, nucleoHomeDir } from "../config.js";
5
5
  import { buildMcp, ensureManifestTarget, installFromLocalDir, installFromRelease, readInstallManifest, } from "../install.js";
6
6
  import { configureTool } from "../adapters/agents.js";
7
7
  import { ensureSkillsSession, isInvalidTokenError } from "../session.js";
8
- async function fetchReleaseWithSession(target, emailHint) {
8
+ async function fetchProReleaseWithSession(target, emailHint) {
9
9
  let session = await ensureSkillsSession(emailHint);
10
10
  try {
11
11
  return await fetchSkillsRelease(session.token, target);
@@ -20,8 +20,9 @@ async function fetchReleaseWithSession(target, emailHint) {
20
20
  }
21
21
  }
22
22
  export async function runSetup(options = {}) {
23
- console.log("\nNucleo Skills setup");
24
- console.log("───────────────────\n");
23
+ const free = Boolean(options.free);
24
+ console.log(free ? "\nNucleo Skills setup (free)" : "\nNucleo Skills setup");
25
+ console.log(free ? "─────────────────────────\n" : "───────────────────\n");
25
26
  const target = options.target ??
26
27
  (await select({
27
28
  message: "Which package?",
@@ -57,13 +58,34 @@ export async function runSetup(options = {}) {
57
58
  }
58
59
  }
59
60
  await installFromLocalDir(options.from);
60
- await ensureManifestTarget(target);
61
+ await ensureManifestTarget(target, {
62
+ tier: free ? "free" : "pro",
63
+ product: free ? "nucleo-skills-free" : "nucleo-skills",
64
+ });
65
+ }
66
+ else if (free) {
67
+ console.log(`Fetching free ${target} release…`);
68
+ let release;
69
+ try {
70
+ release = await fetchSkillsFreeRelease(target);
71
+ }
72
+ catch (error) {
73
+ if (error instanceof ApiError) {
74
+ throw new Error(error.message);
75
+ }
76
+ throw error;
77
+ }
78
+ console.log(` Version: ${release.version}`);
79
+ console.log(` Families: ${release.families.join(", ")}`);
80
+ console.log(` Files: ${release.files.map((f) => f.id).join(", ")}`);
81
+ console.log("\nDownloading…");
82
+ await installFromRelease(release, target);
61
83
  }
62
84
  else {
63
85
  console.log(`Fetching ${target} release…`);
64
86
  let release;
65
87
  try {
66
- release = await fetchReleaseWithSession(target, options.email);
88
+ release = await fetchProReleaseWithSession(target, options.email);
67
89
  }
68
90
  catch (error) {
69
91
  if (error instanceof ApiError) {
@@ -87,8 +109,7 @@ export async function runSetup(options = {}) {
87
109
  console.log("\n✓ Done");
88
110
  console.log(` Install path: ${installRoot()}`);
89
111
  console.log(` Target: ${manifest?.target ?? target}`);
112
+ console.log(` Tier: ${manifest?.tier ?? (free ? "free" : "pro")}`);
90
113
  console.log(` Version: ${manifest?.version ?? "(none)"}`);
91
- console.log(` Families: ${(manifest?.families ?? []).join(", ") || "(see manifest)"}`);
92
- console.log("\nNext: reload MCP in your editor, then ask:");
93
- console.log(' “What Nucleo icon families do I have?”\n');
114
+ console.log("\nReload MCP in your editor, then try asking about Nucleo icons.\n");
94
115
  }
@@ -23,6 +23,7 @@ export async function runStatus() {
23
23
  const manifest = await readInstallManifest();
24
24
  console.log(`Version: ${manifest?.version ?? "(missing)"}`);
25
25
  console.log(`Target: ${manifest?.target ?? "(missing)"}`);
26
+ console.log(`Tier: ${manifest?.tier ?? "(missing)"}`);
26
27
  console.log(`Product: ${manifest?.product ?? "(missing)"}`);
27
28
  console.log(`Families: ${(manifest?.families ?? []).join(", ") || "(none)"}`);
28
29
  console.log(`MCP entry: ${existsSync(mcpEntryPath()) ? mcpEntryPath() : "(missing — run setup/update)"}`);
@@ -1,10 +1,10 @@
1
1
  import { confirm } from "@inquirer/prompts";
2
- import { ApiError, fetchSkillsRelease } from "../api.js";
2
+ import { ApiError, fetchSkillsFreeRelease, fetchSkillsRelease } from "../api.js";
3
3
  import { installRoot } from "../config.js";
4
4
  import { buildMcp, ensureManifestTarget, installFromLocalDir, installFromRelease, readInstallManifest, } from "../install.js";
5
5
  import { configureTool } from "../adapters/agents.js";
6
6
  import { ensureSkillsSession, isInvalidTokenError } from "../session.js";
7
- async function fetchReleaseWithSession(target) {
7
+ async function fetchProReleaseWithSession(target) {
8
8
  let session = await ensureSkillsSession();
9
9
  try {
10
10
  return await fetchSkillsRelease(session.token, target);
@@ -18,6 +18,18 @@ async function fetchReleaseWithSession(target) {
18
18
  throw error;
19
19
  }
20
20
  }
21
+ function isFreeInstall(manifest) {
22
+ if (!manifest) {
23
+ return false;
24
+ }
25
+ if (manifest.tier === "free") {
26
+ return true;
27
+ }
28
+ if (typeof manifest.product === "string" && manifest.product.includes("free")) {
29
+ return true;
30
+ }
31
+ return false;
32
+ }
21
33
  export async function runUpdate(options = {}) {
22
34
  console.log("\nNucleo Skills update");
23
35
  console.log("────────────────────\n");
@@ -27,10 +39,14 @@ export async function runUpdate(options = {}) {
27
39
  }
28
40
  const target = manifest?.target ?? "react";
29
41
  const localVersion = manifest?.version ?? "0.0.0";
42
+ const free = isFreeInstall(manifest);
30
43
  if (options.from) {
31
44
  console.log(`Updating from local folder:\n ${options.from}\n`);
32
45
  await installFromLocalDir(options.from);
33
- await ensureManifestTarget(target);
46
+ await ensureManifestTarget(target, {
47
+ tier: free ? "free" : "pro",
48
+ product: free ? "nucleo-skills-free" : "nucleo-skills",
49
+ });
34
50
  console.log("Building MCP…");
35
51
  await buildMcp();
36
52
  await reconfigure(options.tools);
@@ -41,7 +57,14 @@ export async function runUpdate(options = {}) {
41
57
  }
42
58
  let release;
43
59
  try {
44
- release = await fetchReleaseWithSession(target);
60
+ if (free) {
61
+ console.log("Channel: free");
62
+ release = await fetchSkillsFreeRelease(target);
63
+ }
64
+ else {
65
+ console.log("Channel: pro");
66
+ release = await fetchProReleaseWithSession(target);
67
+ }
45
68
  }
46
69
  catch (error) {
47
70
  if (error instanceof ApiError) {
package/dist/config.d.ts CHANGED
@@ -15,6 +15,8 @@ export interface NucleoCredentials {
15
15
  }
16
16
  export interface InstallManifest {
17
17
  product?: string;
18
+ /** free | pro — drives update channel without requiring --free again */
19
+ tier?: "free" | "pro" | string;
18
20
  target?: PackageTarget | string;
19
21
  version?: string;
20
22
  families?: string[];
package/dist/index.js CHANGED
@@ -54,6 +54,7 @@ program
54
54
  .description("Sign in, download your licensed package, configure your editor")
55
55
  .option("-t, --target <target>", "react | vanilla")
56
56
  .option("--tools <list>", "comma-separated: cursor,claude,codex (default: prompt, or cursor with --yes)")
57
+ .option("--free", "Install free icon families (no Nucleo login)")
57
58
  .option("--from <dir>", "Install from a local prepared package (skip API download)")
58
59
  .option("--email <email>", "Nucleo account email (skip prompt)")
59
60
  .option("-y, --yes", "Skip confirmation prompts")
@@ -70,6 +71,7 @@ program
70
71
  tools,
71
72
  email: opts.email,
72
73
  yes: Boolean(opts.yes),
74
+ free: Boolean(opts.free),
73
75
  });
74
76
  }
75
77
  catch (error) {
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
2
  import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
3
- import { join } from "node:path";
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)) {
@@ -138,7 +173,8 @@ export async function installFromRelease(release, target, root = installRoot())
138
173
  await rm(downloadsDir, { recursive: true, force: true }).catch(() => undefined);
139
174
  }
140
175
  await writeInstallManifest({
141
- product: "nucleo-skills",
176
+ product: release.tier === "free" ? "nucleo-skills-free" : "nucleo-skills",
177
+ tier: release.tier === "free" ? "free" : "pro",
142
178
  target,
143
179
  version: release.version,
144
180
  families: release.families,
@@ -191,10 +227,7 @@ export async function buildMcp(root = installRoot()) {
191
227
  const hasDist = existsSync(distEntry);
192
228
  console.log(" npm install (mcp)…");
193
229
  try {
194
- await execFileAsync("npm", ["install", "--omit=dev"], {
195
- cwd: mcpDir,
196
- env: process.env,
197
- });
230
+ await runNpm(["install", "--omit=dev"], mcpDir);
198
231
  }
199
232
  catch (error) {
200
233
  const detail = error instanceof Error ? error.message : String(error);
@@ -203,10 +236,7 @@ export async function buildMcp(root = installRoot()) {
203
236
  if (!hasDist || existsSync(join(mcpDir, "src"))) {
204
237
  console.log(" npm run build (mcp)…");
205
238
  try {
206
- await execFileAsync("npm", ["run", "build"], {
207
- cwd: mcpDir,
208
- env: process.env,
209
- });
239
+ await runNpm(["run", "build"], mcpDir);
210
240
  }
211
241
  catch (error) {
212
242
  if (!existsSync(distEntry)) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nucleoicons/skills",
3
- "version": "0.1.5",
4
- "description": "CLI to install and update Nucleo Pro MCP + icon skills",
3
+ "version": "0.1.7",
4
+ "description": "CLI to install and update Nucleo MCP + icon skills (Pro and free)",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "nucleo-skills": "./dist/index.js"