@nucleoicons/skills 0.1.8 → 0.1.10
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 +18 -10
- package/dist/api.d.ts +2 -2
- package/dist/api.js +2 -2
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +17 -6
- package/dist/commands/status.js +3 -2
- package/dist/commands/update.js +21 -3
- package/dist/config.d.ts +6 -1
- package/dist/config.js +4 -0
- package/dist/index.js +1 -1
- package/dist/install.d.ts +17 -3
- package/dist/install.js +188 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ CLI to install and update **Nucleo** MCP + icon skills on your machine.
|
|
|
5
5
|
Once installed, your AI coding tool can help you **use Nucleo icons in code**. Typical uses:
|
|
6
6
|
|
|
7
7
|
- Search icons by name or idea across your installed families (Pro and/or free)
|
|
8
|
-
- Pick an icon and get the right React component or SVG for your project
|
|
8
|
+
- Pick an icon and get the right **React** component, **Vue** SFC, or **vanilla SVG** for your project
|
|
9
9
|
- Customize icons when inserting them (color, secondary color, stroke width, duo opacity, corners)
|
|
10
10
|
|
|
11
11
|
Ask your editor naturally, for example: “find a settings icon in Nucleo” or “insert the home icon as a React component.”
|
|
@@ -26,17 +26,25 @@ npx @nucleoicons/skills setup --target react --tools cursor
|
|
|
26
26
|
npx @nucleoicons/skills setup --free --target react --tools cursor
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
**Required for setup:** `--target` (`react` or `vanilla`) and `--tools` (`cursor`, `claude`, `codex`).
|
|
29
|
+
**Required for setup:** `--target` (`react`, `vue`, or `vanilla`) and `--tools` (`cursor`, `claude`, `codex`).
|
|
32
30
|
|
|
33
31
|
```bash
|
|
34
32
|
npx @nucleoicons/skills setup --target react --tools cursor
|
|
35
|
-
npx @nucleoicons/skills setup --
|
|
33
|
+
npx @nucleoicons/skills setup --target vue --tools cursor
|
|
34
|
+
npx @nucleoicons/skills setup --target vanilla --tools cursor,claude
|
|
35
|
+
npx @nucleoicons/skills setup --free --target vue --tools cursor
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
| `--target` | What you get |
|
|
39
|
+
| ---------- | ------------------------- |
|
|
40
|
+
| `react` | React components (`.tsx`) |
|
|
41
|
+
| `vue` | Vue SFCs (`.vue`) |
|
|
42
|
+
| `vanilla` | SVG files |
|
|
43
|
+
|
|
38
44
|
Setup installs the package under your user home (`~/.nucleo/skills`) and configures the editors you choose (skills + MCP, no manual config step).
|
|
39
45
|
|
|
46
|
+
You can install more than one target. A later `setup --target vue` keeps an existing React install, and the other way around. `update` refreshes every target already on the machine.
|
|
47
|
+
|
|
40
48
|
Reload MCP in your editor, then ask something like:
|
|
41
49
|
|
|
42
50
|
```text
|
|
@@ -45,7 +53,7 @@ What Nucleo icon families do I have?
|
|
|
45
53
|
|
|
46
54
|
## Update
|
|
47
55
|
|
|
48
|
-
Same command for free and Pro
|
|
56
|
+
Same command for free and Pro. The CLI updates every target you have installed (`--free` is not needed again):
|
|
49
57
|
|
|
50
58
|
```bash
|
|
51
59
|
npx @nucleoicons/skills update
|
|
@@ -59,8 +67,8 @@ After updating, reload MCP in your editor again.
|
|
|
59
67
|
|
|
60
68
|
## Editors
|
|
61
69
|
|
|
62
|
-
| Tool | `--tools` value | Auto-configured
|
|
63
|
-
| ----------- | --------------- |
|
|
64
|
-
| Cursor | `cursor` | `~/.cursor/mcp.json` + `~/.cursor/skills/`
|
|
65
|
-
| Claude Code | `claude` | `~/.claude.json` + `~/.claude/skills/`
|
|
70
|
+
| Tool | `--tools` value | Auto-configured |
|
|
71
|
+
| ----------- | --------------- | -------------------------------------------- |
|
|
72
|
+
| Cursor | `cursor` | `~/.cursor/mcp.json` + `~/.cursor/skills/` |
|
|
73
|
+
| Claude Code | `claude` | `~/.claude.json` + `~/.claude/skills/` |
|
|
66
74
|
| Codex | `codex` | `~/.codex/config.toml` + `~/.agents/skills/` |
|
package/dist/api.d.ts
CHANGED
|
@@ -44,12 +44,12 @@ export interface DownloadProgress {
|
|
|
44
44
|
*/
|
|
45
45
|
export declare function skillsLogin(email: string, password: string): Promise<SkillsLoginData>;
|
|
46
46
|
/**
|
|
47
|
-
* GET /skills/release?token=…&target=react|vanilla
|
|
47
|
+
* GET /skills/release?token=…&target=react|vanilla|vue
|
|
48
48
|
* Response: { data: { version, families, files, downloadReady } }
|
|
49
49
|
*/
|
|
50
50
|
export declare function fetchSkillsRelease(token: string, target: PackageTarget): Promise<SkillsReleaseData>;
|
|
51
51
|
/**
|
|
52
|
-
* GET /skills/release-free?target=react|vanilla (no auth)
|
|
52
|
+
* GET /skills/release-free?target=react|vanilla|vue (no auth)
|
|
53
53
|
*/
|
|
54
54
|
export declare function fetchSkillsFreeRelease(target: PackageTarget): Promise<SkillsReleaseData>;
|
|
55
55
|
declare function formatBytes(bytes: number): string;
|
package/dist/api.js
CHANGED
|
@@ -65,7 +65,7 @@ export async function skillsLogin(email, password) {
|
|
|
65
65
|
return json.data;
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
|
-
* GET /skills/release?token=…&target=react|vanilla
|
|
68
|
+
* GET /skills/release?token=…&target=react|vanilla|vue
|
|
69
69
|
* Response: { data: { version, families, files, downloadReady } }
|
|
70
70
|
*/
|
|
71
71
|
export async function fetchSkillsRelease(token, target) {
|
|
@@ -86,7 +86,7 @@ export async function fetchSkillsRelease(token, target) {
|
|
|
86
86
|
return { ...json.data, tier: json.data.tier ?? "pro" };
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
|
-
* GET /skills/release-free?target=react|vanilla (no auth)
|
|
89
|
+
* GET /skills/release-free?target=react|vanilla|vue (no auth)
|
|
90
90
|
*/
|
|
91
91
|
export async function fetchSkillsFreeRelease(target) {
|
|
92
92
|
const url = new URL(`${getApiBaseUrl()}/skills/release-free`);
|
package/dist/commands/setup.d.ts
CHANGED
package/dist/commands/setup.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { confirm, select, checkbox } from "@inquirer/prompts";
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
3
|
import { ApiError, fetchSkillsFreeRelease, fetchSkillsRelease } from "../api.js";
|
|
4
|
-
import { installRoot, nucleoHomeDir } from "../config.js";
|
|
5
|
-
import { buildMcp, ensureManifestTarget, hasProFamilies, installFromLocalDir, installFromRelease, readInstallManifest, } from "../install.js";
|
|
4
|
+
import { installRoot, isPackageTarget, nucleoHomeDir, } from "../config.js";
|
|
5
|
+
import { buildMcp, ensureManifestTarget, hasProFamilies, installFromLocalDir, installFromRelease, readInstallManifest, rootHasIconFamilies, } from "../install.js";
|
|
6
6
|
import { configureTool } from "../adapters/agents.js";
|
|
7
7
|
import { ensureSkillsSession, isInvalidTokenError } from "../session.js";
|
|
8
8
|
async function fetchProReleaseWithSession(target, emailHint) {
|
|
@@ -23,11 +23,15 @@ export async function runSetup(options = {}) {
|
|
|
23
23
|
const free = Boolean(options.free);
|
|
24
24
|
console.log(free ? "\nNucleo Skills setup (free)" : "\nNucleo Skills setup");
|
|
25
25
|
console.log(free ? "─────────────────────────\n" : "───────────────────\n");
|
|
26
|
+
if (options.target && !isPackageTarget(options.target)) {
|
|
27
|
+
throw new Error(`Unknown target "${options.target}". Use react, vanilla, or vue.`);
|
|
28
|
+
}
|
|
26
29
|
const target = options.target ??
|
|
27
30
|
(await select({
|
|
28
31
|
message: "Which package?",
|
|
29
32
|
choices: [
|
|
30
33
|
{ name: "React components", value: "react" },
|
|
34
|
+
{ name: "Vue components", value: "vue" },
|
|
31
35
|
{ name: "Vanilla SVG", value: "vanilla" },
|
|
32
36
|
],
|
|
33
37
|
}));
|
|
@@ -45,6 +49,12 @@ export async function runSetup(options = {}) {
|
|
|
45
49
|
throw new Error("Select at least one tool to configure.");
|
|
46
50
|
}
|
|
47
51
|
await mkdir(nucleoHomeDir(), { recursive: true });
|
|
52
|
+
const existing = await readInstallManifest();
|
|
53
|
+
const primary = existing?.target && isPackageTarget(existing.target) ? existing.target : null;
|
|
54
|
+
const placement = rootHasIconFamilies() && primary && primary !== target ? "nested" : "root";
|
|
55
|
+
if (placement === "nested") {
|
|
56
|
+
console.log(`Keeping the installed ${primary} package. Adding ${target} alongside it.\n`);
|
|
57
|
+
}
|
|
48
58
|
if (options.from) {
|
|
49
59
|
console.log(`\nInstalling from local folder:\n ${options.from}\n`);
|
|
50
60
|
if (!options.yes) {
|
|
@@ -64,7 +74,7 @@ export async function runSetup(options = {}) {
|
|
|
64
74
|
});
|
|
65
75
|
}
|
|
66
76
|
else if (free) {
|
|
67
|
-
const mergingIntoPro = hasProFamilies();
|
|
77
|
+
const mergingIntoPro = placement === "root" && hasProFamilies();
|
|
68
78
|
if (mergingIntoPro) {
|
|
69
79
|
console.log("Existing Pro install detected — free icons will be added alongside Pro.\n");
|
|
70
80
|
}
|
|
@@ -83,7 +93,7 @@ export async function runSetup(options = {}) {
|
|
|
83
93
|
console.log(` Families: ${release.families.join(", ")}`);
|
|
84
94
|
console.log(` Files: ${release.files.map((f) => f.id).join(", ")}`);
|
|
85
95
|
console.log("\nDownloading…");
|
|
86
|
-
await installFromRelease(release, target);
|
|
96
|
+
await installFromRelease(release, target, undefined, { placement });
|
|
87
97
|
}
|
|
88
98
|
else {
|
|
89
99
|
console.log(`Fetching ${target} release (Pro + free)…`);
|
|
@@ -101,7 +111,7 @@ export async function runSetup(options = {}) {
|
|
|
101
111
|
console.log(` Families: ${release.families.join(", ")}`);
|
|
102
112
|
console.log(` Files: ${release.files.map((f) => f.id).join(", ")}`);
|
|
103
113
|
console.log("\nDownloading…");
|
|
104
|
-
await installFromRelease(release, target);
|
|
114
|
+
await installFromRelease(release, target, undefined, { placement });
|
|
105
115
|
}
|
|
106
116
|
console.log("Building MCP…");
|
|
107
117
|
await buildMcp();
|
|
@@ -112,7 +122,8 @@ export async function runSetup(options = {}) {
|
|
|
112
122
|
const manifest = await readInstallManifest();
|
|
113
123
|
console.log("\n✓ Done");
|
|
114
124
|
console.log(` Install path: ${installRoot()}`);
|
|
115
|
-
|
|
125
|
+
const installedTargets = manifest?.targets?.filter((entry) => typeof entry === "string") ?? [];
|
|
126
|
+
console.log(` Targets: ${installedTargets.join(", ") || manifest?.target || target}`);
|
|
116
127
|
console.log(` Tier: ${manifest?.tier ?? (free ? "free" : "pro")}`);
|
|
117
128
|
console.log(` Version: ${manifest?.version ?? "(none)"}`);
|
|
118
129
|
console.log(` Families: ${(manifest?.families ?? []).join(", ") || "(none)"}`);
|
package/dist/commands/status.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { installRoot, credentialsPath } from "../config.js";
|
|
3
3
|
import { loadCredentials } from "../credentials.js";
|
|
4
|
-
import { mcpEntryPath, readInstallManifest } from "../install.js";
|
|
4
|
+
import { listInstalledTargets, mcpEntryPath, readInstallManifest } from "../install.js";
|
|
5
5
|
export async function runStatus() {
|
|
6
6
|
console.log("\nNucleo Skills status");
|
|
7
7
|
console.log("────────────────────\n");
|
|
@@ -22,7 +22,8 @@ export async function runStatus() {
|
|
|
22
22
|
}
|
|
23
23
|
const manifest = await readInstallManifest();
|
|
24
24
|
console.log(`Version: ${manifest?.version ?? "(missing)"}`);
|
|
25
|
-
|
|
25
|
+
const targets = listInstalledTargets(manifest);
|
|
26
|
+
console.log(`Targets: ${targets.join(", ") || manifest?.target || "(missing)"}`);
|
|
26
27
|
console.log(`Tier: ${manifest?.tier ?? "(missing)"}`);
|
|
27
28
|
console.log(`Product: ${manifest?.product ?? "(missing)"}`);
|
|
28
29
|
console.log(`Families: ${(manifest?.families ?? []).join(", ") || "(none)"}`);
|
package/dist/commands/update.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { confirm } from "@inquirer/prompts";
|
|
2
2
|
import { ApiError, fetchSkillsFreeRelease, fetchSkillsRelease } from "../api.js";
|
|
3
3
|
import { installRoot } from "../config.js";
|
|
4
|
-
import { buildMcp, ensureManifestTarget, installFromLocalDir, installFromRelease, isFreeOnlyInstall, readInstallManifest, } from "../install.js";
|
|
4
|
+
import { buildMcp, ensureManifestTarget, installFromLocalDir, installFromRelease, isFreeOnlyInstall, listInstalledTargets, readInstallManifest, } from "../install.js";
|
|
5
5
|
import { configureTool } from "../adapters/agents.js";
|
|
6
6
|
import { ensureSkillsSession, isInvalidTokenError } from "../session.js";
|
|
7
7
|
async function fetchProReleaseWithSession(target) {
|
|
@@ -25,7 +25,8 @@ export async function runUpdate(options = {}) {
|
|
|
25
25
|
if (!manifest && !options.from) {
|
|
26
26
|
throw new Error(`No install found at ${installRoot()}.\nRun: nucleo-skills setup`);
|
|
27
27
|
}
|
|
28
|
-
const
|
|
28
|
+
const targets = listInstalledTargets(manifest ?? null);
|
|
29
|
+
const target = targets[0] ?? (manifest?.target ?? "react");
|
|
29
30
|
const localVersion = manifest?.version ?? "0.0.0";
|
|
30
31
|
const freeOnly = isFreeOnlyInstall(installRoot(), manifest);
|
|
31
32
|
if (options.from) {
|
|
@@ -60,6 +61,7 @@ export async function runUpdate(options = {}) {
|
|
|
60
61
|
const remoteVersion = release.version;
|
|
61
62
|
console.log(` Installed: ${localVersion}`);
|
|
62
63
|
console.log(` Latest: ${remoteVersion}`);
|
|
64
|
+
console.log(` Targets: ${targets.join(", ") || target}`);
|
|
63
65
|
console.log(` Families: ${release.families.join(", ")}`);
|
|
64
66
|
if (options.check) {
|
|
65
67
|
if (remoteVersion === localVersion) {
|
|
@@ -85,7 +87,23 @@ export async function runUpdate(options = {}) {
|
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
console.log("\nDownloading…");
|
|
88
|
-
|
|
90
|
+
const extras = targets.filter((installed) => installed !== target);
|
|
91
|
+
for (const extra of extras) {
|
|
92
|
+
console.log(`\n${extra}`);
|
|
93
|
+
const extraRelease = freeOnly
|
|
94
|
+
? await fetchSkillsFreeRelease(extra)
|
|
95
|
+
: await fetchProReleaseWithSession(extra);
|
|
96
|
+
await installFromRelease(extraRelease, extra, undefined, { placement: "nested" });
|
|
97
|
+
}
|
|
98
|
+
const rootRelease = extras.length === 0
|
|
99
|
+
? release
|
|
100
|
+
: freeOnly
|
|
101
|
+
? await fetchSkillsFreeRelease(target)
|
|
102
|
+
: await fetchProReleaseWithSession(target);
|
|
103
|
+
if (extras.length > 0) {
|
|
104
|
+
console.log(`\n${target}`);
|
|
105
|
+
}
|
|
106
|
+
await installFromRelease(rootRelease, target, undefined, { placement: "root" });
|
|
89
107
|
console.log("Building MCP…");
|
|
90
108
|
await buildMcp();
|
|
91
109
|
await reconfigure(options.tools ?? ["cursor"]);
|
package/dist/config.d.ts
CHANGED
|
@@ -6,7 +6,9 @@ export declare function nucleoHomeDir(): string;
|
|
|
6
6
|
export declare function credentialsPath(): string;
|
|
7
7
|
/** Where the downloaded Pro package lives (MCP, skills, icons) */
|
|
8
8
|
export declare function installRoot(): string;
|
|
9
|
-
export
|
|
9
|
+
export declare const PACKAGE_TARGETS: readonly ["react", "vanilla", "vue"];
|
|
10
|
+
export type PackageTarget = (typeof PACKAGE_TARGETS)[number];
|
|
11
|
+
export declare function isPackageTarget(value: string): value is PackageTarget;
|
|
10
12
|
export type AgentTool = "cursor" | "claude" | "codex";
|
|
11
13
|
export interface NucleoCredentials {
|
|
12
14
|
email: string;
|
|
@@ -17,7 +19,10 @@ export interface InstallManifest {
|
|
|
17
19
|
product?: string;
|
|
18
20
|
/** free | pro | pro+free — drives update channel without requiring --free again */
|
|
19
21
|
tier?: "free" | "pro" | "pro+free" | string;
|
|
22
|
+
/** Original install. Stays at the root of ~/.nucleo/skills so older packages keep working. */
|
|
20
23
|
target?: PackageTarget | string;
|
|
24
|
+
/** Every installed format, including `target`. Extra formats live in `targets/<id>/`. */
|
|
25
|
+
targets?: PackageTarget[] | string[];
|
|
21
26
|
version?: string;
|
|
22
27
|
families?: string[];
|
|
23
28
|
[key: string]: unknown;
|
package/dist/config.js
CHANGED
|
@@ -19,3 +19,7 @@ export function credentialsPath() {
|
|
|
19
19
|
export function installRoot() {
|
|
20
20
|
return join(nucleoHomeDir(), "skills");
|
|
21
21
|
}
|
|
22
|
+
export const PACKAGE_TARGETS = ["react", "vanilla", "vue"];
|
|
23
|
+
export function isPackageTarget(value) {
|
|
24
|
+
return PACKAGE_TARGETS.includes(value);
|
|
25
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -52,7 +52,7 @@ program
|
|
|
52
52
|
program
|
|
53
53
|
.command("setup")
|
|
54
54
|
.description("Sign in, download your licensed package, configure your editor")
|
|
55
|
-
.option("-t, --target <target>", "react | vanilla")
|
|
55
|
+
.option("-t, --target <target>", "react | vanilla | vue")
|
|
56
56
|
.option("--tools <list>", "comma-separated: cursor,claude,codex (default: prompt, or cursor with --yes)")
|
|
57
57
|
.option("--free", "Install free icon families (no Nucleo login)")
|
|
58
58
|
.option("--from <dir>", "Install from a local prepared package (skip API download)")
|
package/dist/install.d.ts
CHANGED
|
@@ -11,14 +11,23 @@ export declare const FREE_FAMILIES: readonly ["flags", "cursors", "credit-cards"
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function resolveNpmBin(): string;
|
|
13
13
|
export declare function readInstallManifest(root?: string): Promise<InstallManifest | null>;
|
|
14
|
+
/**
|
|
15
|
+
* Replace the original install at the root. Extra targets live in `targets/`
|
|
16
|
+
* and are moved aside so they survive the wipe.
|
|
17
|
+
*/
|
|
14
18
|
export declare function cleanInstallRoot(root?: string): Promise<void>;
|
|
15
|
-
/**
|
|
19
|
+
/** Icon families sitting directly in the original install (not under `targets/`). */
|
|
20
|
+
export declare function rootHasIconFamilies(root?: string): boolean;
|
|
21
|
+
export declare function targetInstallDir(target: PackageTarget, root?: string): string;
|
|
22
|
+
/** True if any Pro family catalog is present at the root or under `targets/`. */
|
|
16
23
|
export declare function hasProFamilies(root?: string): boolean;
|
|
17
|
-
/** True if any free family catalog is present
|
|
24
|
+
/** True if any free family catalog is present at the root or under `targets/`. */
|
|
18
25
|
export declare function hasFreeFamilies(root?: string): boolean;
|
|
19
26
|
/**
|
|
20
|
-
*
|
|
27
|
+
* Original target at the install root, then every other format.
|
|
28
|
+
* A single-target install from an older CLI is just `[manifest.target]`.
|
|
21
29
|
*/
|
|
30
|
+
export declare function listInstalledTargets(manifest: InstallManifest | null, root?: string): PackageTarget[];
|
|
22
31
|
export declare function detectInstalledFamilies(root?: string): string[];
|
|
23
32
|
export declare function resolveInstallTier(root?: string): {
|
|
24
33
|
hasPro: boolean;
|
|
@@ -37,6 +46,11 @@ export interface InstallFromReleaseOptions {
|
|
|
37
46
|
* Default: auto — merge free releases onto an existing Pro install.
|
|
38
47
|
*/
|
|
39
48
|
merge?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* `root` replaces the original install and keeps `targets/`.
|
|
51
|
+
* `nested` adds this format under `targets/<target>/` and leaves the original install in place.
|
|
52
|
+
*/
|
|
53
|
+
placement?: "root" | "nested";
|
|
40
54
|
}
|
|
41
55
|
/**
|
|
42
56
|
* Download all release files (signed URLs) and install into the root.
|
package/dist/install.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { cp, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
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
|
-
import { installRoot, nucleoHomeDir } from "./config.js";
|
|
8
|
+
import { installRoot, isPackageTarget, nucleoHomeDir, } from "./config.js";
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
/** Licensed Pro families — never installed via the free (no-auth) channel. */
|
|
11
11
|
export const PRO_FAMILIES = ["core", "ui", "sharp", "micro", "pixel"];
|
|
@@ -66,24 +66,121 @@ export async function readInstallManifest(root = installRoot()) {
|
|
|
66
66
|
return null;
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
+
function targetsStashPath() {
|
|
70
|
+
return join(nucleoHomeDir(), "targets-stash");
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Replace the original install at the root. Extra targets live in `targets/`
|
|
74
|
+
* and are moved aside so they survive the wipe.
|
|
75
|
+
*/
|
|
69
76
|
export async function cleanInstallRoot(root = installRoot()) {
|
|
77
|
+
const targetsDir = join(root, "targets");
|
|
78
|
+
const stash = targetsStashPath();
|
|
79
|
+
if (!existsSync(targetsDir) && existsSync(stash)) {
|
|
80
|
+
await mkdir(root, { recursive: true });
|
|
81
|
+
await rename(stash, targetsDir);
|
|
82
|
+
}
|
|
83
|
+
let stashed = false;
|
|
84
|
+
if (existsSync(targetsDir)) {
|
|
85
|
+
await rm(stash, { recursive: true, force: true });
|
|
86
|
+
await rename(targetsDir, stash);
|
|
87
|
+
stashed = true;
|
|
88
|
+
}
|
|
70
89
|
await rm(root, { recursive: true, force: true });
|
|
71
90
|
await mkdir(root, { recursive: true });
|
|
91
|
+
if (stashed) {
|
|
92
|
+
await rename(stash, join(root, "targets"));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function directoryHasProFamilies(dir) {
|
|
96
|
+
return PRO_FAMILIES.some((family) => existsSync(join(dir, family, "icons.json")));
|
|
72
97
|
}
|
|
73
|
-
|
|
98
|
+
function directoryHasFreeFamilies(dir) {
|
|
99
|
+
return FREE_FAMILIES.some((family) => existsSync(join(dir, family, "icons.json")));
|
|
100
|
+
}
|
|
101
|
+
/** Icon families sitting directly in the original install (not under `targets/`). */
|
|
102
|
+
export function rootHasIconFamilies(root = installRoot()) {
|
|
103
|
+
return directoryHasProFamilies(root) || directoryHasFreeFamilies(root);
|
|
104
|
+
}
|
|
105
|
+
export function targetInstallDir(target, root = installRoot()) {
|
|
106
|
+
return join(root, "targets", target);
|
|
107
|
+
}
|
|
108
|
+
/** True if any Pro family catalog is present at the root or under `targets/`. */
|
|
74
109
|
export function hasProFamilies(root = installRoot()) {
|
|
75
|
-
|
|
110
|
+
if (directoryHasProFamilies(root)) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
const targetsDir = join(root, "targets");
|
|
114
|
+
if (!existsSync(targetsDir)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
return readdirSync(targetsDir).some((name) => directoryHasProFamilies(join(targetsDir, name)));
|
|
76
118
|
}
|
|
77
|
-
/** True if any free family catalog is present
|
|
119
|
+
/** True if any free family catalog is present at the root or under `targets/`. */
|
|
78
120
|
export function hasFreeFamilies(root = installRoot()) {
|
|
79
|
-
|
|
121
|
+
if (directoryHasFreeFamilies(root)) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
const targetsDir = join(root, "targets");
|
|
125
|
+
if (!existsSync(targetsDir)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return readdirSync(targetsDir).some((name) => directoryHasFreeFamilies(join(targetsDir, name)));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Original target at the install root, then every other format.
|
|
132
|
+
* A single-target install from an older CLI is just `[manifest.target]`.
|
|
133
|
+
*/
|
|
134
|
+
export function listInstalledTargets(manifest, root = installRoot()) {
|
|
135
|
+
const found = new Set();
|
|
136
|
+
if (manifest?.target && isPackageTarget(manifest.target)) {
|
|
137
|
+
found.add(manifest.target);
|
|
138
|
+
}
|
|
139
|
+
if (Array.isArray(manifest?.targets)) {
|
|
140
|
+
for (const entry of manifest.targets) {
|
|
141
|
+
if (typeof entry === "string" && isPackageTarget(entry)) {
|
|
142
|
+
found.add(entry);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const targetsDir = join(root, "targets");
|
|
147
|
+
if (existsSync(targetsDir)) {
|
|
148
|
+
for (const name of readdirSync(targetsDir)) {
|
|
149
|
+
if (!isPackageTarget(name)) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const dir = join(targetsDir, name);
|
|
153
|
+
if (directoryHasProFamilies(dir) || directoryHasFreeFamilies(dir)) {
|
|
154
|
+
found.add(name);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const primary = manifest?.target && isPackageTarget(manifest.target)
|
|
159
|
+
? manifest.target
|
|
160
|
+
: rootHasIconFamilies(root)
|
|
161
|
+
? ([...found][0] ?? null)
|
|
162
|
+
: null;
|
|
163
|
+
const rest = [...found].filter((target) => target !== primary);
|
|
164
|
+
return primary ? [primary, ...rest] : rest;
|
|
80
165
|
}
|
|
81
166
|
/**
|
|
82
167
|
* Detect installed icon families by looking for `{family}/icons.json`.
|
|
83
168
|
*/
|
|
84
|
-
|
|
169
|
+
function familiesInDir(dir) {
|
|
85
170
|
const known = [...PRO_FAMILIES, ...FREE_FAMILIES];
|
|
86
|
-
return known.filter((family) => existsSync(join(
|
|
171
|
+
return known.filter((family) => existsSync(join(dir, family, "icons.json")));
|
|
172
|
+
}
|
|
173
|
+
export function detectInstalledFamilies(root = installRoot()) {
|
|
174
|
+
const found = new Set(familiesInDir(root));
|
|
175
|
+
const targetsDir = join(root, "targets");
|
|
176
|
+
if (existsSync(targetsDir)) {
|
|
177
|
+
for (const name of readdirSync(targetsDir)) {
|
|
178
|
+
for (const family of familiesInDir(join(targetsDir, name))) {
|
|
179
|
+
found.add(family);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return [...found];
|
|
87
184
|
}
|
|
88
185
|
export function resolveInstallTier(root = installRoot()) {
|
|
89
186
|
const hasPro = hasProFamilies(root);
|
|
@@ -180,7 +277,12 @@ export async function installFromRelease(release, target, root = installRoot(),
|
|
|
180
277
|
throw new Error(`Release files missing download urls: ${missingUrl.map((f) => f.id).join(", ")}`);
|
|
181
278
|
}
|
|
182
279
|
const isFreeRelease = release.tier === "free";
|
|
183
|
-
const
|
|
280
|
+
const placement = options.placement ?? "root";
|
|
281
|
+
const destination = placement === "nested" ? targetInstallDir(target, root) : root;
|
|
282
|
+
const previous = await readInstallManifest(root);
|
|
283
|
+
const previousTargets = listInstalledTargets(previous, root);
|
|
284
|
+
const merge = options.merge ??
|
|
285
|
+
(isFreeRelease && existsSync(destination) && directoryHasProFamilies(destination));
|
|
184
286
|
// Safety: free channel must never claim Pro family modules.
|
|
185
287
|
if (isFreeRelease) {
|
|
186
288
|
const forbidden = release.files.filter((f) => PRO_FAMILIES.includes(f.id));
|
|
@@ -221,26 +323,43 @@ export async function installFromRelease(release, target, root = installRoot(),
|
|
|
221
323
|
}
|
|
222
324
|
downloaded.push({ id: file.id, zipPath });
|
|
223
325
|
}
|
|
224
|
-
// 2) Extract —
|
|
225
|
-
console.log(
|
|
226
|
-
|
|
227
|
-
|
|
326
|
+
// 2) Extract — a failed download above does not wipe a previous install
|
|
327
|
+
console.log(placement === "nested"
|
|
328
|
+
? ` Extracting ${target} alongside the existing install…`
|
|
329
|
+
: merge
|
|
330
|
+
? " Merging into existing install (Pro families kept)…"
|
|
331
|
+
: " Extracting…");
|
|
332
|
+
if (placement === "nested") {
|
|
333
|
+
const staging = join(nucleoHomeDir(), "staging");
|
|
334
|
+
await rm(staging, { recursive: true, force: true });
|
|
335
|
+
await mkdir(staging, { recursive: true });
|
|
336
|
+
try {
|
|
337
|
+
for (const file of downloaded) {
|
|
338
|
+
await extractReleaseZip(file.zipPath, file.id, staging);
|
|
339
|
+
}
|
|
340
|
+
if (!merge) {
|
|
341
|
+
await rm(destination, { recursive: true, force: true });
|
|
342
|
+
}
|
|
343
|
+
await mkdir(destination, { recursive: true });
|
|
344
|
+
await mkdir(root, { recursive: true });
|
|
345
|
+
await placeStagedRelease(staging, destination, root);
|
|
346
|
+
}
|
|
347
|
+
finally {
|
|
348
|
+
await rm(staging, { recursive: true, force: true }).catch(() => undefined);
|
|
349
|
+
}
|
|
350
|
+
if (familiesInDir(destination).length === 0) {
|
|
351
|
+
throw new Error(`No icon families were extracted for ${target}.`);
|
|
352
|
+
}
|
|
228
353
|
}
|
|
229
354
|
else {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
for (const file of downloaded) {
|
|
233
|
-
const zipInfo = await stat(file.zipPath);
|
|
234
|
-
process.stdout.write(` ▸ ${file.id} (${formatBytes(zipInfo.size)}) `);
|
|
235
|
-
try {
|
|
236
|
-
await extractZipMerge(file.zipPath, root);
|
|
237
|
-
console.log(` ✓ ${file.id}`);
|
|
355
|
+
if (!merge) {
|
|
356
|
+
await cleanInstallRoot(root);
|
|
238
357
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
358
|
+
else {
|
|
359
|
+
await mkdir(root, { recursive: true });
|
|
360
|
+
}
|
|
361
|
+
for (const file of downloaded) {
|
|
362
|
+
await extractReleaseZip(file.zipPath, file.id, root);
|
|
244
363
|
}
|
|
245
364
|
}
|
|
246
365
|
await assertInstallLooksComplete(root);
|
|
@@ -250,14 +369,55 @@ export async function installFromRelease(release, target, root = installRoot(),
|
|
|
250
369
|
}
|
|
251
370
|
const families = detectInstalledFamilies(root);
|
|
252
371
|
const { tier } = resolveInstallTier(root);
|
|
372
|
+
const primary = placement === "nested" && previous?.target && isPackageTarget(previous.target)
|
|
373
|
+
? previous.target
|
|
374
|
+
: target;
|
|
375
|
+
const targets = [...new Set([primary, ...previousTargets, target])];
|
|
253
376
|
await writeInstallManifest({
|
|
254
377
|
product: tier === "free" ? "nucleo-skills-free" : "nucleo-skills",
|
|
255
378
|
tier,
|
|
256
|
-
target,
|
|
379
|
+
target: primary,
|
|
380
|
+
targets,
|
|
257
381
|
version: release.version,
|
|
258
382
|
families,
|
|
259
383
|
}, root);
|
|
260
384
|
}
|
|
385
|
+
async function extractReleaseZip(zipPath, id, dest) {
|
|
386
|
+
const zipInfo = await stat(zipPath);
|
|
387
|
+
process.stdout.write(` ▸ ${id} (${formatBytes(zipInfo.size)}) `);
|
|
388
|
+
try {
|
|
389
|
+
await extractZipMerge(zipPath, dest);
|
|
390
|
+
console.log(` ✓ ${id}`);
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
394
|
+
throw new Error(`Failed extracting ${id} (${formatBytes(zipInfo.size)}). ${detail}\n` +
|
|
395
|
+
`If this keeps happening, free disk space or try again with:\n` +
|
|
396
|
+
` NODE_OPTIONS=--max-old-space-size=8192 npx @nucleoicons/skills setup …`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/** Copy mcp/skills to the install root and family folders to `familyDest`. Skips manifest.json. */
|
|
400
|
+
async function placeStagedRelease(staging, familyDest, sharedDest) {
|
|
401
|
+
const entries = await readdir(staging, { withFileTypes: true });
|
|
402
|
+
for (const entry of entries) {
|
|
403
|
+
if (entry.name === "manifest.json") {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
const from = join(staging, entry.name);
|
|
407
|
+
if (entry.name === "mcp" || entry.name === "skills") {
|
|
408
|
+
const to = join(sharedDest, entry.name);
|
|
409
|
+
await rm(to, { recursive: true, force: true });
|
|
410
|
+
await cp(from, to, { recursive: true });
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (!entry.isDirectory() || !existsSync(join(from, "icons.json"))) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const to = join(familyDest, entry.name);
|
|
417
|
+
await rm(to, { recursive: true, force: true });
|
|
418
|
+
await cp(from, to, { recursive: true });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
261
421
|
/**
|
|
262
422
|
* Copy a local prepared package (e.g. packages/react) into the install root.
|
|
263
423
|
*/
|