Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"Bash(bun test:*)",
"Bash(bun run test-cli:*)",
"Bash(rg:*)",
"Bash(find:*)"
"Bash(find:*)",
"Bash(ls:*)"
],
"deny": []
},
Expand Down
62 changes: 10 additions & 52 deletions CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ mirascope-ui [global-flags] <command> [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 <path>` - Use local registry.json file at specified path
- `--registry-url <url>` - Override registry URL (default: https://ui.mirascope.com)
- `--target <path>` - Target directory for file operations (default: current directory)
Expand All @@ -81,23 +81,17 @@ Creates initial manifest.json file and sets up mirascope-ui/ directory structure

#### `mirascope-ui add <component1> [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 <component1> [component2] ...`

Expand Down Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
26 changes: 25 additions & 1 deletion bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 <path>] --target <target-path> <command>"
);
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())
Expand Down
97 changes: 91 additions & 6 deletions src/commands/add.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => <button>Updated</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 = () => <button>Old</button>;"
);

await writeFile(
"mirascope-ui/manifest.json",
Expand All @@ -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 = () => <button>Updated</button>;");
});

test("adds multiple components", async () => {
Expand Down Expand Up @@ -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", () => {
Expand Down
Loading