@nucleoicons/skills 0.1.6 → 0.1.8
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 +14 -2
- package/dist/api.d.ts +5 -0
- package/dist/api.js +20 -1
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +38 -12
- package/dist/commands/status.js +1 -0
- package/dist/commands/update.js +12 -4
- package/dist/config.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/install.d.ts +38 -4
- package/dist/install.js +94 -12
- package/package.json +2 -2
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
|
|
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,25 @@ Requires **Node.js 20+**.
|
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
|
+
**Pro** (Nucleo account — licensed families **plus** free sets in the same install):
|
|
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
|
+
|
|
29
|
+
If Pro is already installed, `setup --free` **merges** free families in and keeps Pro. Free-only installs never download Pro families (login required for Pro).
|
|
30
|
+
|
|
21
31
|
**Required for setup:** `--target` (`react` or `vanilla`) and `--tools` (`cursor`, `claude`, `codex`).
|
|
22
32
|
|
|
23
33
|
```bash
|
|
24
34
|
npx @nucleoicons/skills setup --target react --tools cursor
|
|
25
|
-
npx @nucleoicons/skills setup --target vanilla --tools cursor,claude
|
|
35
|
+
npx @nucleoicons/skills setup --free --target vanilla --tools cursor,claude
|
|
26
36
|
```
|
|
27
37
|
|
|
28
38
|
Setup installs the package under your user home (`~/.nucleo/skills`) and configures the editors you choose (skills + MCP, no manual config step).
|
|
@@ -35,6 +45,8 @@ What Nucleo icon families do I have?
|
|
|
35
45
|
|
|
36
46
|
## Update
|
|
37
47
|
|
|
48
|
+
Same command for free and Pro — the CLI remembers which package you installed (`--free` is not needed again):
|
|
49
|
+
|
|
38
50
|
```bash
|
|
39
51
|
npx @nucleoicons/skills update
|
|
40
52
|
```
|
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) {
|
package/dist/commands/setup.d.ts
CHANGED
package/dist/commands/setup.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { mkdir } from "node:fs/promises";
|
|
2
1
|
import { confirm, select, checkbox } from "@inquirer/prompts";
|
|
3
|
-
import {
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { ApiError, fetchSkillsFreeRelease, fetchSkillsRelease } from "../api.js";
|
|
4
4
|
import { installRoot, nucleoHomeDir } from "../config.js";
|
|
5
|
-
import { buildMcp, ensureManifestTarget, installFromLocalDir, installFromRelease, readInstallManifest, } from "../install.js";
|
|
5
|
+
import { buildMcp, ensureManifestTarget, hasProFamilies, 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
|
|
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
|
-
|
|
24
|
-
console.log("
|
|
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,38 @@ 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
|
+
const mergingIntoPro = hasProFamilies();
|
|
68
|
+
if (mergingIntoPro) {
|
|
69
|
+
console.log("Existing Pro install detected — free icons will be added alongside Pro.\n");
|
|
70
|
+
}
|
|
71
|
+
console.log(`Fetching free ${target} release…`);
|
|
72
|
+
let release;
|
|
73
|
+
try {
|
|
74
|
+
release = await fetchSkillsFreeRelease(target);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error instanceof ApiError) {
|
|
78
|
+
throw new Error(error.message);
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
console.log(` Version: ${release.version}`);
|
|
83
|
+
console.log(` Families: ${release.families.join(", ")}`);
|
|
84
|
+
console.log(` Files: ${release.files.map((f) => f.id).join(", ")}`);
|
|
85
|
+
console.log("\nDownloading…");
|
|
86
|
+
await installFromRelease(release, target);
|
|
61
87
|
}
|
|
62
88
|
else {
|
|
63
|
-
console.log(`Fetching ${target} release…`);
|
|
89
|
+
console.log(`Fetching ${target} release (Pro + free)…`);
|
|
64
90
|
let release;
|
|
65
91
|
try {
|
|
66
|
-
release = await
|
|
92
|
+
release = await fetchProReleaseWithSession(target, options.email);
|
|
67
93
|
}
|
|
68
94
|
catch (error) {
|
|
69
95
|
if (error instanceof ApiError) {
|
|
@@ -87,8 +113,8 @@ export async function runSetup(options = {}) {
|
|
|
87
113
|
console.log("\n✓ Done");
|
|
88
114
|
console.log(` Install path: ${installRoot()}`);
|
|
89
115
|
console.log(` Target: ${manifest?.target ?? target}`);
|
|
116
|
+
console.log(` Tier: ${manifest?.tier ?? (free ? "free" : "pro")}`);
|
|
90
117
|
console.log(` Version: ${manifest?.version ?? "(none)"}`);
|
|
91
|
-
console.log(` Families: ${(manifest?.families ?? []).join(", ") || "(
|
|
92
|
-
console.log("\
|
|
93
|
-
console.log(' “What Nucleo icon families do I have?”\n');
|
|
118
|
+
console.log(` Families: ${(manifest?.families ?? []).join(", ") || "(none)"}`);
|
|
119
|
+
console.log("\nReload MCP in your editor, then try asking about Nucleo icons.\n");
|
|
94
120
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -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)"}`);
|
package/dist/commands/update.js
CHANGED
|
@@ -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
|
-
import { buildMcp, ensureManifestTarget, installFromLocalDir, installFromRelease, readInstallManifest, } from "../install.js";
|
|
4
|
+
import { buildMcp, ensureManifestTarget, installFromLocalDir, installFromRelease, isFreeOnlyInstall, readInstallManifest, } from "../install.js";
|
|
5
5
|
import { configureTool } from "../adapters/agents.js";
|
|
6
6
|
import { ensureSkillsSession, isInvalidTokenError } from "../session.js";
|
|
7
|
-
async function
|
|
7
|
+
async function fetchProReleaseWithSession(target) {
|
|
8
8
|
let session = await ensureSkillsSession();
|
|
9
9
|
try {
|
|
10
10
|
return await fetchSkillsRelease(session.token, target);
|
|
@@ -27,6 +27,7 @@ export async function runUpdate(options = {}) {
|
|
|
27
27
|
}
|
|
28
28
|
const target = manifest?.target ?? "react";
|
|
29
29
|
const localVersion = manifest?.version ?? "0.0.0";
|
|
30
|
+
const freeOnly = isFreeOnlyInstall(installRoot(), manifest);
|
|
30
31
|
if (options.from) {
|
|
31
32
|
console.log(`Updating from local folder:\n ${options.from}\n`);
|
|
32
33
|
await installFromLocalDir(options.from);
|
|
@@ -41,7 +42,14 @@ export async function runUpdate(options = {}) {
|
|
|
41
42
|
}
|
|
42
43
|
let release;
|
|
43
44
|
try {
|
|
44
|
-
|
|
45
|
+
if (freeOnly) {
|
|
46
|
+
console.log("Channel: free");
|
|
47
|
+
release = await fetchSkillsFreeRelease(target);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log("Channel: pro (+ free)");
|
|
51
|
+
release = await fetchProReleaseWithSession(target);
|
|
52
|
+
}
|
|
45
53
|
}
|
|
46
54
|
catch (error) {
|
|
47
55
|
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 | pro+free — drives update channel without requiring --free again */
|
|
19
|
+
tier?: "free" | "pro" | "pro+free" | 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,9 @@
|
|
|
1
1
|
import { type SkillsReleaseData } from "./api.js";
|
|
2
2
|
import { type InstallManifest, type PackageTarget } from "./config.js";
|
|
3
|
+
/** Licensed Pro families — never installed via the free (no-auth) channel. */
|
|
4
|
+
export declare const PRO_FAMILIES: readonly ["core", "ui", "sharp", "micro", "pixel"];
|
|
5
|
+
/** Free families from react-free.zip / vanilla-free.zip. */
|
|
6
|
+
export declare const FREE_FAMILIES: readonly ["flags", "cursors", "credit-cards", "social-media", "isometric", "arcade", "glass"];
|
|
3
7
|
/**
|
|
4
8
|
* Resolve a real npm binary. Prefer the one next to the running Node
|
|
5
9
|
* (`process.execPath`) so we don't depend on PATH / shell aliases
|
|
@@ -8,12 +12,42 @@ import { type InstallManifest, type PackageTarget } from "./config.js";
|
|
|
8
12
|
export declare function resolveNpmBin(): string;
|
|
9
13
|
export declare function readInstallManifest(root?: string): Promise<InstallManifest | null>;
|
|
10
14
|
export declare function cleanInstallRoot(root?: string): Promise<void>;
|
|
15
|
+
/** True if any Pro family catalog is present under the install root. */
|
|
16
|
+
export declare function hasProFamilies(root?: string): boolean;
|
|
17
|
+
/** True if any free family catalog is present under the install root. */
|
|
18
|
+
export declare function hasFreeFamilies(root?: string): boolean;
|
|
11
19
|
/**
|
|
12
|
-
*
|
|
13
|
-
* Downloads complete first, then the install folder is replaced — so a failed
|
|
14
|
-
* download does not wipe a previous working install.
|
|
20
|
+
* Detect installed icon families by looking for `{family}/icons.json`.
|
|
15
21
|
*/
|
|
16
|
-
export declare function
|
|
22
|
+
export declare function detectInstalledFamilies(root?: string): string[];
|
|
23
|
+
export declare function resolveInstallTier(root?: string): {
|
|
24
|
+
hasPro: boolean;
|
|
25
|
+
hasFree: boolean;
|
|
26
|
+
tier: "free" | "pro" | "pro+free";
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Free-only installs use the public free channel.
|
|
30
|
+
* Any Pro families on disk → Pro (authenticated) channel, which also ships free.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isFreeOnlyInstall(root?: string, manifest?: InstallManifest | null): boolean;
|
|
33
|
+
export interface InstallFromReleaseOptions {
|
|
34
|
+
/**
|
|
35
|
+
* When true, extract into the existing root without wiping.
|
|
36
|
+
* Used so `setup --free` can add free families beside Pro.
|
|
37
|
+
* Default: auto — merge free releases onto an existing Pro install.
|
|
38
|
+
*/
|
|
39
|
+
merge?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Download all release files (signed URLs) and install into the root.
|
|
43
|
+
*
|
|
44
|
+
* - Pro releases: replace the install root, then extract (Pro zips + free zip).
|
|
45
|
+
* - Free releases on top of Pro: merge (keep Pro families).
|
|
46
|
+
* - Free-only: replace the install root with free content.
|
|
47
|
+
*
|
|
48
|
+
* Downloads complete first — a failed download does not wipe a previous install.
|
|
49
|
+
*/
|
|
50
|
+
export declare function installFromRelease(release: SkillsReleaseData, target: PackageTarget, root?: string, options?: InstallFromReleaseOptions): Promise<void>;
|
|
17
51
|
/**
|
|
18
52
|
* Copy a local prepared package (e.g. packages/react) into the install root.
|
|
19
53
|
*/
|
package/dist/install.js
CHANGED
|
@@ -7,6 +7,18 @@ 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
|
+
/** Licensed Pro families — never installed via the free (no-auth) channel. */
|
|
11
|
+
export const PRO_FAMILIES = ["core", "ui", "sharp", "micro", "pixel"];
|
|
12
|
+
/** Free families from react-free.zip / vanilla-free.zip. */
|
|
13
|
+
export const FREE_FAMILIES = [
|
|
14
|
+
"flags",
|
|
15
|
+
"cursors",
|
|
16
|
+
"credit-cards",
|
|
17
|
+
"social-media",
|
|
18
|
+
"isometric",
|
|
19
|
+
"arcade",
|
|
20
|
+
"glass",
|
|
21
|
+
];
|
|
10
22
|
/**
|
|
11
23
|
* Resolve a real npm binary. Prefer the one next to the running Node
|
|
12
24
|
* (`process.execPath`) so we don't depend on PATH / shell aliases
|
|
@@ -58,6 +70,52 @@ export async function cleanInstallRoot(root = installRoot()) {
|
|
|
58
70
|
await rm(root, { recursive: true, force: true });
|
|
59
71
|
await mkdir(root, { recursive: true });
|
|
60
72
|
}
|
|
73
|
+
/** True if any Pro family catalog is present under the install root. */
|
|
74
|
+
export function hasProFamilies(root = installRoot()) {
|
|
75
|
+
return PRO_FAMILIES.some((family) => existsSync(join(root, family, "icons.json")));
|
|
76
|
+
}
|
|
77
|
+
/** True if any free family catalog is present under the install root. */
|
|
78
|
+
export function hasFreeFamilies(root = installRoot()) {
|
|
79
|
+
return FREE_FAMILIES.some((family) => existsSync(join(root, family, "icons.json")));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Detect installed icon families by looking for `{family}/icons.json`.
|
|
83
|
+
*/
|
|
84
|
+
export function detectInstalledFamilies(root = installRoot()) {
|
|
85
|
+
const known = [...PRO_FAMILIES, ...FREE_FAMILIES];
|
|
86
|
+
return known.filter((family) => existsSync(join(root, family, "icons.json")));
|
|
87
|
+
}
|
|
88
|
+
export function resolveInstallTier(root = installRoot()) {
|
|
89
|
+
const hasPro = hasProFamilies(root);
|
|
90
|
+
const hasFree = hasFreeFamilies(root);
|
|
91
|
+
if (hasPro && hasFree) {
|
|
92
|
+
return { hasPro, hasFree, tier: "pro+free" };
|
|
93
|
+
}
|
|
94
|
+
if (hasPro) {
|
|
95
|
+
return { hasPro, hasFree, tier: "pro" };
|
|
96
|
+
}
|
|
97
|
+
return { hasPro, hasFree, tier: "free" };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Free-only installs use the public free channel.
|
|
101
|
+
* Any Pro families on disk → Pro (authenticated) channel, which also ships free.
|
|
102
|
+
*/
|
|
103
|
+
export function isFreeOnlyInstall(root = installRoot(), manifest) {
|
|
104
|
+
if (hasProFamilies(root)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (manifest?.tier === "pro" || manifest?.tier === "pro+free") {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (manifest?.tier === "free") {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
if (typeof manifest?.product === "string" && manifest.product.includes("free")) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
// No Pro on disk and no explicit Pro tier → free-only channel.
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
61
119
|
async function canRunUnzip() {
|
|
62
120
|
try {
|
|
63
121
|
await execFileAsync("unzip", ["-v"], { timeout: 5_000 });
|
|
@@ -104,11 +162,15 @@ async function assertInstallLooksComplete(root) {
|
|
|
104
162
|
}
|
|
105
163
|
}
|
|
106
164
|
/**
|
|
107
|
-
* Download all release files (signed URLs) and
|
|
108
|
-
*
|
|
109
|
-
*
|
|
165
|
+
* Download all release files (signed URLs) and install into the root.
|
|
166
|
+
*
|
|
167
|
+
* - Pro releases: replace the install root, then extract (Pro zips + free zip).
|
|
168
|
+
* - Free releases on top of Pro: merge (keep Pro families).
|
|
169
|
+
* - Free-only: replace the install root with free content.
|
|
170
|
+
*
|
|
171
|
+
* Downloads complete first — a failed download does not wipe a previous install.
|
|
110
172
|
*/
|
|
111
|
-
export async function installFromRelease(release, target, root = installRoot()) {
|
|
173
|
+
export async function installFromRelease(release, target, root = installRoot(), options = {}) {
|
|
112
174
|
if (!release.downloadReady) {
|
|
113
175
|
throw new Error(release.message ||
|
|
114
176
|
"Downloads are not ready on the server yet. Please try again later.");
|
|
@@ -117,6 +179,15 @@ export async function installFromRelease(release, target, root = installRoot())
|
|
|
117
179
|
if (missingUrl.length > 0) {
|
|
118
180
|
throw new Error(`Release files missing download urls: ${missingUrl.map((f) => f.id).join(", ")}`);
|
|
119
181
|
}
|
|
182
|
+
const isFreeRelease = release.tier === "free";
|
|
183
|
+
const merge = options.merge ?? (isFreeRelease && existsSync(root) && hasProFamilies(root));
|
|
184
|
+
// Safety: free channel must never claim Pro family modules.
|
|
185
|
+
if (isFreeRelease) {
|
|
186
|
+
const forbidden = release.files.filter((f) => PRO_FAMILIES.includes(f.id));
|
|
187
|
+
if (forbidden.length > 0) {
|
|
188
|
+
throw new Error(`Free release unexpectedly included Pro modules: ${forbidden.map((f) => f.id).join(", ")}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
120
191
|
const downloadsDir = join(nucleoHomeDir(), "downloads");
|
|
121
192
|
await mkdir(downloadsDir, { recursive: true });
|
|
122
193
|
const downloaded = [];
|
|
@@ -150,9 +221,14 @@ export async function installFromRelease(release, target, root = installRoot())
|
|
|
150
221
|
}
|
|
151
222
|
downloaded.push({ id: file.id, zipPath });
|
|
152
223
|
}
|
|
153
|
-
// 2)
|
|
154
|
-
console.log(" Extracting…");
|
|
155
|
-
|
|
224
|
+
// 2) Extract — wipe unless merging free into Pro
|
|
225
|
+
console.log(merge ? " Merging into existing install (Pro families kept)…" : " Extracting…");
|
|
226
|
+
if (!merge) {
|
|
227
|
+
await cleanInstallRoot(root);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
await mkdir(root, { recursive: true });
|
|
231
|
+
}
|
|
156
232
|
for (const file of downloaded) {
|
|
157
233
|
const zipInfo = await stat(file.zipPath);
|
|
158
234
|
process.stdout.write(` ▸ ${file.id} (${formatBytes(zipInfo.size)}) `);
|
|
@@ -172,11 +248,14 @@ export async function installFromRelease(release, target, root = installRoot())
|
|
|
172
248
|
finally {
|
|
173
249
|
await rm(downloadsDir, { recursive: true, force: true }).catch(() => undefined);
|
|
174
250
|
}
|
|
251
|
+
const families = detectInstalledFamilies(root);
|
|
252
|
+
const { tier } = resolveInstallTier(root);
|
|
175
253
|
await writeInstallManifest({
|
|
176
|
-
product: "nucleo-skills",
|
|
254
|
+
product: tier === "free" ? "nucleo-skills-free" : "nucleo-skills",
|
|
255
|
+
tier,
|
|
177
256
|
target,
|
|
178
257
|
version: release.version,
|
|
179
|
-
families
|
|
258
|
+
families,
|
|
180
259
|
}, root);
|
|
181
260
|
}
|
|
182
261
|
/**
|
|
@@ -204,15 +283,18 @@ export async function ensureManifestTarget(target, extras = {}, root = installRo
|
|
|
204
283
|
manifest = {};
|
|
205
284
|
}
|
|
206
285
|
}
|
|
286
|
+
const families = extras.families ?? detectInstalledFamilies(root);
|
|
287
|
+
const { tier: detectedTier } = resolveInstallTier(root);
|
|
207
288
|
manifest = {
|
|
208
289
|
...manifest,
|
|
209
290
|
...extras,
|
|
210
291
|
target: extras.target ?? manifest.target ?? target,
|
|
211
292
|
version: extras.version ?? manifest.version ?? "0.0.0-local",
|
|
293
|
+
families,
|
|
294
|
+
tier: extras.tier ?? detectedTier,
|
|
295
|
+
product: extras.product ??
|
|
296
|
+
(detectedTier === "free" ? "nucleo-skills-free" : "nucleo-skills"),
|
|
212
297
|
};
|
|
213
|
-
if (extras.families) {
|
|
214
|
-
manifest.families = extras.families;
|
|
215
|
-
}
|
|
216
298
|
await writeInstallManifest(manifest, root);
|
|
217
299
|
return manifest;
|
|
218
300
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nucleoicons/skills",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "CLI to install and update Nucleo
|
|
3
|
+
"version": "0.1.8",
|
|
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"
|