From e7ca0114a1f267308a218b8d77314256bcb74f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dandelion=20Man=C3=A9?= Date: Tue, 3 Jun 2025 16:37:01 -0700 Subject: [PATCH] feat: recursive `add`, simpler `sync` - add command now auto removes then adds, and adds deps recursively. - sync is basically an alias for adding things (or adding everything) --- .claude/settings.local.json | 3 +- CLI.md | 62 +++----------- README.md | 4 +- bin/cli.ts | 26 +++++- src/commands/add.test.ts | 97 ++++++++++++++++++++-- src/commands/add.ts | 149 ++++++++++++++++++++-------------- src/commands/remove.ts | 54 ++++++++---- src/commands/sync.test.ts | 14 ++-- src/commands/sync.ts | 23 +----- tests/helpers/test-utils.ts | 6 ++ tests/integration/add.test.ts | 117 ++++++++++++++++++-------- 11 files changed, 356 insertions(+), 199 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 243f450..51e8fce 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -8,7 +8,8 @@ "Bash(bun test:*)", "Bash(bun run test-cli:*)", "Bash(rg:*)", - "Bash(find:*)" + "Bash(find:*)", + "Bash(ls:*)" ], "deny": [] }, diff --git a/CLI.md b/CLI.md index 7aa97ff..f5d488a 100644 --- a/CLI.md +++ b/CLI.md @@ -64,7 +64,7 @@ mirascope-ui [global-flags] [command-args] ### Global Flags -- `--local` - Use local registry.json file in current directory +- `--local` - Use local registry.json file in current directory. When using `--local`, `--target` must be set. - `--local-path ` - Use local registry.json file at specified path - `--registry-url ` - Override registry URL (default: https://ui.mirascope.com) - `--target ` - Target directory for file operations (default: current directory) @@ -81,23 +81,17 @@ Creates initial manifest.json file and sets up mirascope-ui/ directory structure #### `mirascope-ui add [component2] ...` -Adds new components to the project. +Adds or updates components in the project. - Downloads component files from registry to `mirascope-ui/` -- Resolves and includes registry dependencies (e.g., components using `cn` util automatically get `utils`) +- Recursively resolves registry dependencies (e.g., `button-link` → `button` → `utils`) +- Auto-syncs components: If component already tracked, removes and re-adds with latest version - Installs npm dependencies via `bun add` - Updates manifest to track new components -- Skips components already tracked (use `sync` to update them) -#### `mirascope-ui sync [component1] [component2] ...]` +#### `mirascope-ui sync [component1] [component2] ...` -Updates existing tracked components to latest versions. - -- **No components specified**: Syncs all tracked components -- **Components specified**: Syncs only named components -- **Implementation**: Runs `remove` then `add` commands to ensure clean updates -- Resolves new dependencies that may have been added to components -- Updates manifest timestamps +Updates all tracked components to latest versions. If components are passed, then it is an alias for calling `mirascpe-ui add`. #### `mirascope-ui remove [component2] ...` @@ -129,11 +123,12 @@ For testing registry changes during development: #### `--local` flag ```bash -mirascope-ui --local add button +mirascope-ui --local --target ../other/path add button ``` - Reads `registry.json` from current directory -- Useful when working within the registry project itself +- Useful when you want to test local changes to registry within another project. +- Must use `--target` flag too (since installing registry components into the registry itself does not make sense) #### `--local-path` flag @@ -160,7 +155,7 @@ mirascope-ui --target /path/to/project add button ``` - Changes where files are written (default: current directory) -- Useful for testing or scripting operations on other projects +- Useful for testing changes on other projects ## Integration Points @@ -175,13 +170,6 @@ mirascope-ui --target /path/to/project add button } ``` -### GitHub Actions Integration - -- Daily workflow runs `mirascope-ui status` -- If updates available, runs `mirascope-ui` -- Creates PR with changes and updated manifest -- PR description shows which components were updated - ## Key Behaviors ### Overwrite Strategy @@ -194,20 +182,6 @@ mirascope-ui --target /path/to/project add button Components can declare dependencies on other registry components using `registryDependencies`. For example, most UI components depend on the `utils` component for the `cn()` utility function. -When adding or syncing components, registry dependencies are automatically resolved and included. This ensures all required utilities and base components are available. - -### Dependency Management - -- Installs npm dependencies via `bun add` for new components -- Registry dependencies automatically resolved and included -- Does not remove npm dependencies when removing components (to avoid breaking local code) - -### Overwrite Strategy - -- Registry files are **always overwritten** during sync operations -- No merge conflicts - registry version always wins -- Clear separation between registry files (`mirascope-ui/`) and local components (`src/`) prevents accidental modifications - ## Examples ### Basic Usage @@ -258,19 +232,3 @@ mirascope-ui --target ./my-project add button } } ``` - -GitHub Actions workflow: - -```yaml -- name: Sync UI Registry - run: | - bun mirascope-ui status - bun mirascope-ui sync -``` - -## Architecture Notes - -- **Command delegation**: `sync` command delegates to `remove` + `add` for clean updates -- **Atomic operations**: All file operations are atomic - either fully succeed or fail cleanly -- **Comprehensive testing**: 100+ tests covering all commands and edge cases -- **TypeScript**: Full type safety throughout codebase diff --git a/README.md b/README.md index e5aacf5..adf8215 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,10 @@ bun add -D @mirascope/ui # Initialize your project bunx mirascope-ui init -# Add components +# Add components (automatically syncs if already exists) bunx mirascope-ui add button dialog -# Keep components in sync +# Sync all tracked components bunx mirascope-ui sync ``` diff --git a/bin/cli.ts b/bin/cli.ts index 7bfb88e..ac3fb08 100644 --- a/bin/cli.ts +++ b/bin/cli.ts @@ -8,7 +8,7 @@ import { StatusCommand } from "../src/commands/status"; import { ExecutionContext } from "../src/commands/base"; import { FileRegistry, RemoteRegistry } from "../src/registry"; import { existsSync } from "fs"; -import { join } from "path"; +import { join, resolve } from "path"; import { REGISTRY_URL } from "@/src/constants"; const COMMANDS = { @@ -92,6 +92,30 @@ async function main() { process.exit(1); } + // Validate --local requires --target + if (globalFlags.local && !globalFlags.target) { + console.error("❌ --local requires --target to specify where to install components"); + console.error( + "Usage: mirascope-ui --local [--local-path ] --target " + ); + process.exit(1); + } + + // Validate source and target are different directories + if (globalFlags.local) { + const sourcePath = globalFlags.localPath || process.cwd(); + const resolvedSource = resolve(sourcePath); + const resolvedTarget = resolve(targetPath); + + if (resolvedSource === resolvedTarget) { + console.error("❌ Registry source and target directories cannot be the same"); + console.error( + "Use --local-path to specify a different registry location, or --target to specify a different target location." + ); + process.exit(1); + } + } + // Create registry based on flags const registry = globalFlags.local ? new FileRegistry(globalFlags.localPath || process.cwd()) diff --git a/src/commands/add.test.ts b/src/commands/add.test.ts index bd58550..63a36eb 100644 --- a/src/commands/add.test.ts +++ b/src/commands/add.test.ts @@ -161,14 +161,25 @@ describe("AddCommand", () => { expect(logSpy).toHaveBeenCalledWith("✅ Added 1 component"); }); - test("warns about already tracked components", async () => { + test("syncs already tracked components", async () => { const buttonComponent: RegistryComponent = { name: "button", type: "registry:ui" as const, files: [{ path: "mirascope-ui/ui/button.tsx", type: "registry:ui" as const, content: "" }], }; - const context = createTestContext([buttonComponent], {}, tempDir); + const files = { + "mirascope-ui/ui/button.tsx": "export const Button = () => ;", + }; + + const context = createTestContext([buttonComponent], files, tempDir); + + // Create existing button file + await mkdir("mirascope-ui/ui", { recursive: true }); + await writeFile( + "mirascope-ui/ui/button.tsx", + "export const Button = () => ;" + ); await writeFile( "mirascope-ui/manifest.json", @@ -189,8 +200,13 @@ describe("AddCommand", () => { await command.execute(["button"], context); - expect(logSpy).toHaveBeenCalledWith("⚠️ Already tracking: button"); - expect(logSpy).toHaveBeenCalledWith("✅ All components already tracked"); + // Should show sync message + expect(logSpy).toHaveBeenCalledWith("🔄 Syncing button..."); + expect(logSpy).toHaveBeenCalledWith("✅ Added 1 component"); + + // Component should be updated + const buttonContent = await readFile("mirascope-ui/ui/button.tsx", "utf-8"); + expect(buttonContent).toBe("export const Button = () => ;"); }); test("adds multiple components", async () => { @@ -289,13 +305,82 @@ describe("AddCommand", () => { expect(manifest.components.button).toBeDefined(); expect(installSpy).toHaveBeenCalledWith( - ["@radix-ui/react-alert-dialog", "@radix-ui/react-slot"], + expect.arrayContaining(["@radix-ui/react-alert-dialog", "@radix-ui/react-slot"]), tempDir ); - expect(logSpy).toHaveBeenCalledWith("🔗 Resolving registry dependencies: button"); + expect(logSpy).toHaveBeenCalledWith("📦 Adding button..."); expect(logSpy).toHaveBeenCalledWith("✅ Added 2 components"); }); + + test("resolves recursive registry dependencies", async () => { + const components: RegistryComponent[] = [ + { + name: "primary", + type: "registry:ui" as const, + dependencies: ["dep-a"], + registryDependencies: ["secondary"], + files: [ + { path: "mirascope-ui/ui/primary.tsx", type: "registry:ui" as const, content: "" }, + ], + }, + { + name: "secondary", + type: "registry:ui" as const, + dependencies: ["dep-b"], + registryDependencies: ["tertiary"], + files: [ + { path: "mirascope-ui/ui/secondary.tsx", type: "registry:ui" as const, content: "" }, + ], + }, + { + name: "tertiary", + type: "registry:lib" as const, + dependencies: ["dep-c"], + files: [ + { path: "mirascope-ui/lib/tertiary.ts", type: "registry:lib" as const, content: "" }, + ], + }, + ]; + + const files = { + "mirascope-ui/ui/primary.tsx": "export const Primary = () => null;", + "mirascope-ui/ui/secondary.tsx": "export const Secondary = () => null;", + "mirascope-ui/lib/tertiary.ts": "export const tertiary = () => {};", + }; + + const context = createTestContext(components, files, tempDir); + + await mkdir("mirascope-ui", { recursive: true }); + await writeFile( + "mirascope-ui/manifest.json", + JSON.stringify({ + registryUrl: REGISTRY_URL, + components: {}, + lastFullSync: "", + }) + ); + + const command = new AddCommand(); + const installSpy = spyOn(command as any, "installDependencies").mockResolvedValue(undefined); + + await command.execute(["primary"], context); + + const manifest = JSON.parse(await readFile("mirascope-ui/manifest.json", "utf-8")); + + // All three components should be installed (primary -> secondary -> tertiary) + expect(manifest.components.primary).toBeDefined(); + expect(manifest.components.secondary).toBeDefined(); + expect(manifest.components.tertiary).toBeDefined(); + + // All npm dependencies from all levels should be collected + expect(installSpy).toHaveBeenCalledWith( + expect.arrayContaining(["dep-a", "dep-b", "dep-c"]), + tempDir + ); + + expect(logSpy).toHaveBeenCalledWith("✅ Added 3 components"); + }); }); describe("error handling", () => { diff --git a/src/commands/add.ts b/src/commands/add.ts index 7ab2539..6922983 100644 --- a/src/commands/add.ts +++ b/src/commands/add.ts @@ -5,6 +5,7 @@ import { writeFile, mkdir } from "fs/promises"; import { join, dirname } from "path"; import { parseArgs } from "util"; import { InitCommand } from "./init"; +import { RemoveCommand } from "./remove"; export class AddCommand extends BaseCommand { async execute(args: string[], context: ExecutionContext): Promise { @@ -53,81 +54,109 @@ export class AddCommand extends BaseCommand { await initCommand.execute([], context); } - const components = await findComponents(context.registry, componentNames); - - // Check for already tracked components - const manifestData = await manifest.read(); - const alreadyTracked = components.filter((c) => c.name in manifestData.components); - if (alreadyTracked.length > 0) { - console.log(`⚠️ Already tracking: ${alreadyTracked.map((c) => c.name).join(", ")}`); - console.log(" Use 'mirascope-ui sync' to update them"); + // Track what we've processed in this operation to avoid duplicates + const processedComponents = new Set(); + const allNpmDeps = new Set(); + const addedComponents: string[] = []; + + // Process each requested component recursively + for (const componentName of componentNames) { + await this.addComponentRecursively( + componentName, + context, + processedComponents, + allNpmDeps, + addedComponents + ); } - const newComponents = components.filter((c) => !(c.name in manifestData.components)); - if (newComponents.length === 0) { - console.log("✅ All components already tracked"); - return; + // Install all collected npm dependencies + if (allNpmDeps.size > 0) { + console.log(`📦 Installing dependencies: ${Array.from(allNpmDeps).join(", ")}`); + await this.installDependencies(Array.from(allNpmDeps), context.targetPath); } - // Collect all dependencies (including registry dependencies) - const allDeps = new Set(); - const registryDeps = new Set(); + console.log( + `✅ Added ${addedComponents.length} component${addedComponents.length === 1 ? "" : "s"}` + ); + addedComponents.forEach((c) => console.log(` • ${c}`)); + } catch (error) { + console.error(`❌ ${error instanceof Error ? error.message : String(error)}`); + process.exit(1); + } + } - for (const component of newComponents) { - component.dependencies?.forEach((dep) => allDeps.add(dep)); - component.registryDependencies?.forEach((dep) => registryDeps.add(dep)); - } + private async addComponentRecursively( + componentName: string, + context: ExecutionContext, + processedComponents: Set, + allNpmDeps: Set, + addedComponents: string[] + ): Promise { + // Skip if already processed in this operation + if (processedComponents.has(componentName)) { + return; + } - // Resolve registry dependencies - if (registryDeps.size > 0) { - console.log(`🔗 Resolving registry dependencies: ${Array.from(registryDeps).join(", ")}`); - const depComponents = await findComponents(context.registry, Array.from(registryDeps)); - for (const dep of depComponents) { - if (!(dep.name in manifestData.components)) { - newComponents.push(dep); - dep.dependencies?.forEach((d) => allDeps.add(d)); - } - } - } + processedComponents.add(componentName); - // Download and save all component files - for (const component of newComponents) { - console.log(`📦 Adding ${component.name}...`); + const manifest = new ManifestManager(context.targetPath); + const manifestData = await manifest.read(); - const files: string[] = []; - for (const file of component.files) { - const content = await context.registry.fetchComponentFile(file.path); + // If component is already tracked, remove it first (sync behavior) + if (componentName in manifestData.components) { + console.log(`🔄 Syncing ${componentName}...`); + const removeCommand = new RemoveCommand(); + await removeCommand.removeComponent(componentName, context, false); + } - // Determine target path - const targetPath = file.target || this.getDefaultTargetPath(file.path); - const fullTargetPath = join(context.targetPath, targetPath); + // Fetch the component + const components = await findComponents(context.registry, [componentName]); + if (components.length === 0) { + throw new Error(`Component '${componentName}' not found in registry`); + } - // Ensure directory exists - await mkdir(dirname(fullTargetPath), { recursive: true }); + const component = components[0]; + + // First, recursively add all registry dependencies + if (component.registryDependencies && component.registryDependencies.length > 0) { + for (const dep of component.registryDependencies) { + await this.addComponentRecursively( + dep, + context, + processedComponents, + allNpmDeps, + addedComponents + ); + } + } - // Write file - await writeFile(fullTargetPath, content, "utf-8"); - files.push(targetPath); - } + // Now add this component + const isSync = componentName in manifestData.components; + console.log(`📦 ${isSync ? "Syncing" : "Adding"} ${componentName}...`); - // Add to manifest - await manifest.addComponent(component.name, files); - } + const files: string[] = []; + for (const file of component.files) { + const content = await context.registry.fetchComponentFile(file.path); - // Install npm dependencies if any - if (allDeps.size > 0) { - console.log(`📦 Installing dependencies: ${Array.from(allDeps).join(", ")}`); - await this.installDependencies(Array.from(allDeps), context.targetPath); - } + // Determine target path + const targetPath = file.target || this.getDefaultTargetPath(file.path); + const fullTargetPath = join(context.targetPath, targetPath); - console.log( - `✅ Added ${newComponents.length} component${newComponents.length === 1 ? "" : "s"}` - ); - newComponents.forEach((c) => console.log(` • ${c.name}`)); - } catch (error) { - console.error(`❌ ${error instanceof Error ? error.message : String(error)}`); - process.exit(1); + // Ensure directory exists + await mkdir(dirname(fullTargetPath), { recursive: true }); + + // Write file + await writeFile(fullTargetPath, content, "utf-8"); + files.push(targetPath); } + + // Add to manifest + await manifest.addComponent(componentName, files); + addedComponents.push(componentName); + + // Collect npm dependencies + component.dependencies?.forEach((dep) => allNpmDeps.add(dep)); } private getDefaultTargetPath(sourcePath: string): string { diff --git a/src/commands/remove.ts b/src/commands/remove.ts index 72ee1d8..ba473c8 100644 --- a/src/commands/remove.ts +++ b/src/commands/remove.ts @@ -69,23 +69,7 @@ export class RemoveCommand extends BaseCommand { for (const componentName of componentNames) { console.log(`📦 Removing ${componentName}...`); - - const componentInfo = manifestData.components[componentName]; - - // Remove files - for (const file of componentInfo.files) { - const fullFilePath = join(context.targetPath, file); - try { - await rm(fullFilePath, { force: true }); - } catch (error) { - console.warn( - `⚠️ Could not remove file ${file}: ${error instanceof Error ? error.message : String(error)}` - ); - } - } - - // Remove from manifest - await manifest.removeComponent(componentName); + await this.removeComponent(componentName, context, true); } console.log( @@ -97,4 +81,40 @@ export class RemoveCommand extends BaseCommand { process.exit(1); } } + + /** + * Core removal logic shared between public and silent removal methods. + */ + async removeComponent( + componentName: string, + context: ExecutionContext, + showWarnings: boolean = false + ): Promise { + const manifest = new ManifestManager(context.targetPath); + const manifestData = await manifest.read(); + + // Skip if component not tracked + if (!(componentName in manifestData.components)) { + return; + } + + const componentInfo = manifestData.components[componentName]; + + // Remove files + for (const file of componentInfo.files) { + const fullFilePath = join(context.targetPath, file); + try { + await rm(fullFilePath, { force: true }); + } catch (error) { + if (showWarnings) { + console.warn( + `⚠️ Could not remove file ${file}: ${error instanceof Error ? error.message : String(error)}` + ); + } + } + } + + // Remove from manifest + await manifest.removeComponent(componentName); + } } diff --git a/src/commands/sync.test.ts b/src/commands/sync.test.ts index 6ed213c..bc5f7e3 100644 --- a/src/commands/sync.test.ts +++ b/src/commands/sync.test.ts @@ -116,9 +116,9 @@ describe("SyncCommand", () => { await command.execute([], context); expect(logSpy).toHaveBeenCalledWith("🔄 Syncing all 2 tracked components..."); - expect(logSpy).toHaveBeenCalledWith("🗑️ Removing components: button, card"); - expect(logSpy).toHaveBeenCalledWith("🔍 Fetching components: button, card"); - expect(logSpy).toHaveBeenCalledWith("✅ Synced 2 components"); + expect(logSpy).toHaveBeenCalledWith("🔄 Syncing button..."); + expect(logSpy).toHaveBeenCalledWith("🔄 Syncing card..."); + expect(logSpy).toHaveBeenCalledWith("✅ Added 2 components"); const buttonContent = await readFile("mirascope-ui/ui/button.tsx", "utf-8"); expect(buttonContent).toBe("export const Button = () => ;"); @@ -165,9 +165,8 @@ describe("SyncCommand", () => { await command.execute(["button"], context); expect(logSpy).toHaveBeenCalledWith("🔄 Syncing components: button"); - expect(logSpy).toHaveBeenCalledWith("🗑️ Removing components: button"); - expect(logSpy).toHaveBeenCalledWith("🔍 Fetching components: button"); - expect(logSpy).toHaveBeenCalledWith("✅ Synced 1 component"); + expect(logSpy).toHaveBeenCalledWith("🔄 Syncing button..."); + expect(logSpy).toHaveBeenCalledWith("✅ Added 1 component"); }); test("shows error for untracked components", async () => { @@ -192,8 +191,7 @@ describe("SyncCommand", () => { await expect(command.execute(["nonexistent"], context)).rejects.toThrow( "process.exit called" ); - expect(errorSpy).toHaveBeenNthCalledWith(1, "❌ Components not tracked: nonexistent"); - expect(errorSpy).toHaveBeenNthCalledWith(2, "Available components:", "button"); + expect(errorSpy).toHaveBeenCalledWith('❌ Component "nonexistent" not found in registry'); }); test("updates lastSync timestamp", async () => { diff --git a/src/commands/sync.ts b/src/commands/sync.ts index e540a3b..ce37635 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -1,6 +1,5 @@ import { BaseCommand, ExecutionContext } from "./base"; import { ManifestManager } from "../manifest"; -import { RemoveCommand } from "./remove"; import { AddCommand } from "./add"; import { parseArgs } from "util"; @@ -55,28 +54,16 @@ export class SyncCommand extends BaseCommand { let componentsToSync: string[]; if (componentNames.length > 0) { - // Sync specific components - const invalidComponents = componentNames.filter( - (name) => !(name in manifestData.components) - ); - if (invalidComponents.length > 0) { - console.error(`❌ Components not tracked: ${invalidComponents.join(", ")}`); - console.error("Available components:", trackedComponents.join(", ")); - process.exit(1); - } + // Sync specific components (no need to validate - AddCommand will handle) componentsToSync = componentNames; console.log(`🔄 Syncing components: ${componentsToSync.join(", ")}`); } else { - // Sync all components + // Sync all tracked components componentsToSync = trackedComponents; console.log(`🔄 Syncing all ${componentsToSync.length} tracked components...`); } - // Step 1: Remove components - const removeCommand = new RemoveCommand(); - await removeCommand.execute(componentsToSync, context); - - // Step 2: Add components back (which will pull latest + dependencies) + // AddCommand now handles remove + add automatically for existing components const addCommand = new AddCommand(); await addCommand.execute(componentsToSync, context); @@ -84,10 +71,6 @@ export class SyncCommand extends BaseCommand { if (componentNames.length === 0) { await manifest.updateFullSync(); } - - console.log( - `✅ Synced ${componentsToSync.length} component${componentsToSync.length === 1 ? "" : "s"}` - ); } catch (error) { console.error(`❌ ${error instanceof Error ? error.message : String(error)}`); process.exit(1); diff --git a/tests/helpers/test-utils.ts b/tests/helpers/test-utils.ts index 96e77f2..2402474 100644 --- a/tests/helpers/test-utils.ts +++ b/tests/helpers/test-utils.ts @@ -23,6 +23,12 @@ export async function createTempProject(basePath: string, name: string): Promise return projectPath; } +export async function createTempRegistry(basePath: string, name: string): Promise { + const registryPath = join(basePath, name); + await mkdir(registryPath, { recursive: true }); + return registryPath; +} + export async function cleanupTempDir(path: string): Promise { try { await rm(path, { recursive: true, force: true }); diff --git a/tests/integration/add.test.ts b/tests/integration/add.test.ts index 49ba8f1..47f0d2e 100644 --- a/tests/integration/add.test.ts +++ b/tests/integration/add.test.ts @@ -1,14 +1,21 @@ import { describe, test, expect, beforeEach, afterEach } from "bun:test"; import { mkdir, writeFile, readFile, stat } from "fs/promises"; import { join } from "path"; -import { runCLI, createTempProject, cleanupTempDir } from "../helpers/test-utils"; +import { + runCLI, + createTempProject, + createTempRegistry, + cleanupTempDir, +} from "../helpers/test-utils"; describe("Add Command Integration", () => { const tempDir = join(process.cwd(), "test-temp-add-integration"); let projectPath: string; + let registryPath: string; beforeEach(async () => { projectPath = await createTempProject(tempDir, "test-project"); + registryPath = await createTempRegistry(tempDir, "test-registry"); }); afterEach(async () => { @@ -35,12 +42,12 @@ describe("Add Command Integration", () => { ], }; - await writeFile(join(projectPath, "registry.json"), JSON.stringify(registryData)); + await writeFile(join(registryPath, "registry.json"), JSON.stringify(registryData)); // Create registry files - await mkdir(join(projectPath, "mirascope-ui", "ui"), { recursive: true }); + await mkdir(join(registryPath, "mirascope-ui", "ui"), { recursive: true }); await writeFile( - join(projectPath, "mirascope-ui", "ui", "button.tsx"), + join(registryPath, "mirascope-ui", "ui", "button.tsx"), "export const Button = () => ;" ); @@ -48,7 +55,10 @@ describe("Add Command Integration", () => { await runCLI(["init"], projectPath); // Add component - const result = await runCLI(["add", "--local", "button"], projectPath); + const result = await runCLI( + ["add", "--local-path", registryPath, "--target", projectPath, "button"], + projectPath + ); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("🔍 Fetching components: button"); @@ -96,16 +106,16 @@ describe("Add Command Integration", () => { ], }; - await writeFile(join(projectPath, "registry.json"), JSON.stringify(registryData)); + await writeFile(join(registryPath, "registry.json"), JSON.stringify(registryData)); // Create registry files - await mkdir(join(projectPath, "mirascope-ui", "ui"), { recursive: true }); + await mkdir(join(registryPath, "mirascope-ui", "ui"), { recursive: true }); await writeFile( - join(projectPath, "mirascope-ui", "ui", "button.tsx"), + join(registryPath, "mirascope-ui", "ui", "button.tsx"), "export const Button = () =>