mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
initial commit and build out
This commit is contained in:
+10
-133
@@ -1,139 +1,16 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
# Editor directories
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Sveltekit cache directory
|
||||
.svelte-kit/
|
||||
|
||||
# vitepress build output
|
||||
**/.vitepress/dist
|
||||
|
||||
# vitepress cache directory
|
||||
**/.vitepress/cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# Firebase cache directory
|
||||
.firebase/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v3
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# Vite logs files
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
# Keep dist/ - it's committed for distribution!
|
||||
# dist/
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
# vibe-design-plugins Repository
|
||||
|
||||
This repository provides cross-provider design skills and commands for LLM-powered development tools (Cursor, Claude Code, Gemini CLI, Codex CLI).
|
||||
|
||||
## Repository Purpose
|
||||
|
||||
Maintain a **single source of truth** for design-focused skills and commands, then automatically transform them into provider-specific formats. Each provider has different capabilities (frontmatter, arguments, modular files), so we use a build system to generate appropriate outputs.
|
||||
|
||||
## Architecture: Option A (Feature-Rich Source)
|
||||
|
||||
We use a **feature-rich source format** that gets transformed for each provider:
|
||||
|
||||
- **Source files** (`source/`): Full metadata with YAML frontmatter, args, descriptions
|
||||
- **Build system** (`scripts/`): Transforms source → provider-specific formats
|
||||
- **Distribution** (`dist/`): Committed output files for 4 providers
|
||||
|
||||
### Why Option A?
|
||||
|
||||
Cursor doesn't support frontmatter or arguments (lowest common denominator). Instead of limiting all providers, we:
|
||||
1. Author with full metadata in source files
|
||||
2. Generate full-featured versions for providers that support it (Claude Code, Gemini, Codex)
|
||||
3. Generate downgraded versions for Cursor (strip frontmatter, rely on appending)
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
vibe-design-plugins/
|
||||
├── source/ # EDIT THESE! Single source of truth
|
||||
│ ├── commands/ # Command definitions with frontmatter
|
||||
│ │ └── normalize.md
|
||||
│ └── skills/ # Skill definitions with frontmatter
|
||||
│ └── frontend-design.md
|
||||
├── dist/ # Generated outputs (committed for users)
|
||||
│ ├── cursor/ # Downgraded (no frontmatter)
|
||||
│ │ ├── commands/*.md
|
||||
│ │ └── rules/*.md
|
||||
│ ├── claude-code/ # Full featured
|
||||
│ │ ├── commands/*.md
|
||||
│ │ └── skills/*/SKILL.md
|
||||
│ ├── gemini/ # TOML commands + modular skills
|
||||
│ │ ├── commands/*.toml
|
||||
│ │ ├── GEMINI.md
|
||||
│ │ └── GEMINI.*.md
|
||||
│ └── codex/ # Custom prompts + modular skills
|
||||
│ ├── prompts/*.md
|
||||
│ ├── AGENTS.md
|
||||
│ └── AGENTS.*.md
|
||||
├── scripts/ # Build system (Bun)
|
||||
│ ├── build.js # Main orchestrator
|
||||
│ ├── lib/
|
||||
│ │ ├── utils.js # Shared utilities
|
||||
│ │ └── transformers/ # Provider-specific transformers
|
||||
│ │ ├── cursor.js
|
||||
│ │ ├── claude-code.js
|
||||
│ │ ├── gemini.js
|
||||
│ │ └── codex.js
|
||||
├── README.md # End user documentation
|
||||
├── DEVELOP.md # Contributor documentation
|
||||
└── package.json # Bun scripts
|
||||
```
|
||||
|
||||
## Source File Format
|
||||
|
||||
### Commands (`source/commands/*.md`)
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: command-name
|
||||
description: Clear description of what this command does
|
||||
args:
|
||||
- name: argname
|
||||
description: Argument description
|
||||
required: false
|
||||
---
|
||||
|
||||
Command prompt here. Use {{argname}} placeholders for arguments.
|
||||
```
|
||||
|
||||
### Skills (`source/skills/*.md`)
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: skill-name
|
||||
description: Clear description of what this skill provides
|
||||
license: License info (optional)
|
||||
---
|
||||
|
||||
Skill instructions for the LLM here.
|
||||
```
|
||||
|
||||
## Build System
|
||||
|
||||
Uses **Bun** for fast builds. Modular architecture:
|
||||
|
||||
- **`utils.js`**: Shared functions (parseFrontmatter, readSourceFiles, writeFile, etc.)
|
||||
- **Transformer pattern**: Each provider has one focused file
|
||||
- **Registry**: `transformers/index.js` exports all transformers
|
||||
- **Main script**: `build.js` orchestrates everything (~50 lines)
|
||||
|
||||
Run: `bun run build`
|
||||
|
||||
## Provider Transformations
|
||||
|
||||
### 1. Cursor (Downgraded)
|
||||
- **Commands**: Body only → `dist/cursor/commands/*.md`
|
||||
- **Skills**: Body only → `dist/cursor/rules/*.md`
|
||||
- **Strips**: All frontmatter, all metadata
|
||||
- **Args**: Not supported, arguments get appended to end of prompt
|
||||
|
||||
### 2. Claude Code (Full Featured)
|
||||
- **Commands**: Full YAML frontmatter → `dist/claude-code/commands/*.md`
|
||||
- **Skills**: Full YAML frontmatter → `dist/claude-code/skills/{name}/SKILL.md`
|
||||
- **Preserves**: All metadata, all args
|
||||
- **Format**: Matches [Anthropic Skills spec](https://github.com/anthropics/skills)
|
||||
|
||||
### 3. Gemini CLI (Full Featured)
|
||||
- **Commands**: TOML format → `dist/gemini/commands/*.toml`
|
||||
- Uses `description` and `prompt` keys
|
||||
- Transforms `{{argname}}` → `{{args}}` (Gemini uses single args string)
|
||||
- **Skills**: Modular with imports → `dist/gemini/GEMINI.{name}.md`
|
||||
- Main `GEMINI.md` uses `@./GEMINI.{name}.md` import syntax
|
||||
- Gemini automatically loads imported files
|
||||
|
||||
### 4. Codex CLI (Full Featured)
|
||||
- **Commands**: Custom prompt format → `dist/codex/prompts/*.md`
|
||||
- Uses `description` and `argument-hint` in frontmatter
|
||||
- Transforms `{{argname}}` → `$ARGNAME` (uppercase variables)
|
||||
- Invoked as `/prompts:<name>`
|
||||
- **Skills**: Modular with routing → `dist/codex/AGENTS.{name}.md`
|
||||
- Main `AGENTS.md` provides routing instructions
|
||||
- Tells Codex when to read which skill file
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### Why commit dist/?
|
||||
End users can copy files directly without needing build tools.
|
||||
|
||||
### Why separate transformers?
|
||||
- Each provider ~30-85 lines, easy to understand
|
||||
- Can modify one without affecting others
|
||||
- Easy to add new providers
|
||||
|
||||
### Why Bun?
|
||||
- Much faster than Node.js (2-4x)
|
||||
- All-in-one toolkit (runtime + package manager)
|
||||
- Zero config, TypeScript native
|
||||
- Node.js compatible (works with existing code)
|
||||
|
||||
### Why modular skills for Gemini/Codex?
|
||||
- Better context management (load only what's needed)
|
||||
- Cleaner file organization
|
||||
- Gemini: Uses native `@file.md` import feature
|
||||
- Codex: Uses routing pattern with AGENTS.md guide
|
||||
|
||||
## Adding New Content
|
||||
|
||||
1. **Create source file** in `source/commands/` or `source/skills/`
|
||||
2. **Add frontmatter** with name, description, args (for commands) or license (for skills)
|
||||
3. **Write body** with instructions/prompt
|
||||
4. **Build**: `bun run build`
|
||||
5. **Test** with your provider
|
||||
6. **Commit** both source and dist files
|
||||
|
||||
## Current Content
|
||||
|
||||
### Commands
|
||||
- **normalize**: Normalize design to match design system
|
||||
|
||||
### Skills
|
||||
- **frontend-design**: Create distinctive, production-grade frontend interfaces
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Source is truth**: Always edit `source/`, never edit `dist/` directly
|
||||
- **Test across providers**: Changes affect 4 different outputs
|
||||
- **Argument handling**: Write prompts that work with both placeholders and appending
|
||||
- **Cursor limitations**: No frontmatter/args, so design for graceful degradation
|
||||
|
||||
## Documentation
|
||||
|
||||
- **README.md**: End user guide (installation, usage)
|
||||
- **DEVELOP.md**: Contributor guide (architecture, build system, adding content)
|
||||
- **This file**: Context for AI assistants and new developers
|
||||
|
||||
## Provider Documentation Links
|
||||
|
||||
- [Cursor Commands](https://cursor.com/docs/agent/chat/commands)
|
||||
- [Cursor Rules](https://cursor.com/docs/context/rules)
|
||||
- [Claude Code Slash Commands](https://code.claude.com/docs/en/slash-commands)
|
||||
- [Anthropic Skills](https://github.com/anthropics/skills)
|
||||
- [Gemini CLI Custom Commands](https://cloud.google.com/blog/topics/developers-practitioners/gemini-cli-custom-slash-commands)
|
||||
- [Gemini CLI GEMINI.md](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/gemini-md.md)
|
||||
- [Codex CLI Slash Commands](https://developers.openai.com/codex/guides/slash-commands)
|
||||
- [Codex CLI AGENTS.md](https://developers.openai.com/codex/guides/agents-md)
|
||||
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
# Developer Guide
|
||||
|
||||
Documentation for contributors to vibe-design-plugins.
|
||||
|
||||
## Architecture
|
||||
|
||||
This repository uses a **feature-rich source format** that transforms into provider-specific formats. We chose "Option A" architecture: maintain full metadata in source files and downgrade for providers with limited support (like Cursor), rather than limiting everyone to the lowest common denominator.
|
||||
|
||||
### Why This Approach?
|
||||
|
||||
Different providers have different capabilities:
|
||||
- **Cursor**: No frontmatter or argument support
|
||||
- **Claude Code, Gemini, Codex**: Full support for metadata and arguments
|
||||
|
||||
By maintaining rich source files, we preserve maximum functionality where supported while still providing working (if simpler) versions for all providers.
|
||||
|
||||
## Source Format
|
||||
|
||||
### Commands (`source/commands/*.md`)
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: command-name
|
||||
description: What this command does
|
||||
args:
|
||||
- name: argname
|
||||
description: Argument description
|
||||
required: false
|
||||
---
|
||||
|
||||
Your command prompt here with {{argname}} placeholders...
|
||||
```
|
||||
|
||||
**Frontmatter fields**:
|
||||
- `name` (required): Command identifier
|
||||
- `description` (required): What the command does
|
||||
- `args` (optional): Array of argument objects
|
||||
- `name`: Argument identifier
|
||||
- `description`: What it's for
|
||||
- `required`: Boolean (defaults to false)
|
||||
|
||||
**Body**: The actual prompt. Use `{{argname}}` for argument placeholders (automatically transformed to provider-specific syntax).
|
||||
|
||||
### Skills (`source/skills/*.md`)
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: skill-name
|
||||
description: What this skill provides
|
||||
license: License info (optional)
|
||||
---
|
||||
|
||||
Your skill instructions here...
|
||||
```
|
||||
|
||||
**Frontmatter fields**:
|
||||
- `name` (required): Skill identifier
|
||||
- `description` (required): What the skill provides
|
||||
- `license` (optional): License/attribution info
|
||||
|
||||
**Body**: The skill instructions for the LLM.
|
||||
|
||||
## Building
|
||||
|
||||
### Prerequisites
|
||||
- Bun (fast JavaScript runtime and package manager)
|
||||
- No external dependencies required
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
# Build all provider formats
|
||||
bun run build
|
||||
|
||||
# Clean dist folder
|
||||
bun run clean
|
||||
|
||||
# Rebuild from scratch
|
||||
bun run rebuild
|
||||
```
|
||||
|
||||
### What Gets Generated
|
||||
|
||||
```
|
||||
source/ → dist/
|
||||
commands/*.md cursor/commands/*.md (body only)
|
||||
skills/*.md cursor/rules/*.md (body only)
|
||||
|
||||
claude-code/commands/*.md (full frontmatter)
|
||||
claude-code/skills/*/SKILL.md
|
||||
|
||||
gemini/commands/*.toml (TOML format)
|
||||
gemini/GEMINI*.md (modular)
|
||||
|
||||
codex/prompts/*.md (custom prompt format)
|
||||
codex/AGENTS*.md (modular)
|
||||
```
|
||||
|
||||
## Provider Transformations
|
||||
|
||||
### Cursor (Downgraded)
|
||||
- Strips ALL frontmatter
|
||||
- Body only → `dist/cursor/commands/*.md` and `dist/cursor/rules/*.md`
|
||||
- Argument placeholders remain as descriptive text (no substitution)
|
||||
|
||||
### Claude Code (Full Featured)
|
||||
- Keeps full YAML frontmatter + body
|
||||
- Commands → `dist/claude-code/commands/*.md`
|
||||
- Skills → `dist/claude-code/skills/{name}/SKILL.md`
|
||||
|
||||
### Gemini CLI (Full Featured)
|
||||
- Commands converted to TOML format → `dist/gemini/commands/*.toml`
|
||||
- `description` and `prompt` keys
|
||||
- Arguments converted to `{{args}}` (Gemini uses single args string)
|
||||
- Skills → Modular `GEMINI.{name}.md` files
|
||||
- Main `GEMINI.md` imports skill files using `@./GEMINI.{name}.md` syntax
|
||||
- Uses Gemini's native import feature for modular context files
|
||||
|
||||
### Codex CLI (Full Featured)
|
||||
- Commands → Custom prompts with `argument-hint` → `dist/codex/prompts/*.md`
|
||||
- Frontmatter uses `description` and `argument-hint` (not `args` array)
|
||||
- Placeholders transformed from `{{argname}}` to `$ARGNAME` (uppercase)
|
||||
- Invoked as `/prompts:<name>`
|
||||
- Skills → Modular `AGENTS.{name}.md` files
|
||||
- Main `AGENTS.md` guides Codex on when to read each skill file
|
||||
- Includes skill descriptions to help Codex decide which file to read
|
||||
|
||||
## Adding New Content
|
||||
|
||||
### 1. Create Source File
|
||||
|
||||
**For a command**:
|
||||
```bash
|
||||
# Create source/commands/mycommand.md
|
||||
touch source/commands/mycommand.md
|
||||
```
|
||||
|
||||
Add frontmatter and content following the format above.
|
||||
|
||||
**For a skill**:
|
||||
```bash
|
||||
# Create source/skills/myskill.md
|
||||
touch source/skills/myskill.md
|
||||
```
|
||||
|
||||
Add frontmatter and content following the format above.
|
||||
|
||||
### 2. Build
|
||||
|
||||
```bash
|
||||
bun run build
|
||||
```
|
||||
|
||||
This generates all 4 provider formats automatically.
|
||||
|
||||
### 3. Test
|
||||
|
||||
Test with your provider of choice to ensure it works correctly. Remember that Cursor will have limited functionality.
|
||||
|
||||
### 4. Commit
|
||||
|
||||
Commit both source and dist files:
|
||||
```bash
|
||||
git add source/ dist/
|
||||
git commit -m "Add [command/skill name]"
|
||||
```
|
||||
|
||||
**Important**: The `dist/` directory is committed intentionally so end users can use files without building.
|
||||
|
||||
## Build System Details
|
||||
|
||||
The build system (`scripts/build.js`) is a single 410-line Node.js script with:
|
||||
- Custom YAML frontmatter parser (no dependencies)
|
||||
- Provider-specific transformation functions
|
||||
- Automatic directory management
|
||||
- Zero external dependencies (pure Node.js)
|
||||
|
||||
### Key Functions
|
||||
|
||||
- `parseFrontmatter()`: Extracts YAML frontmatter and body
|
||||
- `readSourceFiles()`: Recursively reads source files
|
||||
- `transformCursor()`: Strips frontmatter for Cursor
|
||||
- `transformClaudeCode()`: Keeps full format
|
||||
- `transformGemini()`: Converts to TOML + modular skills
|
||||
- `transformCodex()`: Full format + modular skills
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Command Writing
|
||||
|
||||
1. **Clear descriptions**: Make purpose obvious
|
||||
2. **Meaningful argument names**: Use descriptive names
|
||||
3. **Flexible prompts**: Write prompts that work even without argument substitution (for Cursor compatibility)
|
||||
4. **Test across providers**: Verify it works in multiple contexts
|
||||
|
||||
### Skill Writing
|
||||
|
||||
1. **Focused scope**: One clear domain per skill
|
||||
2. **Clear instructions**: LLM should understand exactly what to do
|
||||
3. **Include examples**: Where they clarify intent
|
||||
4. **State constraints**: What NOT to do as clearly as what to do
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
- [Cursor Commands](https://cursor.com/docs/agent/chat/commands)
|
||||
- [Cursor Rules](https://cursor.com/docs/context/rules)
|
||||
- [Claude Code Slash Commands](https://code.claude.com/docs/en/slash-commands)
|
||||
- [Anthropic Skills (Claude Code)](https://github.com/anthropics/skills)
|
||||
- [Gemini CLI Custom Commands](https://cloud.google.com/blog/topics/developers-practitioners/gemini-cli-custom-slash-commands)
|
||||
- [Gemini CLI Skills](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/gemini-md.md)
|
||||
- [Codex CLI Slash Commands](https://developers.openai.com/codex/guides/slash-commands#create-your-own-slash-commands-with-custom-prompts)
|
||||
- [Codex CLI Agents](https://developers.openai.com/codex/guides/agents-md)
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
vibe-design-plugins/
|
||||
├── source/ # Edit these! Source of truth
|
||||
│ ├── commands/ # Command definitions
|
||||
│ │ └── normalize.md
|
||||
│ └── skills/ # Skill definitions
|
||||
│ └── frontend-design.md
|
||||
├── dist/ # Generated (committed for users)
|
||||
│ ├── cursor/
|
||||
│ ├── claude-code/
|
||||
│ ├── gemini/
|
||||
│ └── codex/
|
||||
├── scripts/
|
||||
│ └── build.js # Build system (410 lines, zero deps)
|
||||
├── package.json # ESM project config
|
||||
├── README.md # User documentation
|
||||
├── DEVELOP.md # This file
|
||||
└── .gitignore
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build fails with YAML parsing errors
|
||||
- Check frontmatter indentation (YAML is indent-sensitive)
|
||||
- Ensure `---` delimiters are on their own lines
|
||||
- Verify colons have spaces after them (`key: value`)
|
||||
|
||||
### Output doesn't match expectations
|
||||
- Check the transformer function for your provider in `scripts/build.js`
|
||||
- Verify source file has correct frontmatter structure
|
||||
- Run `npm run rebuild` to ensure clean build
|
||||
|
||||
### Provider doesn't recognize the files
|
||||
- Check installation path for your provider
|
||||
- Verify file naming matches provider requirements
|
||||
- Consult provider's documentation (links above)
|
||||
|
||||
## Questions?
|
||||
|
||||
Open an issue or submit a PR!
|
||||
|
||||
@@ -1,2 +1,130 @@
|
||||
# vibe-design-plugins
|
||||
Claude + Cursor plugins that make your LLM of choice great at UX / frontend design.
|
||||
|
||||
Cross-provider design skills and commands for LLM-powered development tools.
|
||||
|
||||
## What's Included
|
||||
|
||||
### Commands
|
||||
- **normalize** - Normalize your design to match your design system and ensure consistency
|
||||
|
||||
### Skills
|
||||
- **frontend-design** - Create distinctive, production-grade frontend interfaces with exceptional design quality
|
||||
|
||||
## Installation
|
||||
|
||||
Choose your provider and copy the files to the appropriate location:
|
||||
|
||||
### Cursor
|
||||
|
||||
Copy commands and rules to your project's `.cursor` directory:
|
||||
|
||||
```bash
|
||||
cp -r dist/cursor/commands/* .cursor/commands/
|
||||
cp -r dist/cursor/rules/* .cursor/rules/
|
||||
```
|
||||
|
||||
**Note**: Cursor doesn't support command arguments or frontmatter, so commands work but with simplified functionality.
|
||||
|
||||
**Reference**:
|
||||
- [Cursor Commands Documentation](https://cursor.com/docs/agent/chat/commands)
|
||||
- [Cursor Rules Documentation](https://cursor.com/docs/context/rules)
|
||||
|
||||
### Claude Code
|
||||
|
||||
Copy to your global Claude directory:
|
||||
|
||||
```bash
|
||||
cp -r dist/claude-code/commands/* ~/.claude/commands/
|
||||
cp -r dist/claude-code/skills/* ~/.claude/skills/
|
||||
```
|
||||
|
||||
Or for project-specific use, copy to `.claude/` in your project root.
|
||||
|
||||
**Reference**:
|
||||
- [Claude Code Slash Commands](https://code.claude.com/docs/en/slash-commands)
|
||||
- [Anthropic Skills Documentation](https://github.com/anthropics/skills)
|
||||
|
||||
### Gemini CLI
|
||||
|
||||
Copy commands and skill files to your project:
|
||||
|
||||
```bash
|
||||
# Commands (global for all projects)
|
||||
cp -r dist/gemini/commands/* ~/.gemini/commands/
|
||||
|
||||
# Skills (project-specific - place at your project root)
|
||||
cp dist/gemini/GEMINI*.md ~/your-project-root/
|
||||
```
|
||||
|
||||
**Note**:
|
||||
- Commands use `.toml` format with `{{args}}` placeholders
|
||||
- `GEMINI.md` uses `@file.md` import syntax to load modular skill files
|
||||
- Skills should be placed at your project root for project-specific context
|
||||
- For global skills, place `GEMINI.md` in `~/.gemini/`
|
||||
|
||||
**Reference**:
|
||||
- [Gemini CLI Custom Slash Commands](https://cloud.google.com/blog/topics/developers-practitioners/gemini-cli-custom-slash-commands)
|
||||
- [Gemini CLI Skills (GEMINI.md)](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/gemini-md.md)
|
||||
|
||||
### Codex CLI
|
||||
|
||||
Copy prompts and skill files:
|
||||
|
||||
```bash
|
||||
cp -r dist/codex/prompts/* ~/.codex/prompts/
|
||||
cp dist/codex/AGENTS*.md ~/your-project-root/
|
||||
```
|
||||
|
||||
**Note**:
|
||||
- Commands are invoked as `/prompts:<name>` (e.g., `/prompts:normalize`)
|
||||
- The `AGENTS.md` file guides Codex to read modular skill files as needed
|
||||
- Place `AGENTS.md` at your repository root
|
||||
|
||||
**Reference**:
|
||||
- [Codex CLI Slash Commands](https://developers.openai.com/codex/guides/slash-commands#create-your-own-slash-commands-with-custom-prompts)
|
||||
- [Codex CLI Skills (AGENTS.md)](https://developers.openai.com/codex/guides/agents-md)
|
||||
|
||||
## Usage
|
||||
|
||||
### Commands
|
||||
|
||||
**Cursor, Claude Code**:
|
||||
```
|
||||
/normalize
|
||||
```
|
||||
|
||||
**Gemini**:
|
||||
```
|
||||
/normalize <optional-feature-name>
|
||||
```
|
||||
|
||||
**Codex**:
|
||||
```
|
||||
/prompts:normalize
|
||||
/prompts:normalize FEATURE="dashboard"
|
||||
```
|
||||
|
||||
### Skills
|
||||
|
||||
Skills are automatically available once installed. Refer to your provider's documentation for how skills are activated:
|
||||
|
||||
- **Cursor**: Rules apply automatically to the context
|
||||
- **Claude Code**: Skills activated via `/skills` or `@skill-name`
|
||||
- **Gemini**: Gemini reads `GEMINI.md` and automatically imports referenced skill files using `@file.md` syntax
|
||||
- **Codex**: Codex reads `AGENTS.md` and automatically loads referenced skill files when needed
|
||||
|
||||
## Provider Comparison
|
||||
|
||||
| Feature | Cursor | Claude Code | Gemini CLI | Codex CLI |
|
||||
|---------|---------|-------------|------------|-----------|
|
||||
| Command Args | ❌ | ✅ | ✅ | ✅ |
|
||||
| Frontmatter | ❌ | ✅ | ✅ (TOML) | ✅ |
|
||||
| Modular Skills | ❌ | ❌ | ✅ | ✅ |
|
||||
|
||||
## Contributing
|
||||
|
||||
Want to add more design skills or commands? See [DEVELOP.md](DEVELOP.md) for contributor guidelines.
|
||||
|
||||
## License
|
||||
|
||||
See LICENSE file.
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: normalize
|
||||
description: Normalize design to match your design system and ensure consistency
|
||||
args:
|
||||
- name: feature
|
||||
description: The page, route, or feature to normalize (optional)
|
||||
required: false
|
||||
---
|
||||
|
||||
The page, route or feature (we'll call it feature from here on out) provided below or via automatic context looks and feels differently than others. Please do the following:
|
||||
|
||||
# Plan
|
||||
|
||||
1. Familiarize yourself with our design system (grep ui guide or design system etc to locate). Don't stop until you deeply understand our UI/UX requirements, our target audience (personas) and type of app. When something isn't immediately clear, ask me.
|
||||
2. Analyze the status quo of our feature. What works today, and what doesn't? Why? Assess the situation to see where the gaps are.
|
||||
3. Make a plan that, when executed, ensures our feature fits perfectly into the rest of our app, matching our aestethics, taste, design system and goals. Great design is effective design. Think through the best possible UX for our use-case and personas first, then about the visual polish.
|
||||
|
||||
# Execute
|
||||
Get to work and redesign the feature, in all areas that are still lacking. That could be typography, use of negative space and overall layout, progressive disclosure of sophistication, responsiveness, colors and gradients, motion design, reusing the right design tokens, class names and components, and thoughtful composition and use of established patterns. This is not an exhaustive list.
|
||||
|
||||
# Clean up
|
||||
- Ensure DRYness: If your choices led to new components that should be re-usable, find out if we have a shared UI component import path, and consolidate the new components there.
|
||||
- Delete any now unused or orphaned code or files when you're done.
|
||||
- This is probably a great time to lint and type-check and ensure we didn't break stuff. Follow the repo's overall guidelines on testing.
|
||||
|
||||
Remember: You are a brilliant frontend designer with impeccable taste, you're equally strong in UX and UI, and you are thorough and precise. Your attention of detail and eye for the end-to-end user experience is world class.
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||
|
||||
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||
|
||||
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||
|
||||
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||
|
||||
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||
|
||||
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||
|
||||
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||
|
||||
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||
|
||||
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
# Codex Agent Instructions
|
||||
|
||||
This repository contains specialized skills for different tasks. When the user requests work in a particular domain, read the corresponding skill file for detailed guidance.
|
||||
|
||||
## Available Skills
|
||||
|
||||
Each skill provides deep expertise in its domain. Use the descriptions below to decide which skill file to read:
|
||||
|
||||
### frontend-design
|
||||
|
||||
**When to use**: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
|
||||
|
||||
**Read**: `AGENTS.frontend-design.md` for complete instructions.
|
||||
|
||||
|
||||
## How to Use Skills
|
||||
|
||||
1. Identify the user's request domain
|
||||
2. Match it to a skill description above
|
||||
3. Read the corresponding skill file
|
||||
4. Follow the guidance in that file
|
||||
|
||||
Multiple skills can be combined when the task requires expertise from different domains.
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
---
|
||||
description: Normalize design to match your design system and ensure consistency
|
||||
argument-hint: [FEATURE=<value>]
|
||||
---
|
||||
|
||||
The page, route or feature (we'll call it feature from here on out) provided below or via automatic context looks and feels differently than others. Please do the following:
|
||||
|
||||
# Plan
|
||||
|
||||
1. Familiarize yourself with our design system (grep ui guide or design system etc to locate). Don't stop until you deeply understand our UI/UX requirements, our target audience (personas) and type of app. When something isn't immediately clear, ask me.
|
||||
2. Analyze the status quo of our feature. What works today, and what doesn't? Why? Assess the situation to see where the gaps are.
|
||||
3. Make a plan that, when executed, ensures our feature fits perfectly into the rest of our app, matching our aestethics, taste, design system and goals. Great design is effective design. Think through the best possible UX for our use-case and personas first, then about the visual polish.
|
||||
|
||||
# Execute
|
||||
Get to work and redesign the feature, in all areas that are still lacking. That could be typography, use of negative space and overall layout, progressive disclosure of sophistication, responsiveness, colors and gradients, motion design, reusing the right design tokens, class names and components, and thoughtful composition and use of established patterns. This is not an exhaustive list.
|
||||
|
||||
# Clean up
|
||||
- Ensure DRYness: If your choices led to new components that should be re-usable, find out if we have a shared UI component import path, and consolidate the new components there.
|
||||
- Delete any now unused or orphaned code or files when you're done.
|
||||
- This is probably a great time to lint and type-check and ensure we didn't break stuff. Follow the repo's overall guidelines on testing.
|
||||
|
||||
Remember: You are a brilliant frontend designer with impeccable taste, you're equally strong in UX and UI, and you are thorough and precise. Your attention of detail and eye for the end-to-end user experience is world class.
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
The page, route or feature (we'll call it feature from here on out) provided below or via automatic context looks and feels differently than others. Please do the following:
|
||||
|
||||
# Plan
|
||||
|
||||
1. Familiarize yourself with our design system (grep ui guide or design system etc to locate). Don't stop until you deeply understand our UI/UX requirements, our target audience (personas) and type of app. When something isn't immediately clear, ask me.
|
||||
2. Analyze the status quo of our feature. What works today, and what doesn't? Why? Assess the situation to see where the gaps are.
|
||||
3. Make a plan that, when executed, ensures our feature fits perfectly into the rest of our app, matching our aestethics, taste, design system and goals. Great design is effective design. Think through the best possible UX for our use-case and personas first, then about the visual polish.
|
||||
|
||||
# Execute
|
||||
Get to work and redesign the feature, in all areas that are still lacking. That could be typography, use of negative space and overall layout, progressive disclosure of sophistication, responsiveness, colors and gradients, motion design, reusing the right design tokens, class names and components, and thoughtful composition and use of established patterns. This is not an exhaustive list.
|
||||
|
||||
# Clean up
|
||||
- Ensure DRYness: If your choices led to new components that should be re-usable, find out if we have a shared UI component import path, and consolidate the new components there.
|
||||
- Delete any now unused or orphaned code or files when you're done.
|
||||
- This is probably a great time to lint and type-check and ensure we didn't break stuff. Follow the repo's overall guidelines on testing.
|
||||
|
||||
Remember: You are a brilliant frontend designer with impeccable taste, you're equally strong in UX and UI, and you are thorough and precise. Your attention of detail and eye for the end-to-end user experience is world class.
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||
|
||||
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||
|
||||
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||
|
||||
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||
|
||||
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||
|
||||
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||
|
||||
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||
|
||||
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||
|
||||
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Gemini Context
|
||||
|
||||
This repository contains specialized skills for different tasks. When you detect a user request in a particular domain, the corresponding skill file will be automatically loaded to provide detailed guidance.
|
||||
|
||||
## Available Skills
|
||||
|
||||
Each skill provides deep expertise in its domain. The skills below are automatically imported and will guide your responses:
|
||||
|
||||
### frontend-design
|
||||
|
||||
**When to use**: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
|
||||
|
||||
@./GEMINI.frontend-design.md
|
||||
|
||||
|
||||
## How Skills Work
|
||||
|
||||
1. Skills are automatically loaded via the import statements above
|
||||
2. When a user request matches a skill domain, apply that skill's guidance
|
||||
3. Multiple skills can be combined when the task requires expertise from different domains
|
||||
4. Follow the detailed instructions provided in each imported skill file
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
description = "Normalize design to match your design system and ensure consistency"
|
||||
prompt = """
|
||||
The page, route or feature (we'll call it feature from here on out) provided below or via automatic context looks and feels differently than others. Please do the following:
|
||||
|
||||
# Plan
|
||||
|
||||
1. Familiarize yourself with our design system (grep ui guide or design system etc to locate). Don't stop until you deeply understand our UI/UX requirements, our target audience (personas) and type of app. When something isn't immediately clear, ask me.
|
||||
2. Analyze the status quo of our feature. What works today, and what doesn't? Why? Assess the situation to see where the gaps are.
|
||||
3. Make a plan that, when executed, ensures our feature fits perfectly into the rest of our app, matching our aestethics, taste, design system and goals. Great design is effective design. Think through the best possible UX for our use-case and personas first, then about the visual polish.
|
||||
|
||||
# Execute
|
||||
Get to work and redesign the feature, in all areas that are still lacking. That could be typography, use of negative space and overall layout, progressive disclosure of sophistication, responsiveness, colors and gradients, motion design, reusing the right design tokens, class names and components, and thoughtful composition and use of established patterns. This is not an exhaustive list.
|
||||
|
||||
# Clean up
|
||||
- Ensure DRYness: If your choices led to new components that should be re-usable, find out if we have a shared UI component import path, and consolidate the new components there.
|
||||
- Delete any now unused or orphaned code or files when you're done.
|
||||
- This is probably a great time to lint and type-check and ensure we didn't break stuff. Follow the repo's overall guidelines on testing.
|
||||
|
||||
Remember: You are a brilliant frontend designer with impeccable taste, you're equally strong in UX and UI, and you are thorough and precise. Your attention of detail and eye for the end-to-end user experience is world class.
|
||||
"""
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "vibe-design-plugins",
|
||||
"version": "1.0.0",
|
||||
"description": "Cross-provider design skills and commands for LLM-powered development tools",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "bun run scripts/build.js",
|
||||
"clean": "rm -rf dist",
|
||||
"rebuild": "bun run clean && bun run build",
|
||||
"test": "bun test"
|
||||
},
|
||||
"keywords": [
|
||||
"cursor",
|
||||
"claude",
|
||||
"gemini",
|
||||
"codex",
|
||||
"design",
|
||||
"frontend",
|
||||
"ux"
|
||||
],
|
||||
"author": "Paul Bakaus",
|
||||
"license": "SEE LICENSE FILE"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Build System for Cross-Provider Design Skills & Commands
|
||||
*
|
||||
* Transforms feature-rich source files into provider-specific formats:
|
||||
* - Cursor: Downgraded (no frontmatter/args)
|
||||
* - Claude Code: Full featured (frontmatter + body)
|
||||
* - Gemini: Full featured (TOML + modular skills)
|
||||
* - Codex: Full featured (custom prompts + modular skills)
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { readSourceFiles } from './lib/utils.js';
|
||||
import {
|
||||
transformCursor,
|
||||
transformClaudeCode,
|
||||
transformGemini,
|
||||
transformCodex
|
||||
} from './lib/transformers/index.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const ROOT_DIR = path.resolve(__dirname, '..');
|
||||
const DIST_DIR = path.join(ROOT_DIR, 'dist');
|
||||
|
||||
/**
|
||||
* Main build process
|
||||
*/
|
||||
function build() {
|
||||
console.log('🔨 Building cross-provider design plugins...\n');
|
||||
|
||||
// Read source files
|
||||
const { commands, skills } = readSourceFiles(ROOT_DIR);
|
||||
console.log(`📖 Read ${commands.length} commands and ${skills.length} skills\n`);
|
||||
|
||||
// Transform for each provider
|
||||
transformCursor(commands, skills, DIST_DIR);
|
||||
transformClaudeCode(commands, skills, DIST_DIR);
|
||||
transformGemini(commands, skills, DIST_DIR);
|
||||
transformCodex(commands, skills, DIST_DIR);
|
||||
|
||||
console.log('\n✨ Build complete!');
|
||||
}
|
||||
|
||||
// Run the build
|
||||
build();
|
||||
@@ -0,0 +1,48 @@
|
||||
import path from 'path';
|
||||
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Claude Code Transformer (Full Featured)
|
||||
*
|
||||
* Keeps full YAML frontmatter with args support.
|
||||
* Skills stored in subdirectories with SKILL.md filename.
|
||||
*/
|
||||
export function transformClaudeCode(commands, skills, distDir) {
|
||||
const commandsDir = path.join(distDir, 'claude-code/commands');
|
||||
const skillsDir = path.join(distDir, 'claude-code/skills');
|
||||
|
||||
cleanDir(path.join(distDir, 'claude-code'));
|
||||
ensureDir(commandsDir);
|
||||
ensureDir(skillsDir);
|
||||
|
||||
// Commands: Keep frontmatter + body
|
||||
for (const command of commands) {
|
||||
const frontmatter = generateYamlFrontmatter({
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
...(command.args.length > 0 && { args: command.args })
|
||||
});
|
||||
|
||||
const content = `${frontmatter}\n\n${command.body}`;
|
||||
const outputPath = path.join(commandsDir, `${command.name}.md`);
|
||||
writeFile(outputPath, content);
|
||||
}
|
||||
|
||||
// Skills: Keep frontmatter + body in subdirectories
|
||||
for (const skill of skills) {
|
||||
const skillDir = path.join(skillsDir, skill.name);
|
||||
|
||||
const frontmatter = generateYamlFrontmatter({
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
...(skill.license && { license: skill.license })
|
||||
});
|
||||
|
||||
const content = `${frontmatter}\n\n${skill.body}`;
|
||||
const outputPath = path.join(skillDir, 'SKILL.md');
|
||||
writeFile(outputPath, content);
|
||||
}
|
||||
|
||||
console.log(`✓ Claude Code: ${commands.length} commands, ${skills.length} skills`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import path from 'path';
|
||||
import { cleanDir, ensureDir, writeFile } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Codex Transformer (Full Featured - Custom Prompts + Modular Skills)
|
||||
*
|
||||
* Commands: Uses argument-hint format with $VARIABLE placeholders
|
||||
* Skills: Creates modular files with guiding AGENTS.md
|
||||
*/
|
||||
export function transformCodex(commands, skills, distDir) {
|
||||
const codexDir = path.join(distDir, 'codex');
|
||||
const promptsDir = path.join(codexDir, 'prompts');
|
||||
|
||||
cleanDir(codexDir);
|
||||
ensureDir(promptsDir);
|
||||
|
||||
// Commands: Transform to Codex prompt format
|
||||
for (const command of commands) {
|
||||
const yamlLines = ['---'];
|
||||
yamlLines.push(`description: ${command.description}`);
|
||||
|
||||
// Build argument-hint from args array
|
||||
if (command.args && command.args.length > 0) {
|
||||
const hints = command.args.map(arg => {
|
||||
const hint = arg.required ? `<${arg.name}>` : `[${arg.name.toUpperCase()}=<value>]`;
|
||||
return hint;
|
||||
});
|
||||
yamlLines.push(`argument-hint: ${hints.join(' ')}`);
|
||||
}
|
||||
|
||||
yamlLines.push('---');
|
||||
|
||||
// Transform {{argname}} to $ARGNAME for Codex
|
||||
let body = command.body;
|
||||
body = body.replace(/\{\{([^}]+)\}\}/g, (match, argName) => {
|
||||
return `$${argName.toUpperCase()}`;
|
||||
});
|
||||
|
||||
const content = `${yamlLines.join('\n')}\n\n${body}`;
|
||||
const outputPath = path.join(promptsDir, `${command.name}.md`);
|
||||
writeFile(outputPath, content);
|
||||
}
|
||||
|
||||
// Skills: Create modular files (body only)
|
||||
const skillEntries = [];
|
||||
for (const skill of skills) {
|
||||
const outputPath = path.join(codexDir, `AGENTS.${skill.name}.md`);
|
||||
writeFile(outputPath, skill.body);
|
||||
skillEntries.push({
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
file: `AGENTS.${skill.name}.md`
|
||||
});
|
||||
}
|
||||
|
||||
// Create main AGENTS.md that guides Codex to the right skill files
|
||||
const agentsMd = [
|
||||
'# Codex Agent Instructions',
|
||||
'',
|
||||
'This repository contains specialized skills for different tasks. When the user requests work in a particular domain, read the corresponding skill file for detailed guidance.',
|
||||
'',
|
||||
'## Available Skills',
|
||||
'',
|
||||
'Each skill provides deep expertise in its domain. Use the descriptions below to decide which skill file to read:',
|
||||
'',
|
||||
...skillEntries.map(skill =>
|
||||
`### ${skill.name}\n\n**When to use**: ${skill.description}\n\n**Read**: \`${skill.file}\` for complete instructions.\n`
|
||||
),
|
||||
'',
|
||||
'## How to Use Skills',
|
||||
'',
|
||||
'1. Identify the user\'s request domain',
|
||||
'2. Match it to a skill description above',
|
||||
'3. Read the corresponding skill file',
|
||||
'4. Follow the guidance in that file',
|
||||
'',
|
||||
'Multiple skills can be combined when the task requires expertise from different domains.'
|
||||
].join('\n');
|
||||
|
||||
writeFile(path.join(codexDir, 'AGENTS.md'), agentsMd);
|
||||
|
||||
console.log(`✓ Codex: ${commands.length} prompts, ${skills.length} skills (modular)`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import path from 'path';
|
||||
import { cleanDir, ensureDir, writeFile } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Cursor Transformer (Downgraded - No Frontmatter/Args)
|
||||
*
|
||||
* Strips all frontmatter and metadata, outputs body only.
|
||||
* Cursor doesn't support arguments or frontmatter.
|
||||
*/
|
||||
export function transformCursor(commands, skills, distDir) {
|
||||
const commandsDir = path.join(distDir, 'cursor/commands');
|
||||
const rulesDir = path.join(distDir, 'cursor/rules');
|
||||
|
||||
cleanDir(path.join(distDir, 'cursor'));
|
||||
ensureDir(commandsDir);
|
||||
ensureDir(rulesDir);
|
||||
|
||||
// Commands: Body only (no frontmatter)
|
||||
for (const command of commands) {
|
||||
const outputPath = path.join(commandsDir, `${command.name}.md`);
|
||||
writeFile(outputPath, command.body);
|
||||
}
|
||||
|
||||
// Skills: Body only (no frontmatter)
|
||||
for (const skill of skills) {
|
||||
const outputPath = path.join(rulesDir, `${skill.name}.md`);
|
||||
writeFile(outputPath, skill.body);
|
||||
}
|
||||
|
||||
console.log(`✓ Cursor: ${commands.length} commands, ${skills.length} skills (downgraded)`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import path from 'path';
|
||||
import { cleanDir, ensureDir, writeFile } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Gemini Transformer (Full Featured - TOML + Modular Skills)
|
||||
*
|
||||
* Commands: Converts to TOML format with {{args}} placeholders
|
||||
* Skills: Creates modular files imported via @./GEMINI.{name}.md syntax
|
||||
*/
|
||||
export function transformGemini(commands, skills, distDir) {
|
||||
const geminiDir = path.join(distDir, 'gemini');
|
||||
const commandsDir = path.join(geminiDir, 'commands');
|
||||
|
||||
cleanDir(geminiDir);
|
||||
ensureDir(commandsDir);
|
||||
|
||||
// Commands: Transform to TOML
|
||||
for (const command of commands) {
|
||||
// Replace named placeholders with {{args}}
|
||||
let prompt = command.body.replace(/\{\{[^}]+\}\}/g, '{{args}}');
|
||||
|
||||
const toml = [
|
||||
`description = "${command.description.replace(/"/g, '\\"')}"`,
|
||||
`prompt = """`,
|
||||
prompt,
|
||||
`"""`
|
||||
].join('\n');
|
||||
|
||||
const outputPath = path.join(commandsDir, `${command.name}.toml`);
|
||||
writeFile(outputPath, toml);
|
||||
}
|
||||
|
||||
// Skills: Create modular files
|
||||
for (const skill of skills) {
|
||||
const outputPath = path.join(geminiDir, `GEMINI.${skill.name}.md`);
|
||||
writeFile(outputPath, skill.body);
|
||||
}
|
||||
|
||||
// Create main GEMINI.md that imports skill files
|
||||
const geminiMd = [
|
||||
'# Gemini Context',
|
||||
'',
|
||||
'This repository contains specialized skills for different tasks. When you detect a user request in a particular domain, the corresponding skill file will be automatically loaded to provide detailed guidance.',
|
||||
'',
|
||||
'## Available Skills',
|
||||
'',
|
||||
'Each skill provides deep expertise in its domain. The skills below are automatically imported and will guide your responses:',
|
||||
'',
|
||||
...skills.map(skill =>
|
||||
`### ${skill.name}\n\n**When to use**: ${skill.description}\n\n@./GEMINI.${skill.name}.md\n`
|
||||
),
|
||||
'',
|
||||
'## How Skills Work',
|
||||
'',
|
||||
'1. Skills are automatically loaded via the import statements above',
|
||||
'2. When a user request matches a skill domain, apply that skill\'s guidance',
|
||||
'3. Multiple skills can be combined when the task requires expertise from different domains',
|
||||
'4. Follow the detailed instructions provided in each imported skill file'
|
||||
].join('\n');
|
||||
|
||||
writeFile(path.join(geminiDir, 'GEMINI.md'), geminiMd);
|
||||
|
||||
console.log(`✓ Gemini: ${commands.length} commands (TOML), ${skills.length} skills (modular)`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export { transformCursor } from './cursor.js';
|
||||
export { transformClaudeCode } from './claude-code.js';
|
||||
export { transformGemini } from './gemini.js';
|
||||
export { transformCodex } from './codex.js';
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Parse frontmatter from markdown content
|
||||
* Returns { frontmatter: object, body: string }
|
||||
*/
|
||||
export function parseFrontmatter(content) {
|
||||
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
||||
const match = content.match(frontmatterRegex);
|
||||
|
||||
if (!match) {
|
||||
return { frontmatter: {}, body: content };
|
||||
}
|
||||
|
||||
const [, frontmatterText, body] = match;
|
||||
const frontmatter = {};
|
||||
|
||||
// Simple YAML parser (handles basic key-value and arrays)
|
||||
const lines = frontmatterText.split('\n');
|
||||
let currentKey = null;
|
||||
let currentArray = null;
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
|
||||
// Calculate indent level
|
||||
const leadingSpaces = line.length - line.trimStart().length;
|
||||
const trimmed = line.trim();
|
||||
|
||||
// Array item at level 2 (nested under a key)
|
||||
if (trimmed.startsWith('- ') && leadingSpaces >= 2) {
|
||||
if (currentArray) {
|
||||
if (trimmed.startsWith('- name:')) {
|
||||
// New object in array
|
||||
const obj = {};
|
||||
obj.name = trimmed.slice(7).trim();
|
||||
currentArray.push(obj);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Property of array object (indented further)
|
||||
if (leadingSpaces >= 4 && currentArray && currentArray.length > 0) {
|
||||
const colonIndex = trimmed.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
const key = trimmed.slice(0, colonIndex).trim();
|
||||
const value = trimmed.slice(colonIndex + 1).trim();
|
||||
const lastObj = currentArray[currentArray.length - 1];
|
||||
lastObj[key] = value === 'true' ? true : value === 'false' ? false : value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Top-level key-value pair
|
||||
if (leadingSpaces === 0) {
|
||||
const colonIndex = trimmed.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
const key = trimmed.slice(0, colonIndex).trim();
|
||||
const value = trimmed.slice(colonIndex + 1).trim();
|
||||
|
||||
if (value) {
|
||||
frontmatter[key] = value;
|
||||
currentKey = key;
|
||||
currentArray = null;
|
||||
} else {
|
||||
// Start of array
|
||||
currentKey = key;
|
||||
currentArray = [];
|
||||
frontmatter[key] = currentArray;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { frontmatter, body: body.trim() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively read all .md files from a directory
|
||||
*/
|
||||
export function readFilesRecursive(dir, fileList = []) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
return fileList;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(dir);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dir, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
readFilesRecursive(filePath, fileList);
|
||||
} else if (file.endsWith('.md')) {
|
||||
fileList.push(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
return fileList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse all source files
|
||||
*/
|
||||
export function readSourceFiles(rootDir) {
|
||||
const commandsDir = path.join(rootDir, 'source/commands');
|
||||
const skillsDir = path.join(rootDir, 'source/skills');
|
||||
|
||||
const commandFiles = readFilesRecursive(commandsDir);
|
||||
const skillFiles = readFilesRecursive(skillsDir);
|
||||
|
||||
const commands = commandFiles.map(filePath => {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const { frontmatter, body } = parseFrontmatter(content);
|
||||
const name = path.basename(filePath, '.md');
|
||||
|
||||
return {
|
||||
name: frontmatter.name || name,
|
||||
description: frontmatter.description || '',
|
||||
args: frontmatter.args || [],
|
||||
body,
|
||||
filePath
|
||||
};
|
||||
});
|
||||
|
||||
const skills = skillFiles.map(filePath => {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const { frontmatter, body } = parseFrontmatter(content);
|
||||
const name = path.basename(filePath, '.md');
|
||||
|
||||
return {
|
||||
name: frontmatter.name || name,
|
||||
description: frontmatter.description || '',
|
||||
license: frontmatter.license || '',
|
||||
body,
|
||||
filePath
|
||||
};
|
||||
});
|
||||
|
||||
return { commands, skills };
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure directory exists, create if needed
|
||||
*/
|
||||
export function ensureDir(dirPath) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean directory (remove all contents)
|
||||
*/
|
||||
export function cleanDir(dirPath) {
|
||||
if (fs.existsSync(dirPath)) {
|
||||
fs.rmSync(dirPath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write file with automatic directory creation
|
||||
*/
|
||||
export function writeFile(filePath, content) {
|
||||
const dir = path.dirname(filePath);
|
||||
ensureDir(dir);
|
||||
fs.writeFileSync(filePath, content, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate YAML frontmatter string
|
||||
*/
|
||||
export function generateYamlFrontmatter(data) {
|
||||
const lines = ['---'];
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (Array.isArray(value)) {
|
||||
lines.push(`${key}:`);
|
||||
for (const item of value) {
|
||||
if (typeof item === 'object') {
|
||||
lines.push(` - name: ${item.name}`);
|
||||
if (item.description) lines.push(` description: ${item.description}`);
|
||||
if (item.required !== undefined) lines.push(` required: ${item.required}`);
|
||||
} else {
|
||||
lines.push(` - ${item}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lines.push(`${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push('---');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: normalize
|
||||
description: Normalize design to match your design system and ensure consistency
|
||||
args:
|
||||
- name: feature
|
||||
description: The page, route, or feature to normalize (optional)
|
||||
required: false
|
||||
---
|
||||
|
||||
The page, route or feature (we'll call it feature from here on out) provided below or via automatic context looks and feels differently than others. Please do the following:
|
||||
|
||||
# Plan
|
||||
|
||||
1. Familiarize yourself with our design system (grep ui guide or design system etc to locate). Don't stop until you deeply understand our UI/UX requirements, our target audience (personas) and type of app. When something isn't immediately clear, ask me.
|
||||
2. Analyze the status quo of our feature. What works today, and what doesn't? Why? Assess the situation to see where the gaps are.
|
||||
3. Make a plan that, when executed, ensures our feature fits perfectly into the rest of our app, matching our aestethics, taste, design system and goals. Great design is effective design. Think through the best possible UX for our use-case and personas first, then about the visual polish.
|
||||
|
||||
# Execute
|
||||
Get to work and redesign the feature, in all areas that are still lacking. That could be typography, use of negative space and overall layout, progressive disclosure of sophistication, responsiveness, colors and gradients, motion design, reusing the right design tokens, class names and components, and thoughtful composition and use of established patterns. This is not an exhaustive list.
|
||||
|
||||
# Clean up
|
||||
- Ensure DRYness: If your choices led to new components that should be re-usable, find out if we have a shared UI component import path, and consolidate the new components there.
|
||||
- Delete any now unused or orphaned code or files when you're done.
|
||||
- This is probably a great time to lint and type-check and ensure we didn't break stuff. Follow the repo's overall guidelines on testing.
|
||||
|
||||
Remember: You are a brilliant frontend designer with impeccable taste, you're equally strong in UX and UI, and you are thorough and precise. Your attention of detail and eye for the end-to-end user experience is world class.
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||
|
||||
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||
|
||||
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||
|
||||
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||
|
||||
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
import { describe, test, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import * as utils from '../scripts/lib/utils.js';
|
||||
import * as transformers from '../scripts/lib/transformers/index.js';
|
||||
|
||||
const TEST_DIR = path.join(process.cwd(), 'test-tmp-build');
|
||||
|
||||
describe('build orchestration', () => {
|
||||
beforeEach(() => {
|
||||
if (fs.existsSync(TEST_DIR)) {
|
||||
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(TEST_DIR, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (fs.existsSync(TEST_DIR)) {
|
||||
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('should call readSourceFiles with root directory', () => {
|
||||
const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({
|
||||
commands: [],
|
||||
skills: []
|
||||
});
|
||||
|
||||
const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => {});
|
||||
const transformClaudeCodeSpy = spyOn(transformers, 'transformClaudeCode').mockImplementation(() => {});
|
||||
const transformGeminiSpy = spyOn(transformers, 'transformGemini').mockImplementation(() => {});
|
||||
const transformCodexSpy = spyOn(transformers, 'transformCodex').mockImplementation(() => {});
|
||||
|
||||
// Simulate the build process
|
||||
const ROOT_DIR = TEST_DIR;
|
||||
const DIST_DIR = path.join(ROOT_DIR, 'dist');
|
||||
|
||||
const { commands, skills } = utils.readSourceFiles(ROOT_DIR);
|
||||
transformers.transformCursor(commands, skills, DIST_DIR);
|
||||
transformers.transformClaudeCode(commands, skills, DIST_DIR);
|
||||
transformers.transformGemini(commands, skills, DIST_DIR);
|
||||
transformers.transformCodex(commands, skills, DIST_DIR);
|
||||
|
||||
expect(readSourceFilesSpy).toHaveBeenCalledWith(ROOT_DIR);
|
||||
|
||||
readSourceFilesSpy.mockRestore();
|
||||
transformCursorSpy.mockRestore();
|
||||
transformClaudeCodeSpy.mockRestore();
|
||||
transformGeminiSpy.mockRestore();
|
||||
transformCodexSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('should call all four transformers with correct arguments', () => {
|
||||
const commands = [
|
||||
{ name: 'cmd1', description: 'Command 1', args: [], body: 'Body 1' }
|
||||
];
|
||||
const skills = [
|
||||
{ name: 'skill1', description: 'Skill 1', license: 'MIT', body: 'Skill body 1' }
|
||||
];
|
||||
|
||||
const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({
|
||||
commands,
|
||||
skills
|
||||
});
|
||||
|
||||
const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => {});
|
||||
const transformClaudeCodeSpy = spyOn(transformers, 'transformClaudeCode').mockImplementation(() => {});
|
||||
const transformGeminiSpy = spyOn(transformers, 'transformGemini').mockImplementation(() => {});
|
||||
const transformCodexSpy = spyOn(transformers, 'transformCodex').mockImplementation(() => {});
|
||||
|
||||
const ROOT_DIR = TEST_DIR;
|
||||
const DIST_DIR = path.join(ROOT_DIR, 'dist');
|
||||
|
||||
const sourceFiles = utils.readSourceFiles(ROOT_DIR);
|
||||
transformers.transformCursor(sourceFiles.commands, sourceFiles.skills, DIST_DIR);
|
||||
transformers.transformClaudeCode(sourceFiles.commands, sourceFiles.skills, DIST_DIR);
|
||||
transformers.transformGemini(sourceFiles.commands, sourceFiles.skills, DIST_DIR);
|
||||
transformers.transformCodex(sourceFiles.commands, sourceFiles.skills, DIST_DIR);
|
||||
|
||||
expect(transformCursorSpy).toHaveBeenCalledWith(commands, skills, DIST_DIR);
|
||||
expect(transformClaudeCodeSpy).toHaveBeenCalledWith(commands, skills, DIST_DIR);
|
||||
expect(transformGeminiSpy).toHaveBeenCalledWith(commands, skills, DIST_DIR);
|
||||
expect(transformCodexSpy).toHaveBeenCalledWith(commands, skills, DIST_DIR);
|
||||
|
||||
readSourceFilesSpy.mockRestore();
|
||||
transformCursorSpy.mockRestore();
|
||||
transformClaudeCodeSpy.mockRestore();
|
||||
transformGeminiSpy.mockRestore();
|
||||
transformCodexSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('should handle empty source files', () => {
|
||||
const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({
|
||||
commands: [],
|
||||
skills: []
|
||||
});
|
||||
|
||||
const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => {});
|
||||
const transformClaudeCodeSpy = spyOn(transformers, 'transformClaudeCode').mockImplementation(() => {});
|
||||
const transformGeminiSpy = spyOn(transformers, 'transformGemini').mockImplementation(() => {});
|
||||
const transformCodexSpy = spyOn(transformers, 'transformCodex').mockImplementation(() => {});
|
||||
|
||||
const ROOT_DIR = TEST_DIR;
|
||||
const DIST_DIR = path.join(ROOT_DIR, 'dist');
|
||||
|
||||
const { commands, skills } = utils.readSourceFiles(ROOT_DIR);
|
||||
transformers.transformCursor(commands, skills, DIST_DIR);
|
||||
transformers.transformClaudeCode(commands, skills, DIST_DIR);
|
||||
transformers.transformGemini(commands, skills, DIST_DIR);
|
||||
transformers.transformCodex(commands, skills, DIST_DIR);
|
||||
|
||||
expect(transformCursorSpy).toHaveBeenCalledWith([], [], DIST_DIR);
|
||||
expect(transformClaudeCodeSpy).toHaveBeenCalledWith([], [], DIST_DIR);
|
||||
expect(transformGeminiSpy).toHaveBeenCalledWith([], [], DIST_DIR);
|
||||
expect(transformCodexSpy).toHaveBeenCalledWith([], [], DIST_DIR);
|
||||
|
||||
readSourceFilesSpy.mockRestore();
|
||||
transformCursorSpy.mockRestore();
|
||||
transformClaudeCodeSpy.mockRestore();
|
||||
transformGeminiSpy.mockRestore();
|
||||
transformCodexSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('integration: full build creates all expected outputs', () => {
|
||||
// Create test source files
|
||||
const commandContent = `---
|
||||
name: test-command
|
||||
description: A test command
|
||||
args:
|
||||
- name: target
|
||||
description: Target parameter
|
||||
required: false
|
||||
---
|
||||
|
||||
This is a test command body with {{target}} placeholder.`;
|
||||
|
||||
const skillContent = `---
|
||||
name: test-skill
|
||||
description: A test skill
|
||||
license: MIT
|
||||
---
|
||||
|
||||
This is a test skill body.`;
|
||||
|
||||
utils.writeFile(path.join(TEST_DIR, 'source/commands/test-command.md'), commandContent);
|
||||
utils.writeFile(path.join(TEST_DIR, 'source/skills/test-skill.md'), skillContent);
|
||||
|
||||
// Run the build process
|
||||
const DIST_DIR = path.join(TEST_DIR, 'dist');
|
||||
const { commands, skills } = utils.readSourceFiles(TEST_DIR);
|
||||
|
||||
transformers.transformCursor(commands, skills, DIST_DIR);
|
||||
transformers.transformClaudeCode(commands, skills, DIST_DIR);
|
||||
transformers.transformGemini(commands, skills, DIST_DIR);
|
||||
transformers.transformCodex(commands, skills, DIST_DIR);
|
||||
|
||||
// Verify Cursor outputs
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'cursor/commands/test-command.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'cursor/rules/test-skill.md'))).toBe(true);
|
||||
|
||||
// Verify Claude Code outputs
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'claude-code/commands/test-command.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'claude-code/skills/test-skill/SKILL.md'))).toBe(true);
|
||||
|
||||
// Verify Gemini outputs
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'gemini/commands/test-command.toml'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'gemini/GEMINI.test-skill.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'gemini/GEMINI.md'))).toBe(true);
|
||||
|
||||
// Verify Codex outputs
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'codex/prompts/test-command.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'codex/AGENTS.test-skill.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'codex/AGENTS.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('integration: verify transformations are correct', () => {
|
||||
const commandContent = `---
|
||||
name: normalize
|
||||
description: Normalize design
|
||||
args:
|
||||
- name: target
|
||||
description: Target element
|
||||
required: false
|
||||
---
|
||||
|
||||
Please normalize {{target}} to match the design system.`;
|
||||
|
||||
utils.writeFile(path.join(TEST_DIR, 'source/commands/normalize.md'), commandContent);
|
||||
|
||||
const DIST_DIR = path.join(TEST_DIR, 'dist');
|
||||
const { commands, skills } = utils.readSourceFiles(TEST_DIR);
|
||||
|
||||
transformers.transformCursor(commands, skills, DIST_DIR);
|
||||
transformers.transformClaudeCode(commands, skills, DIST_DIR);
|
||||
transformers.transformGemini(commands, skills, DIST_DIR);
|
||||
transformers.transformCodex(commands, skills, DIST_DIR);
|
||||
|
||||
// Verify Cursor: body only, no frontmatter
|
||||
const cursorContent = fs.readFileSync(path.join(DIST_DIR, 'cursor/commands/normalize.md'), 'utf-8');
|
||||
expect(cursorContent).not.toContain('---');
|
||||
expect(cursorContent).toContain('{{target}}');
|
||||
|
||||
// Verify Claude Code: full frontmatter
|
||||
const claudeContent = fs.readFileSync(path.join(DIST_DIR, 'claude-code/commands/normalize.md'), 'utf-8');
|
||||
expect(claudeContent).toContain('---');
|
||||
expect(claudeContent).toContain('name: normalize');
|
||||
expect(claudeContent).toContain('{{target}}');
|
||||
|
||||
// Verify Gemini: TOML with {{args}}
|
||||
const geminiContent = fs.readFileSync(path.join(DIST_DIR, 'gemini/commands/normalize.toml'), 'utf-8');
|
||||
expect(geminiContent).toContain('description = "Normalize design"');
|
||||
expect(geminiContent).toContain('{{args}}');
|
||||
expect(geminiContent).not.toContain('{{target}}');
|
||||
|
||||
// Verify Codex: $VARIABLE
|
||||
const codexContent = fs.readFileSync(path.join(DIST_DIR, 'codex/prompts/normalize.md'), 'utf-8');
|
||||
expect(codexContent).toContain('$TARGET');
|
||||
expect(codexContent).not.toContain('{{target}}');
|
||||
});
|
||||
|
||||
test('integration: multiple commands and skills', () => {
|
||||
utils.writeFile(path.join(TEST_DIR, 'source/commands/cmd1.md'), '---\nname: cmd1\n---\nBody1');
|
||||
utils.writeFile(path.join(TEST_DIR, 'source/commands/cmd2.md'), '---\nname: cmd2\n---\nBody2');
|
||||
utils.writeFile(path.join(TEST_DIR, 'source/skills/skill1.md'), '---\nname: skill1\n---\nSkill1');
|
||||
utils.writeFile(path.join(TEST_DIR, 'source/skills/skill2.md'), '---\nname: skill2\n---\nSkill2');
|
||||
|
||||
const DIST_DIR = path.join(TEST_DIR, 'dist');
|
||||
const { commands, skills } = utils.readSourceFiles(TEST_DIR);
|
||||
|
||||
expect(commands).toHaveLength(2);
|
||||
expect(skills).toHaveLength(2);
|
||||
|
||||
transformers.transformCursor(commands, skills, DIST_DIR);
|
||||
transformers.transformClaudeCode(commands, skills, DIST_DIR);
|
||||
transformers.transformGemini(commands, skills, DIST_DIR);
|
||||
transformers.transformCodex(commands, skills, DIST_DIR);
|
||||
|
||||
// Verify all files exist
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'cursor/commands/cmd1.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'cursor/commands/cmd2.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'cursor/rules/skill1.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'cursor/rules/skill2.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should call transformers in correct order', () => {
|
||||
const callOrder = [];
|
||||
|
||||
const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({
|
||||
commands: [],
|
||||
skills: []
|
||||
});
|
||||
|
||||
const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => {
|
||||
callOrder.push('cursor');
|
||||
});
|
||||
const transformClaudeCodeSpy = spyOn(transformers, 'transformClaudeCode').mockImplementation(() => {
|
||||
callOrder.push('claude-code');
|
||||
});
|
||||
const transformGeminiSpy = spyOn(transformers, 'transformGemini').mockImplementation(() => {
|
||||
callOrder.push('gemini');
|
||||
});
|
||||
const transformCodexSpy = spyOn(transformers, 'transformCodex').mockImplementation(() => {
|
||||
callOrder.push('codex');
|
||||
});
|
||||
|
||||
const ROOT_DIR = TEST_DIR;
|
||||
const DIST_DIR = path.join(ROOT_DIR, 'dist');
|
||||
|
||||
const { commands, skills } = utils.readSourceFiles(ROOT_DIR);
|
||||
transformers.transformCursor(commands, skills, DIST_DIR);
|
||||
transformers.transformClaudeCode(commands, skills, DIST_DIR);
|
||||
transformers.transformGemini(commands, skills, DIST_DIR);
|
||||
transformers.transformCodex(commands, skills, DIST_DIR);
|
||||
|
||||
expect(callOrder).toEqual(['cursor', 'claude-code', 'gemini', 'codex']);
|
||||
|
||||
readSourceFilesSpy.mockRestore();
|
||||
transformCursorSpy.mockRestore();
|
||||
transformClaudeCodeSpy.mockRestore();
|
||||
transformGeminiSpy.mockRestore();
|
||||
transformCodexSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { transformClaudeCode } from '../../../scripts/lib/transformers/claude-code.js';
|
||||
import { parseFrontmatter } from '../../../scripts/lib/utils.js';
|
||||
|
||||
const TEST_DIR = path.join(process.cwd(), 'test-tmp-claude');
|
||||
|
||||
describe('transformClaudeCode', () => {
|
||||
beforeEach(() => {
|
||||
if (fs.existsSync(TEST_DIR)) {
|
||||
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (fs.existsSync(TEST_DIR)) {
|
||||
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('should create correct directory structure', () => {
|
||||
const commands = [];
|
||||
const skills = [];
|
||||
|
||||
transformClaudeCode(commands, skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/commands'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/skills'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should preserve full frontmatter for commands', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'test-command',
|
||||
description: 'A test command',
|
||||
args: [
|
||||
{ name: 'target', description: 'The target', required: false },
|
||||
{ name: 'output', description: 'Output format', required: true }
|
||||
],
|
||||
body: 'Command body here.'
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode(commands, [], TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'claude-code/commands/test-command.md');
|
||||
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter.name).toBe('test-command');
|
||||
expect(parsed.frontmatter.description).toBe('A test command');
|
||||
expect(parsed.frontmatter.args).toBeArray();
|
||||
expect(parsed.frontmatter.args).toHaveLength(2);
|
||||
expect(parsed.frontmatter.args[0].name).toBe('target');
|
||||
expect(parsed.frontmatter.args[1].required).toBe(true);
|
||||
expect(parsed.body).toBe('Command body here.');
|
||||
});
|
||||
|
||||
test('should handle commands without args', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'simple-cmd',
|
||||
description: 'Simple command',
|
||||
args: [],
|
||||
body: 'Simple body.'
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/commands/simple-cmd.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter.name).toBe('simple-cmd');
|
||||
expect(parsed.frontmatter.args).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should create skills in subdirectories with SKILL.md filename', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'test-skill',
|
||||
description: 'A test skill',
|
||||
license: 'MIT',
|
||||
body: 'Skill instructions.'
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode([], skills, TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'claude-code/skills/test-skill/SKILL.md');
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter.name).toBe('test-skill');
|
||||
expect(parsed.frontmatter.description).toBe('A test skill');
|
||||
expect(parsed.frontmatter.license).toBe('MIT');
|
||||
expect(parsed.body).toBe('Skill instructions.');
|
||||
});
|
||||
|
||||
test('should handle skills without license', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'no-license-skill',
|
||||
description: 'Skill without license',
|
||||
license: '',
|
||||
body: 'Body content.'
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode([], skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/skills/no-license-skill/SKILL.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter.license).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should handle multiple commands', () => {
|
||||
const commands = [
|
||||
{ name: 'cmd1', description: 'Command 1', args: [], body: 'Body 1' },
|
||||
{ name: 'cmd2', description: 'Command 2', args: [], body: 'Body 2' },
|
||||
{ name: 'cmd3', description: 'Command 3', args: [], body: 'Body 3' }
|
||||
];
|
||||
|
||||
transformClaudeCode(commands, [], TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/commands/cmd1.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/commands/cmd2.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/commands/cmd3.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle multiple skills', () => {
|
||||
const skills = [
|
||||
{ name: 'skill1', description: 'Skill 1', license: 'MIT', body: 'Body 1' },
|
||||
{ name: 'skill2', description: 'Skill 2', license: 'Apache', body: 'Body 2' }
|
||||
];
|
||||
|
||||
transformClaudeCode([], skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/skills/skill1/SKILL.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/skills/skill2/SKILL.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should clean existing directory before writing', () => {
|
||||
fs.mkdirSync(path.join(TEST_DIR, 'claude-code/commands'), { recursive: true });
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'claude-code/commands/old.md'), 'old');
|
||||
|
||||
const commands = [{ name: 'new', description: 'New', args: [], body: 'New' }];
|
||||
transformClaudeCode(commands, [], TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/commands/old.md'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/commands/new.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should preserve {{placeholder}} syntax in body', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'with-placeholder',
|
||||
description: 'Has placeholder',
|
||||
args: [{ name: 'target', description: 'Target', required: false }],
|
||||
body: 'Process {{target}} and generate output.'
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/commands/with-placeholder.md'), 'utf-8');
|
||||
expect(content).toContain('{{target}}');
|
||||
});
|
||||
|
||||
test('should log correct summary', () => {
|
||||
const consoleMock = mock(() => {});
|
||||
const originalLog = console.log;
|
||||
console.log = consoleMock;
|
||||
|
||||
const commands = [{ name: 'cmd1', description: 'Test', args: [], body: 'body' }];
|
||||
const skills = [{ name: 'skill1', description: 'Test', license: '', body: 'body' }];
|
||||
|
||||
transformClaudeCode(commands, skills, TEST_DIR);
|
||||
|
||||
console.log = originalLog;
|
||||
|
||||
expect(consoleMock).toHaveBeenCalledWith('✓ Claude Code: 1 commands, 1 skills');
|
||||
});
|
||||
|
||||
test('should handle empty arrays', () => {
|
||||
transformClaudeCode([], [], TEST_DIR);
|
||||
|
||||
const commandFiles = fs.readdirSync(path.join(TEST_DIR, 'claude-code/commands'));
|
||||
const skillDirs = fs.readdirSync(path.join(TEST_DIR, 'claude-code/skills'));
|
||||
|
||||
expect(commandFiles).toHaveLength(0);
|
||||
expect(skillDirs).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should format frontmatter correctly with args', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'test',
|
||||
description: 'Test command',
|
||||
args: [
|
||||
{ name: 'arg1', description: 'First arg', required: true },
|
||||
{ name: 'arg2', description: 'Second arg', required: false }
|
||||
],
|
||||
body: 'Body'
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/commands/test.md'), 'utf-8');
|
||||
|
||||
expect(content).toContain('---');
|
||||
expect(content).toContain('name: test');
|
||||
expect(content).toContain('description: Test command');
|
||||
expect(content).toContain('args:');
|
||||
expect(content).toContain('- name: arg1');
|
||||
expect(content).toContain('description: First arg');
|
||||
expect(content).toContain('required: true');
|
||||
expect(content).toContain('- name: arg2');
|
||||
expect(content).toContain('required: false');
|
||||
});
|
||||
|
||||
test('should preserve multiline body content', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'multiline',
|
||||
description: 'Test',
|
||||
args: [],
|
||||
body: `First paragraph.
|
||||
|
||||
Second paragraph with details.
|
||||
|
||||
- List item 1
|
||||
- List item 2`
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/commands/multiline.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.body).toContain('First paragraph.');
|
||||
expect(parsed.body).toContain('Second paragraph');
|
||||
expect(parsed.body).toContain('- List item 1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { transformCodex } from '../../../scripts/lib/transformers/codex.js';
|
||||
import { parseFrontmatter } from '../../../scripts/lib/utils.js';
|
||||
|
||||
const TEST_DIR = path.join(process.cwd(), 'test-tmp-codex');
|
||||
|
||||
describe('transformCodex', () => {
|
||||
beforeEach(() => {
|
||||
if (fs.existsSync(TEST_DIR)) {
|
||||
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (fs.existsSync(TEST_DIR)) {
|
||||
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('should create correct directory structure', () => {
|
||||
const commands = [];
|
||||
const skills = [];
|
||||
|
||||
transformCodex(commands, skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex/prompts'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should create command with custom frontmatter format', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'test-command',
|
||||
description: 'A test command',
|
||||
args: [],
|
||||
body: 'Command body content.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'codex/prompts/test-command.md');
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter.description).toBe('A test command');
|
||||
expect(parsed.body).toBe('Command body content.');
|
||||
});
|
||||
|
||||
test('should create argument-hint for required args', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'with-args',
|
||||
description: 'Command with args',
|
||||
args: [
|
||||
{ name: 'target', description: 'Target', required: true },
|
||||
{ name: 'output', description: 'Output', required: true }
|
||||
],
|
||||
body: 'Body'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/with-args.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter['argument-hint']).toBe('<target> <output>');
|
||||
});
|
||||
|
||||
test('should create argument-hint for optional args', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'optional-args',
|
||||
description: 'Command with optional args',
|
||||
args: [
|
||||
{ name: 'format', description: 'Format', required: false }
|
||||
],
|
||||
body: 'Body'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/optional-args.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter['argument-hint']).toBe('[FORMAT=<value>]');
|
||||
});
|
||||
|
||||
test('should create argument-hint with mixed required and optional args', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'mixed-args',
|
||||
description: 'Mixed args',
|
||||
args: [
|
||||
{ name: 'input', description: 'Input', required: true },
|
||||
{ name: 'format', description: 'Format', required: false },
|
||||
{ name: 'output', description: 'Output', required: true }
|
||||
],
|
||||
body: 'Body'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/mixed-args.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter['argument-hint']).toBe('<input> [FORMAT=<value>] <output>');
|
||||
});
|
||||
|
||||
test('should transform {{argname}} to $ARGNAME', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'normalize',
|
||||
description: 'Normalize',
|
||||
args: [{ name: 'target', description: 'Target', required: false }],
|
||||
body: 'Please normalize {{target}} to match the design system.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/normalize.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.body).toContain('$TARGET');
|
||||
expect(parsed.body).not.toContain('{{target}}');
|
||||
});
|
||||
|
||||
test('should transform multiple different placeholders', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'multi-arg',
|
||||
description: 'Multiple args',
|
||||
args: [],
|
||||
body: 'Process {{input}} and output to {{output}} with {{format}}.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/multi-arg.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.body).toContain('$INPUT');
|
||||
expect(parsed.body).toContain('$OUTPUT');
|
||||
expect(parsed.body).toContain('$FORMAT');
|
||||
});
|
||||
|
||||
test('should create modular skill files', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'test-skill',
|
||||
description: 'A test skill',
|
||||
license: 'MIT',
|
||||
body: 'Skill instructions here.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex([], skills, TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'codex/AGENTS.test-skill.md');
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||
expect(content).toBe('Skill instructions here.');
|
||||
});
|
||||
|
||||
test('should create main AGENTS.md with routing instructions', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'frontend-design',
|
||||
description: 'Create distinctive, production-grade frontend interfaces',
|
||||
license: 'MIT',
|
||||
body: 'Frontend design instructions.'
|
||||
},
|
||||
{
|
||||
name: 'backend-api',
|
||||
description: 'Design robust API endpoints',
|
||||
license: 'Apache',
|
||||
body: 'Backend API instructions.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex([], skills, TEST_DIR);
|
||||
|
||||
const agentsMdPath = path.join(TEST_DIR, 'codex/AGENTS.md');
|
||||
expect(fs.existsSync(agentsMdPath)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(agentsMdPath, 'utf-8');
|
||||
expect(content).toContain('# Codex Agent Instructions');
|
||||
expect(content).toContain('## Available Skills');
|
||||
expect(content).toContain('### frontend-design');
|
||||
expect(content).toContain('**When to use**: Create distinctive, production-grade frontend interfaces');
|
||||
expect(content).toContain('**Read**: `AGENTS.frontend-design.md`');
|
||||
expect(content).toContain('### backend-api');
|
||||
expect(content).toContain('**Read**: `AGENTS.backend-api.md`');
|
||||
});
|
||||
|
||||
test('should handle multiple commands', () => {
|
||||
const commands = [
|
||||
{ name: 'cmd1', description: 'Command 1', args: [], body: 'Body 1' },
|
||||
{ name: 'cmd2', description: 'Command 2', args: [], body: 'Body 2' },
|
||||
{ name: 'cmd3', description: 'Command 3', args: [], body: 'Body 3' }
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex/prompts/cmd1.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex/prompts/cmd2.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex/prompts/cmd3.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should clean existing directory before writing', () => {
|
||||
fs.mkdirSync(path.join(TEST_DIR, 'codex/prompts'), { recursive: true });
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'codex/prompts/old.md'), 'old');
|
||||
|
||||
const commands = [{ name: 'new', description: 'New', args: [], body: 'New' }];
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex/prompts/old.md'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex/prompts/new.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle commands without args', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'no-args',
|
||||
description: 'No args command',
|
||||
args: [],
|
||||
body: 'Body content'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/no-args.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter['argument-hint']).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should preserve multiline body', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'multiline',
|
||||
description: 'Test',
|
||||
args: [],
|
||||
body: `First line.
|
||||
|
||||
Second line after blank.
|
||||
|
||||
- Bullet 1
|
||||
- Bullet 2`
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/multiline.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.body).toContain('First line.\n\nSecond line');
|
||||
expect(parsed.body).toContain('- Bullet 1\n- Bullet 2');
|
||||
});
|
||||
|
||||
test('should log correct summary', () => {
|
||||
const consoleMock = mock(() => {});
|
||||
const originalLog = console.log;
|
||||
console.log = consoleMock;
|
||||
|
||||
const commands = [{ name: 'cmd1', description: 'Test', args: [], body: 'body' }];
|
||||
const skills = [{ name: 'skill1', description: 'Test', license: '', body: 'body' }];
|
||||
|
||||
transformCodex(commands, skills, TEST_DIR);
|
||||
|
||||
console.log = originalLog;
|
||||
|
||||
expect(consoleMock).toHaveBeenCalledWith('✓ Codex: 1 prompts, 1 skills (modular)');
|
||||
});
|
||||
|
||||
test('should handle empty arrays', () => {
|
||||
transformCodex([], [], TEST_DIR);
|
||||
|
||||
const promptFiles = fs.readdirSync(path.join(TEST_DIR, 'codex/prompts'));
|
||||
expect(promptFiles).toHaveLength(0);
|
||||
|
||||
// Should still create AGENTS.md even with no skills
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex/AGENTS.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle body without placeholders', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'no-placeholders',
|
||||
description: 'No placeholders',
|
||||
args: [],
|
||||
body: 'Just plain text without any placeholders.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/no-placeholders.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.body).toBe('Just plain text without any placeholders.');
|
||||
});
|
||||
|
||||
test('AGENTS.md should have proper structure', () => {
|
||||
const skills = [
|
||||
{ name: 'skill1', description: 'First skill', license: '', body: 'body1' }
|
||||
];
|
||||
|
||||
transformCodex([], skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/AGENTS.md'), 'utf-8');
|
||||
|
||||
expect(content).toContain('# Codex Agent Instructions');
|
||||
expect(content).toContain('## Available Skills');
|
||||
expect(content).toContain('## How to Use Skills');
|
||||
expect(content).toContain('### skill1');
|
||||
expect(content).toContain('**When to use**: First skill');
|
||||
expect(content).toContain('**Read**: `AGENTS.skill1.md`');
|
||||
});
|
||||
|
||||
test('should handle arg names with hyphens', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'hyphen-arg',
|
||||
description: 'Test',
|
||||
args: [],
|
||||
body: 'Process {{my-input}} and {{output-file}}.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/hyphen-arg.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.body).toContain('$MY-INPUT');
|
||||
expect(parsed.body).toContain('$OUTPUT-FILE');
|
||||
});
|
||||
|
||||
test('should create proper frontmatter structure', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'test',
|
||||
description: 'Test command',
|
||||
args: [{ name: 'arg1', description: 'Arg 1', required: true }],
|
||||
body: 'Body'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/test.md'), 'utf-8');
|
||||
|
||||
expect(content).toContain('---');
|
||||
expect(content).toContain('description: Test command');
|
||||
expect(content).toContain('argument-hint: <arg1>');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { transformCursor } from '../../../scripts/lib/transformers/cursor.js';
|
||||
|
||||
const TEST_DIR = path.join(process.cwd(), 'test-tmp-cursor');
|
||||
|
||||
describe('transformCursor', () => {
|
||||
beforeEach(() => {
|
||||
if (fs.existsSync(TEST_DIR)) {
|
||||
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (fs.existsSync(TEST_DIR)) {
|
||||
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('should create correct directory structure', () => {
|
||||
const commands = [];
|
||||
const skills = [];
|
||||
|
||||
transformCursor(commands, skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/commands'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/rules'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should strip frontmatter from commands and output body only', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'test-command',
|
||||
description: 'A test command',
|
||||
args: [{ name: 'target', description: 'Target', required: false }],
|
||||
body: 'This is the command body content.'
|
||||
}
|
||||
];
|
||||
const skills = [];
|
||||
|
||||
transformCursor(commands, skills, TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'cursor/commands/test-command.md');
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||
expect(content).toBe('This is the command body content.');
|
||||
expect(content).not.toContain('---');
|
||||
expect(content).not.toContain('name:');
|
||||
expect(content).not.toContain('description:');
|
||||
});
|
||||
|
||||
test('should strip frontmatter from skills and output body only', () => {
|
||||
const commands = [];
|
||||
const skills = [
|
||||
{
|
||||
name: 'test-skill',
|
||||
description: 'A test skill',
|
||||
license: 'MIT',
|
||||
body: 'These are the skill instructions.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCursor(commands, skills, TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'cursor/rules/test-skill.md');
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||
expect(content).toBe('These are the skill instructions.');
|
||||
expect(content).not.toContain('---');
|
||||
expect(content).not.toContain('license:');
|
||||
});
|
||||
|
||||
test('should handle multiple commands', () => {
|
||||
const commands = [
|
||||
{ name: 'cmd1', description: 'Command 1', args: [], body: 'Body 1' },
|
||||
{ name: 'cmd2', description: 'Command 2', args: [], body: 'Body 2' },
|
||||
{ name: 'cmd3', description: 'Command 3', args: [], body: 'Body 3' }
|
||||
];
|
||||
const skills = [];
|
||||
|
||||
transformCursor(commands, skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/commands/cmd1.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/commands/cmd2.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/commands/cmd3.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle multiple skills', () => {
|
||||
const commands = [];
|
||||
const skills = [
|
||||
{ name: 'skill1', description: 'Skill 1', license: 'MIT', body: 'Skill body 1' },
|
||||
{ name: 'skill2', description: 'Skill 2', license: 'Apache', body: 'Skill body 2' }
|
||||
];
|
||||
|
||||
transformCursor(commands, skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/rules/skill1.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/rules/skill2.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should clean existing directory before writing', () => {
|
||||
// Create a pre-existing file
|
||||
fs.mkdirSync(path.join(TEST_DIR, 'cursor/commands'), { recursive: true });
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'cursor/commands/old-file.md'), 'old content');
|
||||
|
||||
const commands = [
|
||||
{ name: 'new-cmd', description: 'New', args: [], body: 'New body' }
|
||||
];
|
||||
|
||||
transformCursor(commands, [], TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/commands/old-file.md'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/commands/new-cmd.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle commands with placeholder args in body', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'normalize',
|
||||
description: 'Normalize design',
|
||||
args: [{ name: 'target', description: 'Target', required: false }],
|
||||
body: 'Please normalize {{target}} to match the design system.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCursor(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/commands/normalize.md'), 'utf-8');
|
||||
// Cursor transformer should preserve the body as-is
|
||||
expect(content).toBe('Please normalize {{target}} to match the design system.');
|
||||
});
|
||||
|
||||
test('should log correct summary', () => {
|
||||
const consoleMock = mock(() => {});
|
||||
const originalLog = console.log;
|
||||
console.log = consoleMock;
|
||||
|
||||
const commands = [{ name: 'cmd1', description: '', args: [], body: 'body1' }];
|
||||
const skills = [{ name: 'skill1', description: '', license: '', body: 'body1' }];
|
||||
|
||||
transformCursor(commands, skills, TEST_DIR);
|
||||
|
||||
console.log = originalLog;
|
||||
|
||||
expect(consoleMock).toHaveBeenCalledWith('✓ Cursor: 1 commands, 1 skills (downgraded)');
|
||||
});
|
||||
|
||||
test('should handle empty commands and skills arrays', () => {
|
||||
transformCursor([], [], TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/commands'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/rules'))).toBe(true);
|
||||
|
||||
const commandFiles = fs.readdirSync(path.join(TEST_DIR, 'cursor/commands'));
|
||||
const ruleFiles = fs.readdirSync(path.join(TEST_DIR, 'cursor/rules'));
|
||||
|
||||
expect(commandFiles).toHaveLength(0);
|
||||
expect(ruleFiles).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should preserve line breaks and formatting in body', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'formatted',
|
||||
description: 'Test',
|
||||
args: [],
|
||||
body: `Line 1
|
||||
|
||||
Line 3 after blank line
|
||||
|
||||
- Bullet 1
|
||||
- Bullet 2
|
||||
|
||||
End.`
|
||||
}
|
||||
];
|
||||
|
||||
transformCursor(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/commands/formatted.md'), 'utf-8');
|
||||
expect(content).toContain('Line 1\n\nLine 3');
|
||||
expect(content).toContain('- Bullet 1\n- Bullet 2');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { transformGemini } from '../../../scripts/lib/transformers/gemini.js';
|
||||
|
||||
const TEST_DIR = path.join(process.cwd(), 'test-tmp-gemini');
|
||||
|
||||
describe('transformGemini', () => {
|
||||
beforeEach(() => {
|
||||
if (fs.existsSync(TEST_DIR)) {
|
||||
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (fs.existsSync(TEST_DIR)) {
|
||||
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('should create correct directory structure', () => {
|
||||
const commands = [];
|
||||
const skills = [];
|
||||
|
||||
transformGemini(commands, skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/commands'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should convert command to TOML format', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'test-command',
|
||||
description: 'A test command',
|
||||
args: [],
|
||||
body: 'Command body content.'
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini(commands, [], TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'gemini/commands/test-command.toml');
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||
expect(content).toContain('description = "A test command"');
|
||||
expect(content).toContain('prompt = """');
|
||||
expect(content).toContain('Command body content.');
|
||||
expect(content).toContain('"""');
|
||||
});
|
||||
|
||||
test('should replace {{argname}} placeholders with {{args}}', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'normalize',
|
||||
description: 'Normalize design',
|
||||
args: [{ name: 'target', description: 'Target', required: false }],
|
||||
body: 'Please normalize {{target}} to match the design system.'
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/commands/normalize.toml'), 'utf-8');
|
||||
expect(content).toContain('{{args}}');
|
||||
expect(content).not.toContain('{{target}}');
|
||||
});
|
||||
|
||||
test('should replace multiple different placeholders with {{args}}', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'multi-arg',
|
||||
description: 'Multiple args',
|
||||
args: [],
|
||||
body: 'Process {{input}} and output to {{output}} with {{format}}.'
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/commands/multi-arg.toml'), 'utf-8');
|
||||
const argsMatches = content.match(/\{\{args\}\}/g);
|
||||
expect(argsMatches).toHaveLength(3);
|
||||
});
|
||||
|
||||
test('should escape quotes in description', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'with-quotes',
|
||||
description: 'A command with "quotes" in description',
|
||||
args: [],
|
||||
body: 'Body content.'
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/commands/with-quotes.toml'), 'utf-8');
|
||||
expect(content).toContain('description = "A command with \\"quotes\\" in description"');
|
||||
});
|
||||
|
||||
test('should create modular skill files', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'test-skill',
|
||||
description: 'A test skill',
|
||||
license: 'MIT',
|
||||
body: 'Skill instructions here.'
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini([], skills, TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'gemini/GEMINI.test-skill.md');
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||
expect(content).toBe('Skill instructions here.');
|
||||
});
|
||||
|
||||
test('should create main GEMINI.md with imports', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'frontend-design',
|
||||
description: 'Create distinctive, production-grade frontend interfaces',
|
||||
license: 'MIT',
|
||||
body: 'Frontend design instructions.'
|
||||
},
|
||||
{
|
||||
name: 'backend-api',
|
||||
description: 'Design robust API endpoints',
|
||||
license: 'Apache',
|
||||
body: 'Backend API instructions.'
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini([], skills, TEST_DIR);
|
||||
|
||||
const geminiMdPath = path.join(TEST_DIR, 'gemini/GEMINI.md');
|
||||
expect(fs.existsSync(geminiMdPath)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(geminiMdPath, 'utf-8');
|
||||
expect(content).toContain('# Gemini Context');
|
||||
expect(content).toContain('frontend-design');
|
||||
expect(content).toContain('Create distinctive, production-grade frontend interfaces');
|
||||
expect(content).toContain('@./GEMINI.frontend-design.md');
|
||||
expect(content).toContain('backend-api');
|
||||
expect(content).toContain('@./GEMINI.backend-api.md');
|
||||
});
|
||||
|
||||
test('should handle multiple commands', () => {
|
||||
const commands = [
|
||||
{ name: 'cmd1', description: 'Command 1', args: [], body: 'Body 1' },
|
||||
{ name: 'cmd2', description: 'Command 2', args: [], body: 'Body 2' },
|
||||
{ name: 'cmd3', description: 'Command 3', args: [], body: 'Body 3' }
|
||||
];
|
||||
|
||||
transformGemini(commands, [], TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/commands/cmd1.toml'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/commands/cmd2.toml'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/commands/cmd3.toml'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should clean existing directory before writing', () => {
|
||||
fs.mkdirSync(path.join(TEST_DIR, 'gemini/commands'), { recursive: true });
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'gemini/commands/old.toml'), 'old');
|
||||
|
||||
const commands = [{ name: 'new', description: 'New', args: [], body: 'New' }];
|
||||
transformGemini(commands, [], TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/commands/old.toml'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/commands/new.toml'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should preserve multiline body in TOML triple-quoted strings', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'multiline',
|
||||
description: 'Test',
|
||||
args: [],
|
||||
body: `First line.
|
||||
|
||||
Second line after blank.
|
||||
|
||||
- Bullet 1
|
||||
- Bullet 2`
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/commands/multiline.toml'), 'utf-8');
|
||||
expect(content).toContain('First line.\n\nSecond line');
|
||||
expect(content).toContain('- Bullet 1\n- Bullet 2');
|
||||
});
|
||||
|
||||
test('should log correct summary', () => {
|
||||
const consoleMock = mock(() => {});
|
||||
const originalLog = console.log;
|
||||
console.log = consoleMock;
|
||||
|
||||
const commands = [{ name: 'cmd1', description: 'Test', args: [], body: 'body' }];
|
||||
const skills = [{ name: 'skill1', description: 'Test', license: '', body: 'body' }];
|
||||
|
||||
transformGemini(commands, skills, TEST_DIR);
|
||||
|
||||
console.log = originalLog;
|
||||
|
||||
expect(consoleMock).toHaveBeenCalledWith('✓ Gemini: 1 commands (TOML), 1 skills (modular)');
|
||||
});
|
||||
|
||||
test('should handle empty arrays', () => {
|
||||
transformGemini([], [], TEST_DIR);
|
||||
|
||||
const commandFiles = fs.readdirSync(path.join(TEST_DIR, 'gemini/commands'));
|
||||
expect(commandFiles).toHaveLength(0);
|
||||
|
||||
// Should still create GEMINI.md even with no skills
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/GEMINI.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should create proper TOML structure', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'test',
|
||||
description: 'Test command',
|
||||
args: [],
|
||||
body: 'Test body'
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/commands/test.toml'), 'utf-8');
|
||||
|
||||
// Check for proper TOML structure
|
||||
const lines = content.split('\n');
|
||||
expect(lines[0]).toMatch(/^description = /);
|
||||
expect(lines[1]).toBe('prompt = """');
|
||||
expect(lines[lines.length - 1]).toBe('"""');
|
||||
});
|
||||
|
||||
test('should handle body without placeholders', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'no-placeholders',
|
||||
description: 'No args',
|
||||
args: [],
|
||||
body: 'Just plain text without any placeholders.'
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/commands/no-placeholders.toml'), 'utf-8');
|
||||
expect(content).toContain('Just plain text without any placeholders.');
|
||||
expect(content).not.toContain('{{args}}');
|
||||
});
|
||||
|
||||
test('GEMINI.md should have proper structure', () => {
|
||||
const skills = [
|
||||
{ name: 'skill1', description: 'First skill', license: '', body: 'body1' }
|
||||
];
|
||||
|
||||
transformGemini([], skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/GEMINI.md'), 'utf-8');
|
||||
|
||||
expect(content).toContain('# Gemini Context');
|
||||
expect(content).toContain('## Available Skills');
|
||||
expect(content).toContain('## How Skills Work');
|
||||
expect(content).toContain('### skill1');
|
||||
expect(content).toContain('**When to use**: First skill');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import {
|
||||
parseFrontmatter,
|
||||
readFilesRecursive,
|
||||
readSourceFiles,
|
||||
ensureDir,
|
||||
cleanDir,
|
||||
writeFile,
|
||||
generateYamlFrontmatter
|
||||
} from '../../scripts/lib/utils.js';
|
||||
|
||||
// Temporary test directory
|
||||
const TEST_DIR = path.join(process.cwd(), 'test-tmp');
|
||||
|
||||
describe('parseFrontmatter', () => {
|
||||
test('should parse basic frontmatter with simple key-value pairs', () => {
|
||||
const content = `---
|
||||
name: test-command
|
||||
description: A test command
|
||||
---
|
||||
|
||||
This is the body content.`;
|
||||
|
||||
const result = parseFrontmatter(content);
|
||||
expect(result.frontmatter.name).toBe('test-command');
|
||||
expect(result.frontmatter.description).toBe('A test command');
|
||||
expect(result.body).toBe('This is the body content.');
|
||||
});
|
||||
|
||||
test('should parse frontmatter with args array', () => {
|
||||
const content = `---
|
||||
name: test-command
|
||||
description: A test command
|
||||
args:
|
||||
- name: target
|
||||
description: The target to normalize
|
||||
required: false
|
||||
- name: output
|
||||
description: Output format
|
||||
required: true
|
||||
---
|
||||
|
||||
Body here.`;
|
||||
|
||||
const result = parseFrontmatter(content);
|
||||
expect(result.frontmatter.name).toBe('test-command');
|
||||
expect(result.frontmatter.args).toBeArray();
|
||||
expect(result.frontmatter.args).toHaveLength(2);
|
||||
expect(result.frontmatter.args[0].name).toBe('target');
|
||||
expect(result.frontmatter.args[0].description).toBe('The target to normalize');
|
||||
expect(result.frontmatter.args[0].required).toBe(false);
|
||||
expect(result.frontmatter.args[1].required).toBe(true);
|
||||
});
|
||||
|
||||
test('should return empty frontmatter when no frontmatter present', () => {
|
||||
const content = 'Just some content without frontmatter.';
|
||||
const result = parseFrontmatter(content);
|
||||
|
||||
expect(result.frontmatter).toEqual({});
|
||||
expect(result.body).toBe(content);
|
||||
});
|
||||
|
||||
test('should handle empty body', () => {
|
||||
const content = `---
|
||||
name: test
|
||||
---
|
||||
`;
|
||||
const result = parseFrontmatter(content);
|
||||
|
||||
expect(result.frontmatter.name).toBe('test');
|
||||
expect(result.body).toBe('');
|
||||
});
|
||||
|
||||
test('should handle frontmatter with license field', () => {
|
||||
const content = `---
|
||||
name: skill-name
|
||||
description: A skill
|
||||
license: MIT
|
||||
---
|
||||
|
||||
Skill body.`;
|
||||
|
||||
const result = parseFrontmatter(content);
|
||||
expect(result.frontmatter.license).toBe('MIT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateYamlFrontmatter', () => {
|
||||
test('should generate basic frontmatter', () => {
|
||||
const data = {
|
||||
name: 'test-command',
|
||||
description: 'A test'
|
||||
};
|
||||
|
||||
const result = generateYamlFrontmatter(data);
|
||||
expect(result).toContain('---');
|
||||
expect(result).toContain('name: test-command');
|
||||
expect(result).toContain('description: A test');
|
||||
});
|
||||
|
||||
test('should generate frontmatter with args array', () => {
|
||||
const data = {
|
||||
name: 'test',
|
||||
description: 'Test command',
|
||||
args: [
|
||||
{ name: 'target', description: 'The target', required: false },
|
||||
{ name: 'output', description: 'Output format', required: true }
|
||||
]
|
||||
};
|
||||
|
||||
const result = generateYamlFrontmatter(data);
|
||||
expect(result).toContain('args:');
|
||||
expect(result).toContain('- name: target');
|
||||
expect(result).toContain('description: The target');
|
||||
expect(result).toContain('required: false');
|
||||
expect(result).toContain('required: true');
|
||||
});
|
||||
|
||||
test('should roundtrip: generate and parse back', () => {
|
||||
const original = {
|
||||
name: 'roundtrip-test',
|
||||
description: 'Testing roundtrip',
|
||||
args: [
|
||||
{ name: 'arg1', description: 'First arg', required: true }
|
||||
]
|
||||
};
|
||||
|
||||
const yaml = generateYamlFrontmatter(original);
|
||||
const content = `${yaml}\n\nBody content`;
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter.name).toBe(original.name);
|
||||
expect(parsed.frontmatter.description).toBe(original.description);
|
||||
expect(parsed.frontmatter.args).toBeArray();
|
||||
expect(parsed.frontmatter.args[0].name).toBe('arg1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureDir', () => {
|
||||
afterEach(() => {
|
||||
if (fs.existsSync(TEST_DIR)) {
|
||||
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('should create directory if it does not exist', () => {
|
||||
const testPath = path.join(TEST_DIR, 'new-dir');
|
||||
ensureDir(testPath);
|
||||
|
||||
expect(fs.existsSync(testPath)).toBe(true);
|
||||
expect(fs.statSync(testPath).isDirectory()).toBe(true);
|
||||
});
|
||||
|
||||
test('should create nested directories', () => {
|
||||
const testPath = path.join(TEST_DIR, 'level1', 'level2', 'level3');
|
||||
ensureDir(testPath);
|
||||
|
||||
expect(fs.existsSync(testPath)).toBe(true);
|
||||
});
|
||||
|
||||
test('should not throw if directory already exists', () => {
|
||||
const testPath = path.join(TEST_DIR, 'existing');
|
||||
fs.mkdirSync(testPath, { recursive: true });
|
||||
|
||||
expect(() => ensureDir(testPath)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanDir', () => {
|
||||
beforeEach(() => {
|
||||
ensureDir(TEST_DIR);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (fs.existsSync(TEST_DIR)) {
|
||||
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('should remove directory and all contents', () => {
|
||||
const filePath = path.join(TEST_DIR, 'test.txt');
|
||||
fs.writeFileSync(filePath, 'content');
|
||||
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
|
||||
cleanDir(TEST_DIR);
|
||||
expect(fs.existsSync(TEST_DIR)).toBe(false);
|
||||
});
|
||||
|
||||
test('should not throw if directory does not exist', () => {
|
||||
const nonExistent = path.join(TEST_DIR, 'does-not-exist');
|
||||
expect(() => cleanDir(nonExistent)).not.toThrow();
|
||||
});
|
||||
|
||||
test('should remove nested directories', () => {
|
||||
const nestedPath = path.join(TEST_DIR, 'level1', 'level2');
|
||||
ensureDir(nestedPath);
|
||||
fs.writeFileSync(path.join(nestedPath, 'file.txt'), 'content');
|
||||
|
||||
cleanDir(TEST_DIR);
|
||||
expect(fs.existsSync(TEST_DIR)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeFile', () => {
|
||||
afterEach(() => {
|
||||
if (fs.existsSync(TEST_DIR)) {
|
||||
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('should write file with content', () => {
|
||||
const filePath = path.join(TEST_DIR, 'test.txt');
|
||||
const content = 'Hello, world!';
|
||||
|
||||
writeFile(filePath, content);
|
||||
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
expect(fs.readFileSync(filePath, 'utf-8')).toBe(content);
|
||||
});
|
||||
|
||||
test('should create parent directories automatically', () => {
|
||||
const filePath = path.join(TEST_DIR, 'nested', 'deep', 'file.txt');
|
||||
writeFile(filePath, 'content');
|
||||
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
expect(fs.readFileSync(filePath, 'utf-8')).toBe('content');
|
||||
});
|
||||
|
||||
test('should overwrite existing file', () => {
|
||||
const filePath = path.join(TEST_DIR, 'file.txt');
|
||||
writeFile(filePath, 'first');
|
||||
writeFile(filePath, 'second');
|
||||
|
||||
expect(fs.readFileSync(filePath, 'utf-8')).toBe('second');
|
||||
});
|
||||
});
|
||||
|
||||
describe('readFilesRecursive', () => {
|
||||
beforeEach(() => {
|
||||
ensureDir(TEST_DIR);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (fs.existsSync(TEST_DIR)) {
|
||||
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('should find all markdown files in directory', () => {
|
||||
writeFile(path.join(TEST_DIR, 'file1.md'), 'content1');
|
||||
writeFile(path.join(TEST_DIR, 'file2.md'), 'content2');
|
||||
writeFile(path.join(TEST_DIR, 'file3.txt'), 'not markdown');
|
||||
|
||||
const files = readFilesRecursive(TEST_DIR);
|
||||
expect(files).toHaveLength(2);
|
||||
expect(files.some(f => f.endsWith('file1.md'))).toBe(true);
|
||||
expect(files.some(f => f.endsWith('file2.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should find markdown files in nested directories', () => {
|
||||
writeFile(path.join(TEST_DIR, 'root.md'), 'root');
|
||||
writeFile(path.join(TEST_DIR, 'sub', 'nested.md'), 'nested');
|
||||
writeFile(path.join(TEST_DIR, 'sub', 'deep', 'deeper.md'), 'deeper');
|
||||
|
||||
const files = readFilesRecursive(TEST_DIR);
|
||||
expect(files).toHaveLength(3);
|
||||
expect(files.some(f => f.endsWith('root.md'))).toBe(true);
|
||||
expect(files.some(f => f.endsWith('nested.md'))).toBe(true);
|
||||
expect(files.some(f => f.endsWith('deeper.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should return empty array for non-existent directory', () => {
|
||||
const files = readFilesRecursive(path.join(TEST_DIR, 'does-not-exist'));
|
||||
expect(files).toEqual([]);
|
||||
});
|
||||
|
||||
test('should return empty array for directory with no markdown files', () => {
|
||||
writeFile(path.join(TEST_DIR, 'file.txt'), 'text');
|
||||
writeFile(path.join(TEST_DIR, 'file.js'), 'code');
|
||||
|
||||
const files = readFilesRecursive(TEST_DIR);
|
||||
expect(files).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readSourceFiles', () => {
|
||||
const testRootDir = TEST_DIR;
|
||||
|
||||
beforeEach(() => {
|
||||
ensureDir(testRootDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (fs.existsSync(testRootDir)) {
|
||||
fs.rmSync(testRootDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('should read and parse command files', () => {
|
||||
const commandContent = `---
|
||||
name: test-command
|
||||
description: A test command
|
||||
args:
|
||||
- name: target
|
||||
description: Target arg
|
||||
required: false
|
||||
---
|
||||
|
||||
Command body content.`;
|
||||
|
||||
writeFile(path.join(testRootDir, 'source/commands/test-command.md'), commandContent);
|
||||
|
||||
const { commands, skills } = readSourceFiles(testRootDir);
|
||||
|
||||
expect(commands).toHaveLength(1);
|
||||
expect(commands[0].name).toBe('test-command');
|
||||
expect(commands[0].description).toBe('A test command');
|
||||
expect(commands[0].args).toHaveLength(1);
|
||||
expect(commands[0].body).toBe('Command body content.');
|
||||
});
|
||||
|
||||
test('should read and parse skill files', () => {
|
||||
const skillContent = `---
|
||||
name: test-skill
|
||||
description: A test skill
|
||||
license: MIT
|
||||
---
|
||||
|
||||
Skill instructions here.`;
|
||||
|
||||
writeFile(path.join(testRootDir, 'source/skills/test-skill.md'), skillContent);
|
||||
|
||||
const { commands, skills } = readSourceFiles(testRootDir);
|
||||
|
||||
expect(skills).toHaveLength(1);
|
||||
expect(skills[0].name).toBe('test-skill');
|
||||
expect(skills[0].description).toBe('A test skill');
|
||||
expect(skills[0].license).toBe('MIT');
|
||||
expect(skills[0].body).toBe('Skill instructions here.');
|
||||
});
|
||||
|
||||
test('should use filename as name if not in frontmatter', () => {
|
||||
writeFile(path.join(testRootDir, 'source/commands/my-command.md'), 'Just body, no frontmatter.');
|
||||
|
||||
const { commands } = readSourceFiles(testRootDir);
|
||||
|
||||
expect(commands[0].name).toBe('my-command');
|
||||
});
|
||||
|
||||
test('should handle empty source directories', () => {
|
||||
ensureDir(path.join(testRootDir, 'source/commands'));
|
||||
ensureDir(path.join(testRootDir, 'source/skills'));
|
||||
|
||||
const { commands, skills } = readSourceFiles(testRootDir);
|
||||
|
||||
expect(commands).toEqual([]);
|
||||
expect(skills).toEqual([]);
|
||||
});
|
||||
|
||||
test('should read multiple commands and skills', () => {
|
||||
writeFile(path.join(testRootDir, 'source/commands/cmd1.md'), '---\nname: cmd1\n---\nBody1');
|
||||
writeFile(path.join(testRootDir, 'source/commands/cmd2.md'), '---\nname: cmd2\n---\nBody2');
|
||||
writeFile(path.join(testRootDir, 'source/skills/skill1.md'), '---\nname: skill1\n---\nSkill1');
|
||||
writeFile(path.join(testRootDir, 'source/skills/skill2.md'), '---\nname: skill2\n---\nSkill2');
|
||||
|
||||
const { commands, skills } = readSourceFiles(testRootDir);
|
||||
|
||||
expect(commands).toHaveLength(2);
|
||||
expect(skills).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user