@nikala-ui/cli 0.10.1 → 0.11.0-nightly.63085f3
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/dist/commands/add.d.ts +1 -0
- package/dist/commands/add.js +25 -22
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +5 -0
- package/dist/commands/list.d.ts +12 -0
- package/dist/commands/list.js +106 -0
- package/dist/index.js +23 -3
- package/dist/types/registry.d.ts +3 -3
- package/dist/utils/add/write-component-files.d.ts +1 -1
- package/dist/utils/add/write-component-files.js +9 -1
- package/dist/utils/init/configure-alias.js +14 -7
- package/dist/utils/init/setup-ai-rules.d.ts +4 -0
- package/dist/utils/init/setup-ai-rules.js +92 -0
- package/dist/utils/pkg.js +12 -21
- package/dist/utils/registry.d.ts +9 -7
- package/dist/utils/registry.js +49 -30
- package/dist/utils/theme.js +7 -7
- package/package.json +1 -1
package/dist/commands/add.d.ts
CHANGED
package/dist/commands/add.js
CHANGED
|
@@ -22,14 +22,24 @@ export async function add(components = [], options = {}) {
|
|
|
22
22
|
process.exit(1);
|
|
23
23
|
}
|
|
24
24
|
const isHookMode = Boolean(options.hook);
|
|
25
|
-
const
|
|
25
|
+
const isBlockMode = Boolean(options.block);
|
|
26
|
+
let filteredRegistry = registryIndex;
|
|
27
|
+
if (isHookMode) {
|
|
28
|
+
filteredRegistry = registryIndex.filter((item) => item.type === "registry:hook");
|
|
29
|
+
}
|
|
30
|
+
else if (isBlockMode) {
|
|
31
|
+
filteredRegistry = registryIndex.filter((item) => item.type === "registry:block");
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
filteredRegistry = registryIndex.filter((item) => item.type !== "registry:hook");
|
|
35
|
+
}
|
|
26
36
|
const isAllRequested = options.all || components.includes("all");
|
|
27
37
|
let requestedComponents = components.filter((c) => c !== "all");
|
|
28
38
|
if (isAllRequested) {
|
|
29
39
|
requestedComponents = filteredRegistry.map((item) => item.name);
|
|
30
40
|
}
|
|
31
41
|
else if (requestedComponents.length === 0) {
|
|
32
|
-
const itemLabel = isHookMode ? "hooks" : "components";
|
|
42
|
+
const itemLabel = isHookMode ? "hooks" : isBlockMode ? "blocks" : "components";
|
|
33
43
|
const response = await prompts({
|
|
34
44
|
type: "autocompleteMultiselect",
|
|
35
45
|
name: "selectedComponents",
|
|
@@ -47,15 +57,12 @@ export async function add(components = [], options = {}) {
|
|
|
47
57
|
}
|
|
48
58
|
requestedComponents = response.selectedComponents;
|
|
49
59
|
}
|
|
50
|
-
const resolvedTargets = await resolveRegistryDependencies(requestedComponents);
|
|
60
|
+
const resolvedTargets = await resolveRegistryDependencies(requestedComponents, config.registries);
|
|
51
61
|
const componentsDir = path.resolve(cwd, config.alias.components);
|
|
52
|
-
console.log(pc.cyan(`\n🎨 Adding
|
|
62
|
+
console.log(pc.cyan(`\n🎨 Adding items to project...\n`));
|
|
53
63
|
const requiredNpmDeps = new Set();
|
|
54
|
-
if (isHookMode) {
|
|
55
|
-
requiredNpmDeps.add("@nikala-ui/hooks");
|
|
56
|
-
}
|
|
57
64
|
for (const target of resolvedTargets) {
|
|
58
|
-
const item = await getRegistryItem(target);
|
|
65
|
+
const item = await getRegistryItem(target, config.registries);
|
|
59
66
|
if (!item) {
|
|
60
67
|
const availableStr = registryIndex ? registryIndex.map((i) => i.name).join(", ") : "none";
|
|
61
68
|
console.log(pc.red(`❌ "${target}" not found in registry. Available: ${availableStr}`));
|
|
@@ -70,26 +77,22 @@ export async function add(components = [], options = {}) {
|
|
|
70
77
|
}
|
|
71
78
|
// Inspect user package.json and install missing NPM packages
|
|
72
79
|
const userPkgPath = path.join(cwd, "package.json");
|
|
73
|
-
const missingNpmDeps = [];
|
|
74
80
|
if (await fs.pathExists(userPkgPath)) {
|
|
75
81
|
try {
|
|
76
82
|
const userPkg = await fs.readJson(userPkgPath);
|
|
77
|
-
const installedDeps = {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
const installedDeps = {
|
|
84
|
+
...userPkg.dependencies,
|
|
85
|
+
...userPkg.devDependencies,
|
|
86
|
+
};
|
|
87
|
+
const missingDeps = Array.from(requiredNpmDeps).filter((dep) => !installedDeps[dep]);
|
|
88
|
+
if (missingDeps.length > 0) {
|
|
89
|
+
console.log(pc.cyan(`\n📦 Installing missing dependencies: ${missingDeps.join(", ")}...\n`));
|
|
90
|
+
await installDependencies(missingDeps, cwd);
|
|
82
91
|
}
|
|
83
92
|
}
|
|
84
93
|
catch {
|
|
85
|
-
|
|
94
|
+
// Gracefully continue
|
|
86
95
|
}
|
|
87
96
|
}
|
|
88
|
-
|
|
89
|
-
missingNpmDeps.push(...Array.from(requiredNpmDeps));
|
|
90
|
-
}
|
|
91
|
-
if (missingNpmDeps.length > 0) {
|
|
92
|
-
await installDependencies(missingNpmDeps, cwd);
|
|
93
|
-
}
|
|
94
|
-
console.log(pc.cyan("\n✅ Components successfully added!"));
|
|
97
|
+
console.log(pc.green(`\n✨ Successfully added ${resolvedTargets.length} item(s)!`));
|
|
95
98
|
}
|
package/dist/commands/init.d.ts
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -7,6 +7,7 @@ import { writeConfig } from "../utils/file.js";
|
|
|
7
7
|
import { installDependencies } from "../utils/pkg.js";
|
|
8
8
|
import { configureAliases } from "../utils/init/configure-alias.js";
|
|
9
9
|
import { setupCssTheme } from "../utils/init/setup-css.js";
|
|
10
|
+
import { setupAiRules } from "../utils/init/setup-ai-rules.js";
|
|
10
11
|
/**
|
|
11
12
|
* Initializes Nikala UI workspace configuration and sets up design tokens.
|
|
12
13
|
*/
|
|
@@ -124,5 +125,9 @@ export async function init(options) {
|
|
|
124
125
|
},
|
|
125
126
|
});
|
|
126
127
|
console.log(pc.green("✓ Created nikala.config.json"));
|
|
128
|
+
// 6. Setup AI assistant rules if --ai flag is passed
|
|
129
|
+
if (options.ai) {
|
|
130
|
+
await setupAiRules(cwd);
|
|
131
|
+
}
|
|
127
132
|
console.log(pc.green("\n✅ Nikala UI initialized successfully with custom theme!"));
|
|
128
133
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface ListOptions {
|
|
2
|
+
installed?: boolean;
|
|
3
|
+
hook?: boolean;
|
|
4
|
+
component?: boolean;
|
|
5
|
+
block?: boolean;
|
|
6
|
+
json?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Command handler to list all available and locally installed Nikala UI components, blocks, and hooks.
|
|
10
|
+
*/
|
|
11
|
+
export declare function listCommand(options?: ListOptions): Promise<void>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { readConfig } from "../utils/file.js";
|
|
5
|
+
import { getRegistryIndex } from "../utils/registry.js";
|
|
6
|
+
/**
|
|
7
|
+
* Command handler to list all available and locally installed Nikala UI components, blocks, and hooks.
|
|
8
|
+
*/
|
|
9
|
+
export async function listCommand(options = {}) {
|
|
10
|
+
const cwd = process.cwd();
|
|
11
|
+
const config = await readConfig(cwd);
|
|
12
|
+
const registryIndex = await getRegistryIndex();
|
|
13
|
+
if (!registryIndex) {
|
|
14
|
+
console.log(pc.red("❌ Failed to fetch registry index. Ensure network connection."));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const componentsDir = config ? path.resolve(cwd, config.alias.components) : path.join(cwd, "src/components/ui");
|
|
18
|
+
const blocksDir = path.resolve(path.dirname(componentsDir), "blocks");
|
|
19
|
+
const hooksDir = config && config.alias.hooks ? path.resolve(cwd, config.alias.hooks) : path.join(cwd, "src/hooks");
|
|
20
|
+
// Inspect status for all items
|
|
21
|
+
const items = await Promise.all(registryIndex.map(async (item) => {
|
|
22
|
+
const isHook = item.type === "registry:hook";
|
|
23
|
+
const isBlock = item.type === "registry:block";
|
|
24
|
+
const targetDir = isHook ? hooksDir : isBlock ? blocksDir : componentsDir;
|
|
25
|
+
const tsxPath = path.join(targetDir, `${item.name}.tsx`);
|
|
26
|
+
const tsPath = path.join(targetDir, `${item.name}.ts`);
|
|
27
|
+
const isInstalled = (await fs.pathExists(tsxPath)) || (await fs.pathExists(tsPath));
|
|
28
|
+
return {
|
|
29
|
+
...item,
|
|
30
|
+
isInstalled,
|
|
31
|
+
installedPath: isInstalled ? (await fs.pathExists(tsxPath) ? tsxPath : tsPath) : null,
|
|
32
|
+
};
|
|
33
|
+
}));
|
|
34
|
+
// Apply filters
|
|
35
|
+
let filtered = items;
|
|
36
|
+
if (options.installed) {
|
|
37
|
+
filtered = filtered.filter((i) => i.isInstalled);
|
|
38
|
+
}
|
|
39
|
+
if (options.hook) {
|
|
40
|
+
filtered = filtered.filter((i) => i.type === "registry:hook");
|
|
41
|
+
}
|
|
42
|
+
if (options.block) {
|
|
43
|
+
filtered = filtered.filter((i) => i.type === "registry:block");
|
|
44
|
+
}
|
|
45
|
+
if (options.component) {
|
|
46
|
+
filtered = filtered.filter((i) => i.type === "registry:ui");
|
|
47
|
+
}
|
|
48
|
+
// JSON Output mode
|
|
49
|
+
if (options.json) {
|
|
50
|
+
console.log(JSON.stringify(filtered, null, 2));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const uiComponents = filtered.filter((i) => i.type === "registry:ui");
|
|
54
|
+
const blocks = filtered.filter((i) => i.type === "registry:block");
|
|
55
|
+
const hooks = filtered.filter((i) => i.type === "registry:hook");
|
|
56
|
+
const installedCount = items.filter((i) => i.isInstalled).length;
|
|
57
|
+
const installedCompCount = items.filter((i) => i.isInstalled && i.type === "registry:ui").length;
|
|
58
|
+
const installedBlockCount = items.filter((i) => i.isInstalled && i.type === "registry:block").length;
|
|
59
|
+
const installedHookCount = items.filter((i) => i.isInstalled && i.type === "registry:hook").length;
|
|
60
|
+
console.log(`\n📋 ${pc.bold("Nikala UI Registry Catalog")}\n`);
|
|
61
|
+
// 1. UI Components section
|
|
62
|
+
if (!options.hook && !options.block && uiComponents.length > 0) {
|
|
63
|
+
console.log(pc.bold(pc.cyan(`📦 UI Components (${uiComponents.length}):`)));
|
|
64
|
+
for (const comp of uiComponents) {
|
|
65
|
+
const status = comp.isInstalled
|
|
66
|
+
? pc.green("✓ Installed")
|
|
67
|
+
: pc.dim("+ Available");
|
|
68
|
+
const name = comp.isInstalled ? pc.bold(pc.white(comp.name)) : pc.white(comp.name);
|
|
69
|
+
const desc = comp.description ? pc.dim(` — ${comp.description}`) : "";
|
|
70
|
+
console.log(` ${status} ${name.padEnd(20)} ${desc}`);
|
|
71
|
+
}
|
|
72
|
+
console.log("");
|
|
73
|
+
}
|
|
74
|
+
// 2. Marketing & App Blocks section
|
|
75
|
+
if (!options.hook && !options.component && blocks.length > 0) {
|
|
76
|
+
console.log(pc.bold(pc.yellow(`🧱 Blocks (${blocks.length}):`)));
|
|
77
|
+
for (const block of blocks) {
|
|
78
|
+
const status = block.isInstalled
|
|
79
|
+
? pc.green("✓ Installed")
|
|
80
|
+
: pc.dim("+ Available");
|
|
81
|
+
const name = block.isInstalled ? pc.bold(pc.white(block.name)) : pc.white(block.name);
|
|
82
|
+
const desc = block.description ? pc.dim(` — ${block.description}`) : "";
|
|
83
|
+
console.log(` ${status} ${name.padEnd(20)} ${desc}`);
|
|
84
|
+
}
|
|
85
|
+
console.log("");
|
|
86
|
+
}
|
|
87
|
+
// 3. Hooks section
|
|
88
|
+
if (!options.component && !options.block && hooks.length > 0) {
|
|
89
|
+
console.log(pc.bold(pc.magenta(`⚡ Reactive Primitives / Hooks (${hooks.length}):`)));
|
|
90
|
+
for (const hook of hooks) {
|
|
91
|
+
const status = hook.isInstalled
|
|
92
|
+
? pc.green("✓ Installed")
|
|
93
|
+
: pc.dim("+ Available");
|
|
94
|
+
const name = hook.isInstalled ? pc.bold(pc.white(hook.name)) : pc.white(hook.name);
|
|
95
|
+
const desc = hook.description ? pc.dim(` — ${hook.description}`) : "";
|
|
96
|
+
console.log(` ${status} ${name.padEnd(28)} ${desc}`);
|
|
97
|
+
}
|
|
98
|
+
console.log("");
|
|
99
|
+
}
|
|
100
|
+
// Footer summary
|
|
101
|
+
console.log(pc.dim(`────────────────────────────────────────────────────────────────────────────`));
|
|
102
|
+
console.log(`Summary: ${pc.green(`${installedCount} installed`)} (${installedCompCount} UI, ${installedBlockCount} blocks, ${installedHookCount} hooks) · ${pc.cyan(`${registryIndex.length} total in registry`)}`);
|
|
103
|
+
if (!config) {
|
|
104
|
+
console.log(pc.yellow(`\n💡 Tip: Run \`nikala init\` in this directory to initialize Nikala UI configuration.`));
|
|
105
|
+
}
|
|
106
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -8,24 +8,33 @@ import { validateCommand } from "./commands/validate.js";
|
|
|
8
8
|
import { diffCommand } from "./commands/diff.js";
|
|
9
9
|
import { upgradeCommand } from "./commands/upgrade.js";
|
|
10
10
|
import { removeCommand } from "./commands/remove.js";
|
|
11
|
-
|
|
11
|
+
import { listCommand } from "./commands/list.js";
|
|
12
|
+
import { setupAiRules } from "./utils/init/setup-ai-rules.js";
|
|
13
|
+
console.log(`\n🎨 ${pc.bold(pc.red("Nikala UI"))} ${pc.dim("v0.11.0")} — SolidJS + Tailwind v4 components`);
|
|
12
14
|
console.log(` ${pc.italic(pc.dim("Honoring Niko Pirosmani (Nikala)"))}\n`);
|
|
13
15
|
console.log(` ${pc.dim("Docs:")} ${pc.underline(pc.cyan("https://nikala.dev"))}\n`);
|
|
14
16
|
const program = new Command();
|
|
15
17
|
program
|
|
16
18
|
.name("nikala")
|
|
17
19
|
.description("Nikala UI — SolidJS + Tailwind v4 components")
|
|
18
|
-
.version("0.
|
|
20
|
+
.version("0.11.0");
|
|
19
21
|
program
|
|
20
22
|
.command("init")
|
|
21
23
|
.description("Initialize Nikala UI in your project")
|
|
22
24
|
.option("-d, --defaults", "Skip prompts and use defaults")
|
|
25
|
+
.option("--ai", "Generate AI assistant rules (.cursor/rules/nikala.mdc, .cursorrules, AGENTS.md)")
|
|
23
26
|
.action(init);
|
|
27
|
+
program
|
|
28
|
+
.command("rules")
|
|
29
|
+
.alias("ai")
|
|
30
|
+
.description("Generate or update AI assistant rules (.cursor/rules/nikala.mdc, .cursorrules, AGENTS.md)")
|
|
31
|
+
.action(() => setupAiRules(process.cwd()));
|
|
24
32
|
program
|
|
25
33
|
.command("add [components...]")
|
|
26
|
-
.description("Add components or reactive hooks to your project")
|
|
34
|
+
.description("Add components, blocks, or reactive hooks to your project")
|
|
27
35
|
.option("-o, --overwrite", "Overwrite existing files")
|
|
28
36
|
.option("--all", "Add all available items")
|
|
37
|
+
.option("-b, --block", "Add marketing or app block section(s)")
|
|
29
38
|
.option("-h, --hook", "Add reactive hook primitive(s) instead of UI components")
|
|
30
39
|
.action(add);
|
|
31
40
|
// Upgrade / Update command
|
|
@@ -65,4 +74,15 @@ program
|
|
|
65
74
|
.command("diff [component]")
|
|
66
75
|
.description("Compare local component files against latest registry manifests and view differences")
|
|
67
76
|
.action((component) => diffCommand(component));
|
|
77
|
+
// List / Catalog command
|
|
78
|
+
program
|
|
79
|
+
.command("list")
|
|
80
|
+
.alias("ls")
|
|
81
|
+
.description("List available and locally installed Nikala UI components, blocks, and reactive hooks")
|
|
82
|
+
.option("-i, --installed", "Show only locally installed items")
|
|
83
|
+
.option("-c, --component", "Show only UI components")
|
|
84
|
+
.option("-b, --block", "Show only blocks")
|
|
85
|
+
.option("-h, --hook", "Show only reactive hooks")
|
|
86
|
+
.option("--json", "Output results in JSON format")
|
|
87
|
+
.action((options) => listCommand(options));
|
|
68
88
|
program.parse();
|
package/dist/types/registry.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
export interface RegistryFile {
|
|
2
2
|
path: string;
|
|
3
3
|
content: string;
|
|
4
|
-
type: "registry:ui" | "registry:util" | "registry:hook";
|
|
4
|
+
type: "registry:ui" | "registry:util" | "registry:hook" | "registry:block";
|
|
5
5
|
}
|
|
6
6
|
export interface RegistryItem {
|
|
7
7
|
name: string;
|
|
8
8
|
title: string;
|
|
9
9
|
description: string;
|
|
10
|
-
type: "registry:ui" | "registry:util" | "registry:hook";
|
|
10
|
+
type: "registry:ui" | "registry:util" | "registry:hook" | "registry:block";
|
|
11
11
|
dependencies?: string[];
|
|
12
12
|
registryDependencies?: string[];
|
|
13
13
|
files: RegistryFile[];
|
|
@@ -16,7 +16,7 @@ export interface RegistryIndexItem {
|
|
|
16
16
|
name: string;
|
|
17
17
|
title: string;
|
|
18
18
|
description: string;
|
|
19
|
-
type: "registry:ui" | "registry:util" | "registry:hook";
|
|
19
|
+
type: "registry:ui" | "registry:util" | "registry:hook" | "registry:block";
|
|
20
20
|
dependencies?: string[];
|
|
21
21
|
registryDependencies?: string[];
|
|
22
22
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RegistryItem } from "../../types/registry.js";
|
|
2
2
|
/**
|
|
3
|
-
* Writes registry component files to target workspace directories (ui vs
|
|
3
|
+
* Writes registry component files to target workspace directories (ui vs blocks vs hooks).
|
|
4
4
|
*
|
|
5
5
|
* @param cwd - Working directory path of the target project
|
|
6
6
|
* @param item - Registry item manifest containing files
|
|
@@ -2,7 +2,7 @@ import fs from "fs-extra";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import pc from "picocolors";
|
|
4
4
|
/**
|
|
5
|
-
* Writes registry component files to target workspace directories (ui vs
|
|
5
|
+
* Writes registry component files to target workspace directories (ui vs blocks vs hooks).
|
|
6
6
|
*
|
|
7
7
|
* @param cwd - Working directory path of the target project
|
|
8
8
|
* @param item - Registry item manifest containing files
|
|
@@ -16,6 +16,14 @@ export async function writeComponentFiles(cwd, item, componentsDir, overwrite =
|
|
|
16
16
|
const relativePath = file.path.replace(/^ui\//, "");
|
|
17
17
|
targetFilePath = path.join(componentsDir, relativePath);
|
|
18
18
|
}
|
|
19
|
+
else if (file.path.startsWith("blocks/") || file.type === "registry:block") {
|
|
20
|
+
const relativePath = file.path.startsWith("blocks/")
|
|
21
|
+
? file.path.replace(/^blocks\//, "")
|
|
22
|
+
: file.path;
|
|
23
|
+
// Default blocks destination is src/components/blocks
|
|
24
|
+
const blocksDir = path.resolve(path.dirname(componentsDir), "blocks");
|
|
25
|
+
targetFilePath = path.join(blocksDir, relativePath);
|
|
26
|
+
}
|
|
19
27
|
else if (file.path.startsWith("hooks/")) {
|
|
20
28
|
const relativePath = file.path.replace(/^hooks\//, "");
|
|
21
29
|
const hooksDir = path.resolve(cwd, "src/hooks");
|
|
@@ -12,36 +12,43 @@ export async function configureAliases(cwd) {
|
|
|
12
12
|
if (await fs.pathExists(targetConfigPath)) {
|
|
13
13
|
let configContent = await fs.readFile(targetConfigPath, "utf-8");
|
|
14
14
|
let modified = false;
|
|
15
|
+
// 1. Inject Tailwind CSS v4 Vite plugin if missing
|
|
15
16
|
if (!configContent.includes("@tailwindcss/vite")) {
|
|
16
17
|
configContent = `import tailwindcss from "@tailwindcss/vite";\n${configContent}`;
|
|
17
18
|
if (configContent.includes("plugins: [")) {
|
|
18
19
|
configContent = configContent.replace("plugins: [", "plugins: [\n tailwindcss(), ");
|
|
20
|
+
modified = true;
|
|
19
21
|
}
|
|
20
|
-
else
|
|
21
|
-
|
|
22
|
+
else {
|
|
23
|
+
const defineConfigRegex = /(defineConfig\s*\(\s*(?:async\s*)?(?:\([^)]*\)\s*=>\s*)?\{)/;
|
|
24
|
+
if (defineConfigRegex.test(configContent)) {
|
|
25
|
+
configContent = configContent.replace(defineConfigRegex, "$1\n plugins: [tailwindcss()],");
|
|
26
|
+
modified = true;
|
|
27
|
+
}
|
|
22
28
|
}
|
|
23
|
-
modified = true;
|
|
24
29
|
}
|
|
30
|
+
// 2. Inject path alias (@ -> ./src)
|
|
25
31
|
if (!configContent.includes('"@"') && !configContent.includes("'@'")) {
|
|
26
32
|
if (!configContent.includes('import path from "node:path"') && !configContent.includes('import path from "path"')) {
|
|
27
33
|
configContent = `import path from "node:path";\n${configContent}`;
|
|
28
34
|
}
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
const defineConfigRegex = /(defineConfig\s*\(\s*(?:async\s*)?(?:\([^)]*\)\s*=>\s*)?\{)/;
|
|
36
|
+
if (defineConfigRegex.test(configContent)) {
|
|
37
|
+
configContent = configContent.replace(defineConfigRegex, `$1\n resolve: {\n alias: {\n "@": path.resolve(process.cwd(), "./src"),\n },\n },`);
|
|
38
|
+
modified = true;
|
|
31
39
|
}
|
|
32
|
-
modified = true;
|
|
33
40
|
}
|
|
34
41
|
if (modified) {
|
|
35
42
|
await fs.writeFile(targetConfigPath, configContent, "utf-8");
|
|
36
43
|
console.log(pc.green(`✓ Configured Tailwind CSS v4 plugin and path alias in ${path.basename(targetConfigPath)}`));
|
|
37
44
|
}
|
|
38
45
|
}
|
|
46
|
+
// 3. Configure tsconfig.json path mappings (@/* -> ./src/*)
|
|
39
47
|
const tsconfigPath = path.join(cwd, "tsconfig.json");
|
|
40
48
|
if (await fs.pathExists(tsconfigPath)) {
|
|
41
49
|
const tsconfig = await readTsConfig(cwd);
|
|
42
50
|
if (tsconfig) {
|
|
43
51
|
tsconfig.compilerOptions = tsconfig.compilerOptions || {};
|
|
44
|
-
tsconfig.compilerOptions.baseUrl = ".";
|
|
45
52
|
tsconfig.compilerOptions.paths = tsconfig.compilerOptions.paths || {};
|
|
46
53
|
// Correct path mapping with leading relative dot ./src/*
|
|
47
54
|
tsconfig.compilerOptions.paths["@/*"] = ["./src/*"];
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
const cursorRuleContent = `---
|
|
5
|
+
description: Nikala UI & SolidJS Reactivity Development Rules
|
|
6
|
+
globs: **/*.{ts,tsx,js,jsx}
|
|
7
|
+
alwaysApply: true
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Nikala UI & SolidJS Engineering Guidelines
|
|
11
|
+
|
|
12
|
+
> Nikala UI is a copy-paste component system and reactive primitives suite for SolidJS built natively for Tailwind CSS v4.
|
|
13
|
+
|
|
14
|
+
## 1. Strict SolidJS Reactivity Rules
|
|
15
|
+
|
|
16
|
+
1. **NEVER Destructure Props Directly**:
|
|
17
|
+
- \`const { variant, class: className } = props;\` -> ❌ **FORBIDDEN** (breaks SolidJS fine-grained signal tracking).
|
|
18
|
+
- \`const [local, others] = splitProps(props, ["variant", "class"]);\` -> ✅ **REQUIRED**.
|
|
19
|
+
|
|
20
|
+
2. **Children Inspection & Tab Hydration**:
|
|
21
|
+
- ALWAYS wrap \`props.children\` with SolidJS's native \`children(() => props.children)\` memoization helper when inspecting, iterating, or rendering dynamic JSX child nodes inside containers or conditional branches.
|
|
22
|
+
|
|
23
|
+
3. **SSR Safety Guards**:
|
|
24
|
+
- Always include \`typeof window !== "undefined"\` and \`typeof document !== "undefined"\` guards inside browser event listeners or DOM access logic to prevent SSR hydration crashes in SolidStart environments.
|
|
25
|
+
|
|
26
|
+
4. **Anti-FOUC Theme Script**:
|
|
27
|
+
- When using \`ThemeProvider\`, ALWAYS place \`<ThemeScript storageKey="nikala-theme" />\` synchronously inside \`<head>\` or root HTML before \`<ThemeProvider>\` to eliminate flash of unstyled content during SSR.
|
|
28
|
+
|
|
29
|
+
## 2. Tailwind CSS v4 Native Design Tokens
|
|
30
|
+
|
|
31
|
+
- All components must use semantic design tokens defined in modern \`@import "tailwindcss";\` setups (\`bg-background\`, \`text-foreground\`, \`bg-card\`, \`border-border\`, \`bg-primary\`, etc.).
|
|
32
|
+
- Avoid hardcoded arbitrary color values; prefer semantic design tokens.
|
|
33
|
+
|
|
34
|
+
## 3. Pure Copy-Paste Primitives Ownership
|
|
35
|
+
|
|
36
|
+
- UI components are placed in \`src/components/ui/\` (or configured alias).
|
|
37
|
+
- Reactive hooks are placed in \`src/hooks/\` and imported locally:
|
|
38
|
+
\`import { createClipboard } from "@/hooks/create-clipboard";\`
|
|
39
|
+
`;
|
|
40
|
+
const rootAgentsRuleContent = `# Nikala UI & SolidJS Development Guidelines
|
|
41
|
+
|
|
42
|
+
> Nikala UI is a copy-paste component system and reactive primitives suite for SolidJS built natively for Tailwind CSS v4.
|
|
43
|
+
|
|
44
|
+
## 1. Strict SolidJS Reactivity Rules
|
|
45
|
+
|
|
46
|
+
1. **NEVER Destructure Props Directly**:
|
|
47
|
+
- \`const { variant, class: className } = props;\` -> ❌ **FORBIDDEN** (breaks SolidJS fine-grained signal tracking).
|
|
48
|
+
- \`const [local, others] = splitProps(props, ["variant", "class"]);\` -> ✅ **REQUIRED**.
|
|
49
|
+
|
|
50
|
+
2. **Children Inspection & Tab Hydration**:
|
|
51
|
+
- ALWAYS wrap \`props.children\` with SolidJS's native \`children(() => props.children)\` memoization helper when inspecting, iterating, or rendering dynamic JSX child nodes.
|
|
52
|
+
|
|
53
|
+
3. **SSR Safety Guards**:
|
|
54
|
+
- Always include \`typeof window !== "undefined"\` and \`typeof document !== "undefined"\` guards inside browser event listeners or DOM access logic.
|
|
55
|
+
|
|
56
|
+
4. **Anti-FOUC Theme Script**:
|
|
57
|
+
- When using \`ThemeProvider\`, ALWAYS place \`<ThemeScript storageKey="nikala-theme" />\` synchronously inside \`<head>\` or root HTML before \`<ThemeProvider>\`.
|
|
58
|
+
|
|
59
|
+
## 2. Tailwind CSS v4 Native Design Tokens
|
|
60
|
+
|
|
61
|
+
- All components must use semantic design tokens (\`bg-background\`, \`text-foreground\`, \`bg-card\`, \`border-border\`, \`bg-primary\`, etc.).
|
|
62
|
+
|
|
63
|
+
## 3. Pure Copy-Paste Primitives Ownership
|
|
64
|
+
|
|
65
|
+
- UI components live in \`src/components/ui/\`.
|
|
66
|
+
- Reactive hooks live in \`src/hooks/\` and are imported locally:
|
|
67
|
+
\`import { createClipboard } from "@/hooks/create-clipboard";\`
|
|
68
|
+
`;
|
|
69
|
+
/**
|
|
70
|
+
* Sets up AI rules (.cursor/rules/nikala.mdc, .cursorrules, AGENTS.md) in the target workspace.
|
|
71
|
+
*/
|
|
72
|
+
export async function setupAiRules(cwd) {
|
|
73
|
+
try {
|
|
74
|
+
// 1. .cursor/rules/nikala.mdc
|
|
75
|
+
const cursorRulesDir = path.join(cwd, ".cursor", "rules");
|
|
76
|
+
await fs.ensureDir(cursorRulesDir);
|
|
77
|
+
const mdcPath = path.join(cursorRulesDir, "nikala.mdc");
|
|
78
|
+
await fs.writeFile(mdcPath, cursorRuleContent.trim() + "\n", "utf-8");
|
|
79
|
+
// 2. .cursorrules (legacy Cursor format)
|
|
80
|
+
const cursorRulesPath = path.join(cwd, ".cursorrules");
|
|
81
|
+
await fs.writeFile(cursorRulesPath, rootAgentsRuleContent.trim() + "\n", "utf-8");
|
|
82
|
+
// 3. AGENTS.md / CLAUDE.md
|
|
83
|
+
const agentsPath = path.join(cwd, "AGENTS.md");
|
|
84
|
+
if (!(await fs.pathExists(agentsPath))) {
|
|
85
|
+
await fs.writeFile(agentsPath, rootAgentsRuleContent.trim() + "\n", "utf-8");
|
|
86
|
+
}
|
|
87
|
+
console.log(pc.green("✓ Generated AI assistant rules (.cursor/rules/nikala.mdc, .cursorrules, AGENTS.md)"));
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
console.log(pc.yellow(`⚠️ Failed to write AI assistant rules: ${error}`));
|
|
91
|
+
}
|
|
92
|
+
}
|
package/dist/utils/pkg.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import fs from "fs-extra";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
4
|
import pc from "picocolors";
|
|
5
5
|
import stripJsonComments from "strip-json-comments";
|
|
6
|
+
// Standard npm package name regex validator (supports scoped packages and version specifiers)
|
|
7
|
+
const NPM_PACKAGE_REGEX = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*(@[a-zA-Z0-9^~.*><=-]+)?$/;
|
|
6
8
|
/**
|
|
7
9
|
* Detects the package manager used in the target project workspace by checking lockfiles.
|
|
8
10
|
*
|
|
@@ -47,27 +49,16 @@ export async function installDependencies(dependencies, cwd = process.cwd()) {
|
|
|
47
49
|
// Failed to parse original package.json
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
case "pnpm":
|
|
57
|
-
command = `pnpm add ${depsString}`;
|
|
58
|
-
break;
|
|
59
|
-
case "yarn":
|
|
60
|
-
command = `yarn add ${depsString}`;
|
|
61
|
-
break;
|
|
62
|
-
case "npm":
|
|
63
|
-
default:
|
|
64
|
-
command = `npm install ${depsString}`;
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
52
|
+
// Validate and sanitize dependency names to prevent any shell metacharacter injection
|
|
53
|
+
const validDeps = dependencies.filter((dep) => NPM_PACKAGE_REGEX.test(dep));
|
|
54
|
+
if (validDeps.length === 0)
|
|
55
|
+
return;
|
|
56
|
+
const subCommand = pkgManager === "npm" ? "install" : "add";
|
|
57
|
+
const args = [subCommand, ...validDeps];
|
|
67
58
|
console.log(pc.yellow(`\n📦 Installing required component dependencies (${pkgManager})...`));
|
|
68
|
-
console.log(pc.white(` ${
|
|
59
|
+
console.log(pc.white(` ${pkgManager} ${args.join(" ")}\n`));
|
|
69
60
|
try {
|
|
70
|
-
|
|
61
|
+
execFileSync(pkgManager, args, { cwd, stdio: "inherit" });
|
|
71
62
|
// 2. Validate package.json integrity after installation and restore stripped fields if necessary
|
|
72
63
|
if (originalPkgJson && (await fs.pathExists(pkgPath))) {
|
|
73
64
|
try {
|
|
@@ -97,6 +88,6 @@ export async function installDependencies(dependencies, cwd = process.cwd()) {
|
|
|
97
88
|
}
|
|
98
89
|
catch (error) {
|
|
99
90
|
console.log(pc.red(`❌ Failed to install dependencies automatically.`));
|
|
100
|
-
console.log(pc.yellow(` Please run manually: ${
|
|
91
|
+
console.log(pc.yellow(` Please run manually: ${pkgManager} ${args.join(" ")}`));
|
|
101
92
|
}
|
|
102
93
|
}
|
package/dist/utils/registry.d.ts
CHANGED
|
@@ -3,23 +3,25 @@ import type { RegistryIndex, RegistryItem } from "../types/registry.js";
|
|
|
3
3
|
export declare const OFFICIAL_REGISTRY_URL = "https://raw.githubusercontent.com/nikala-ui/ui/main/packages/core/registry";
|
|
4
4
|
/**
|
|
5
5
|
* Reads and parses the central registry index manifest.
|
|
6
|
-
*
|
|
6
|
+
* Prioritizes local monorepo registry during development, and falls back to remote CDN for end-users.
|
|
7
7
|
*
|
|
8
|
+
* @param customRegistryUrl - Optional custom registry base URL to fetch index from
|
|
8
9
|
* @returns The list of available registry items or null if not found.
|
|
9
10
|
*/
|
|
10
|
-
export declare function getRegistryIndex(): Promise<RegistryIndex | null>;
|
|
11
|
+
export declare function getRegistryIndex(customRegistryUrl?: string): Promise<RegistryIndex | null>;
|
|
11
12
|
/**
|
|
12
13
|
* Fetches a component manifest from a remote HTTP(S) URL with cache-busting.
|
|
13
14
|
*/
|
|
14
15
|
export declare function fetchRemoteRegistryItem(url: string): Promise<RegistryItem | null>;
|
|
15
16
|
/**
|
|
16
|
-
* Fetches the manifest for a component by name or remote URL.
|
|
17
|
-
* Checks
|
|
17
|
+
* Fetches the manifest for a component by name, namespace, or remote URL.
|
|
18
|
+
* Checks local package manifest first (for monorepo dev & offline), then custom registries from config, and finally official remote GitHub CDN.
|
|
18
19
|
*
|
|
19
|
-
* @param nameOrUrl - Component identifier
|
|
20
|
+
* @param nameOrUrl - Component identifier (e.g. "button", "@acme/hero-01", "https://...")
|
|
21
|
+
* @param customRegistries - Optional dictionary of custom namespace registries from nikala.config.json
|
|
20
22
|
*/
|
|
21
|
-
export declare function getRegistryItem(nameOrUrl: string): Promise<RegistryItem | null>;
|
|
23
|
+
export declare function getRegistryItem(nameOrUrl: string, customRegistries?: Record<string, string>): Promise<RegistryItem | null>;
|
|
22
24
|
/**
|
|
23
25
|
* Recursively resolves all required internal registry dependencies for a set of component names or URLs.
|
|
24
26
|
*/
|
|
25
|
-
export declare function resolveRegistryDependencies(namesOrUrls: string[]): Promise<string[]>;
|
|
27
|
+
export declare function resolveRegistryDependencies(namesOrUrls: string[], customRegistries?: Record<string, string>): Promise<string[]>;
|
package/dist/utils/registry.js
CHANGED
|
@@ -9,19 +9,40 @@ export const OFFICIAL_REGISTRY_URL = "https://raw.githubusercontent.com/nikala-u
|
|
|
9
9
|
function getLocalRegistryDirectory() {
|
|
10
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
11
|
const __dirname = path.dirname(__filename);
|
|
12
|
+
// 1. Monorepo development path (packages/core/registry)
|
|
13
|
+
const monorepoCorePath = path.resolve(__dirname, "../../../core/registry");
|
|
14
|
+
if (fs.existsSync(monorepoCorePath)) {
|
|
15
|
+
return monorepoCorePath;
|
|
16
|
+
}
|
|
12
17
|
return path.resolve(__dirname, "../../registry");
|
|
13
18
|
}
|
|
14
19
|
/**
|
|
15
20
|
* Reads and parses the central registry index manifest.
|
|
16
|
-
*
|
|
21
|
+
* Prioritizes local monorepo registry during development, and falls back to remote CDN for end-users.
|
|
17
22
|
*
|
|
23
|
+
* @param customRegistryUrl - Optional custom registry base URL to fetch index from
|
|
18
24
|
* @returns The list of available registry items or null if not found.
|
|
19
25
|
*/
|
|
20
|
-
export async function getRegistryIndex() {
|
|
21
|
-
|
|
26
|
+
export async function getRegistryIndex(customRegistryUrl) {
|
|
27
|
+
const baseUrl = customRegistryUrl || OFFICIAL_REGISTRY_URL;
|
|
28
|
+
// 1. Prioritize local monorepo registry if present
|
|
29
|
+
if (!customRegistryUrl) {
|
|
30
|
+
const localDir = getLocalRegistryDirectory();
|
|
31
|
+
const indexPath = path.join(localDir, "index.json");
|
|
32
|
+
if (await fs.pathExists(indexPath)) {
|
|
33
|
+
try {
|
|
34
|
+
const content = await fs.readFile(indexPath, "utf-8");
|
|
35
|
+
return JSON.parse(content);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Fallback to online CDN
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// 2. Fetch online manifest from remote CDN with cache-busting
|
|
22
43
|
try {
|
|
23
44
|
const cacheBuster = Date.now();
|
|
24
|
-
const response = await fetch(`${
|
|
45
|
+
const response = await fetch(`${baseUrl}/index.json?t=${cacheBuster}`, {
|
|
25
46
|
headers: { "Cache-Control": "no-cache, no-store" },
|
|
26
47
|
});
|
|
27
48
|
if (response.ok) {
|
|
@@ -33,18 +54,6 @@ export async function getRegistryIndex() {
|
|
|
33
54
|
catch {
|
|
34
55
|
// Fallback to local files if offline or network error occurs
|
|
35
56
|
}
|
|
36
|
-
// 2. Local package fallback
|
|
37
|
-
const localDir = getLocalRegistryDirectory();
|
|
38
|
-
const indexPath = path.join(localDir, "index.json");
|
|
39
|
-
if (await fs.pathExists(indexPath)) {
|
|
40
|
-
try {
|
|
41
|
-
const content = await fs.readFile(indexPath, "utf-8");
|
|
42
|
-
return JSON.parse(content);
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
57
|
return null;
|
|
49
58
|
}
|
|
50
59
|
/**
|
|
@@ -69,21 +78,29 @@ export async function fetchRemoteRegistryItem(url) {
|
|
|
69
78
|
}
|
|
70
79
|
}
|
|
71
80
|
/**
|
|
72
|
-
* Fetches the manifest for a component by name or remote URL.
|
|
73
|
-
* Checks
|
|
81
|
+
* Fetches the manifest for a component by name, namespace, or remote URL.
|
|
82
|
+
* Checks local package manifest first (for monorepo dev & offline), then custom registries from config, and finally official remote GitHub CDN.
|
|
74
83
|
*
|
|
75
|
-
* @param nameOrUrl - Component identifier
|
|
84
|
+
* @param nameOrUrl - Component identifier (e.g. "button", "@acme/hero-01", "https://...")
|
|
85
|
+
* @param customRegistries - Optional dictionary of custom namespace registries from nikala.config.json
|
|
76
86
|
*/
|
|
77
|
-
export async function getRegistryItem(nameOrUrl) {
|
|
87
|
+
export async function getRegistryItem(nameOrUrl, customRegistries) {
|
|
88
|
+
// 1. Direct HTTP(S) URL
|
|
78
89
|
if (nameOrUrl.startsWith("http://") || nameOrUrl.startsWith("https://")) {
|
|
79
90
|
return fetchRemoteRegistryItem(nameOrUrl);
|
|
80
91
|
}
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
// 2. Custom 3rd-party namespace registry (e.g. "@acme/hero-01")
|
|
93
|
+
if (nameOrUrl.startsWith("@") && nameOrUrl.includes("/")) {
|
|
94
|
+
const [namespace, ...rest] = nameOrUrl.split("/");
|
|
95
|
+
const itemName = rest.join("/");
|
|
96
|
+
const registryBaseUrl = customRegistries?.[namespace];
|
|
97
|
+
if (registryBaseUrl) {
|
|
98
|
+
const cleanBase = registryBaseUrl.replace(/\/$/, "");
|
|
99
|
+
const remoteUrl = `${cleanBase}/${itemName}.json`;
|
|
100
|
+
return fetchRemoteRegistryItem(remoteUrl);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// 3. Local package / Monorepo registry file
|
|
87
104
|
const localDir = getLocalRegistryDirectory();
|
|
88
105
|
const itemPath = path.join(localDir, `${nameOrUrl}.json`);
|
|
89
106
|
if (await fs.pathExists(itemPath)) {
|
|
@@ -92,22 +109,24 @@ export async function getRegistryItem(nameOrUrl) {
|
|
|
92
109
|
return JSON.parse(content);
|
|
93
110
|
}
|
|
94
111
|
catch {
|
|
95
|
-
|
|
112
|
+
// Fallback to online CDN
|
|
96
113
|
}
|
|
97
114
|
}
|
|
98
|
-
|
|
115
|
+
// 4. Official GitHub CDN registry
|
|
116
|
+
const remoteUrl = `${OFFICIAL_REGISTRY_URL}/${nameOrUrl}.json`;
|
|
117
|
+
return fetchRemoteRegistryItem(remoteUrl);
|
|
99
118
|
}
|
|
100
119
|
/**
|
|
101
120
|
* Recursively resolves all required internal registry dependencies for a set of component names or URLs.
|
|
102
121
|
*/
|
|
103
|
-
export async function resolveRegistryDependencies(namesOrUrls) {
|
|
122
|
+
export async function resolveRegistryDependencies(namesOrUrls, customRegistries) {
|
|
104
123
|
const resolved = new Set();
|
|
105
124
|
const queue = [...namesOrUrls];
|
|
106
125
|
while (queue.length > 0) {
|
|
107
126
|
const current = queue.shift();
|
|
108
127
|
if (!current || resolved.has(current))
|
|
109
128
|
continue;
|
|
110
|
-
const item = await getRegistryItem(current);
|
|
129
|
+
const item = await getRegistryItem(current, customRegistries);
|
|
111
130
|
if (!item)
|
|
112
131
|
continue;
|
|
113
132
|
resolved.add(current);
|
package/dist/utils/theme.js
CHANGED
|
@@ -171,16 +171,16 @@ export const PRIMARY_COLORS = {
|
|
|
171
171
|
},
|
|
172
172
|
},
|
|
173
173
|
yellow: {
|
|
174
|
-
light: "oklch(0.
|
|
175
|
-
dark: "oklch(0.
|
|
174
|
+
light: "oklch(0.795 0.184 86.047)",
|
|
175
|
+
dark: "oklch(0.852 0.199 91.936)",
|
|
176
176
|
lightFg: "oklch(0.1450 0 0)",
|
|
177
177
|
darkFg: "oklch(0.1450 0 0)",
|
|
178
178
|
charts: {
|
|
179
|
-
chart1: "oklch(0.
|
|
180
|
-
chart2: "oklch(0.
|
|
181
|
-
chart3: "oklch(0.
|
|
182
|
-
chart4: "oklch(0.
|
|
183
|
-
chart5: "oklch(0.
|
|
179
|
+
chart1: "oklch(0.795 0.184 86.047)",
|
|
180
|
+
chart2: "oklch(0.65 0.16 50)",
|
|
181
|
+
chart3: "oklch(0.55 0.15 85)",
|
|
182
|
+
chart4: "oklch(0.85 0.12 60)",
|
|
183
|
+
chart5: "oklch(0.45 0.14 75)",
|
|
184
184
|
},
|
|
185
185
|
},
|
|
186
186
|
lime: {
|