@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.
@@ -2,6 +2,7 @@ interface AddOptions {
2
2
  overwrite?: boolean;
3
3
  all?: boolean;
4
4
  hook?: boolean;
5
+ block?: boolean;
5
6
  }
6
7
  /**
7
8
  * Command handler to fetch and install Nikala UI components from GitHub remote registry or custom URLs.
@@ -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 filteredRegistry = registryIndex.filter((item) => isHookMode ? item.type === "registry:hook" : item.type !== "registry:hook");
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 components to project...\n`));
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 = { ...userPkg.dependencies, ...userPkg.devDependencies };
78
- for (const dep of requiredNpmDeps) {
79
- if (!installedDeps[dep]) {
80
- missingNpmDeps.push(dep);
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
- missingNpmDeps.push(...Array.from(requiredNpmDeps));
94
+ // Gracefully continue
86
95
  }
87
96
  }
88
- else {
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
  }
@@ -1,5 +1,6 @@
1
1
  interface InitOptions {
2
2
  defaults?: boolean;
3
+ ai?: boolean;
3
4
  }
4
5
  /**
5
6
  * Initializes Nikala UI workspace configuration and sets up design tokens.
@@ -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
- console.log(`\n🎨 ${pc.bold(pc.red("Nikala UI"))} ${pc.dim("v0.10.1")} — SolidJS + Tailwind v4 components`);
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.10.1");
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();
@@ -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 providers).
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 providers).
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 if (configContent.includes("defineConfig({")) {
21
- configContent = configContent.replace("defineConfig({", "defineConfig({\n plugins: [tailwindcss()],");
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
- if (configContent.includes("defineConfig({")) {
30
- configContent = configContent.replace("defineConfig({", `defineConfig({\n resolve: {\n alias: {\n "@": path.resolve(__dirname, "./src"),\n },\n },`);
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,4 @@
1
+ /**
2
+ * Sets up AI rules (.cursor/rules/nikala.mdc, .cursorrules, AGENTS.md) in the target workspace.
3
+ */
4
+ export declare function setupAiRules(cwd: string): Promise<void>;
@@ -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 { execSync } from "node:child_process";
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
- const depsString = dependencies.join(" ");
51
- let command = "";
52
- switch (pkgManager) {
53
- case "bun":
54
- command = `bun add ${depsString}`;
55
- break;
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(` ${command}\n`));
59
+ console.log(pc.white(` ${pkgManager} ${args.join(" ")}\n`));
69
60
  try {
70
- execSync(command, { cwd, stdio: "inherit" });
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: ${command}`));
91
+ console.log(pc.yellow(` Please run manually: ${pkgManager} ${args.join(" ")}`));
101
92
  }
102
93
  }
@@ -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
- * Bypasses GitHub Raw CDN caching using timestamps and fetches fresh index files.
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 official remote GitHub registry first, then falls back to local package manifest if offline.
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 or full HTTP(S) URL
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[]>;
@@ -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
- * Bypasses GitHub Raw CDN caching using timestamps and fetches fresh index files.
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
- // 1. Attempt fetching online manifest from GitHub Raw CDN with cache-busting
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(`${OFFICIAL_REGISTRY_URL}/index.json?t=${cacheBuster}`, {
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 official remote GitHub registry first, then falls back to local package manifest if offline.
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 or full HTTP(S) URL
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
- // 1. Attempt fetching online component manifest from official GitHub CDN
82
- const remoteUrl = `${OFFICIAL_REGISTRY_URL}/${nameOrUrl}.json`;
83
- const remoteItem = await fetchRemoteRegistryItem(remoteUrl);
84
- if (remoteItem)
85
- return remoteItem;
86
- // 2. Fallback to local package files if offline or unreleased
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
- return null;
112
+ // Fallback to online CDN
96
113
  }
97
114
  }
98
- return null;
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);
@@ -171,16 +171,16 @@ export const PRIMARY_COLORS = {
171
171
  },
172
172
  },
173
173
  yellow: {
174
- light: "oklch(0.75 0.18 90)",
175
- dark: "oklch(0.82 0.16 90)",
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.82 0.18 90)",
180
- chart2: "oklch(0.72 0.16 80)",
181
- chart3: "oklch(0.62 0.14 100)",
182
- chart4: "oklch(0.88 0.12 95)",
183
- chart5: "oklch(0.52 0.15 85)",
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: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nikala-ui/cli",
3
- "version": "0.10.1",
3
+ "version": "0.11.0-nightly.63085f3",
4
4
  "description": "Command line interface for Nikala UI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",