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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .claude/commands/audit-skills.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Review the iconmate skill definition at `skills/iconmate/SKILL.md` and verify it accurately reflects the current state of the CLI.

## Steps

1. Read `skills/iconmate/SKILL.md` in full.
2. Run `iconmate --help` and `iconmate add --help`, `iconmate delete --help`, `iconmate list --help`, `iconmate iconify --help` to get the current CLI interface.
3. Read the project `README.md` for any features or commands not covered in the skill.
4. Read `Cargo.toml` for the current version number.
5. Compare and report:
- **Missing commands**: CLI commands not documented in the skill.
- **Outdated flags**: Flags in the skill that no longer exist or have changed.
- **Wrong defaults**: Default values that don't match current behavior.
- **Version mismatch**: If the skill metadata version doesn't match `Cargo.toml`.
- **Missing features**: Significant capabilities described in the README but absent from the skill.
6. Fix any issues found directly in `skills/iconmate/SKILL.md`.
7. Print a summary of what was checked and any changes made.
3 changes: 3 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ tag: tag_and_release

tag_and_release:
sh tag_and_release.sh

sync_readme:
cp README.md npm/README.md
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,14 @@ iconmate add --folder src/assets/icons --icon heroicons:heart --name Heart --out
iconmate delete --folder src/assets/icons
```

### List current icons

```bash
iconmate list --folder src/assets/icons
# or use the default folder (src/assets/icons)
iconmate list
```

### Iconify API Commands

```bash
Expand Down Expand Up @@ -278,6 +286,14 @@ iconmate add --folder src/assets/icons --icon "$(curl -fsSL https://api.iconify.

This means an AI can search, choose, and add icons without opening a browser.

#### Claude Code Skill

For the best AI experience, install the [iconmate skill](https://github.com/Blankeos/iconmate/tree/main/skills/iconmate) so your agent knows all the commands automatically:

```bash
npx skills add Blankeos/iconmate
```

### Package.json Scripts

Best practice: Add sensible defaults to your script runner.
Expand Down
8 changes: 8 additions & 0 deletions npm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,14 @@ iconmate add --folder src/assets/icons --icon heroicons:heart --name Heart --out
iconmate delete --folder src/assets/icons
```

### List current icons

```bash
iconmate list --folder src/assets/icons
# or use the default folder (src/assets/icons)
iconmate list
```

### Iconify API Commands

```bash
Expand Down
132 changes: 132 additions & 0 deletions skills/iconmate/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
---
name: iconmate
description: >-
Use when the user asks to add, search, list, or manage SVG icons in a JS/TS project.
Trigger phrases: "add an icon", "search for icons", "find an icon", "add svg",
"icon for heart", "list icons", "delete icon", "rename icon", "iconmate", "iconify".
metadata:
author: blankeos
version: "1.1.0"
repository: https://github.com/Blankeos/iconmate
license: MIT
---

# iconmate

Add SVG icons to JS/TS projects without icon libraries. Uses Iconify's 200k+ icons or any SVG source.

## Prerequisites

`iconmate` must be installed. If not available, install with any of these methods:

```bash
# npm / pnpm / bun (prebuilt binary via npm)
npm install -g iconmate
pnpm add -g iconmate
bun add -g iconmate

# Run without installing
npx iconmate
pnpm dlx iconmate
bunx iconmate

# Cargo (build from source)
cargo install iconmate

# Cargo binstall (prebuilt binary via cargo)
cargo binstall iconmate
```

## Workflow

Follow these steps when the user wants to add icons:

### 1. Search for icons

```bash
iconmate iconify search <query> --format json --limit 20 --include-collections
```

Present results to the user and let them pick.

### 2. Add the icon

```bash
iconmate add --folder <folder> --icon <prefix:name> --name <PascalCaseName>
```

- `--folder`: Icon output directory (default: `src/assets/icons`). Check `iconmate.config.json` or `iconmate.config.jsonc` in the project root for a configured folder.
- `--icon`: Accepts an Iconify name (e.g. `mdi:heart`), a URL, or raw SVG markup.
- `--name`: PascalCase alias used in the export (e.g. `Heart`).
- `--preset`: Framework preset. Check config for the project default.
- `normal` → `.svg` (plain SVG file)
- `react` → `.tsx` (React component)
- `svelte` → `.svelte` (Svelte component)
- `solid` → `.tsx` (SolidJS component)
- `vue` → `.vue` (Vue component)
- `emptysvg` → `.svg` (empty placeholder SVG)

### 3. Verify

```bash
iconmate list --folder <folder>
```

Confirm the icon appears in the list and the `index.ts` export file is updated.

## Other Commands

### Delete an icon

```bash
iconmate delete --folder <folder>
```

### List current icons

```bash
iconmate list
```

### Browse Iconify collections

```bash
# List all collections
iconmate iconify collections

# List icons in a collection
iconmate iconify collection mdi

# Get raw SVG for one icon
iconmate iconify get mdi:heart
```

### Add from URL or raw SVG

```bash
# From URL
iconmate add --folder src/assets/icons --icon https://api.iconify.design/mdi:heart.svg --name Heart

# From raw SVG
iconmate add --folder src/assets/icons --icon '<svg>...</svg>' --name Heart
```

### Custom export template

```bash
iconmate add --folder src/assets/icons --icon mdi:heart --name Heart \
--output-line-template "export { ReactComponent as Icon%name% } from './%icon%.svg?react';"
```

Template variables: `%name%` (PascalCase alias), `%icon%` (filename without extension), `%ext%` (file extension).

## Configuration

Check for `iconmate.config.json` or `iconmate.config.jsonc` in the project root. If present, respect its `folder`, `preset`, and `output_line_template` values. CLI flags override config values.

## Tips

- Always use `--format json` when searching so you can parse results programmatically.
- When adding multiple icons, run each `iconmate add` command separately. The export index is updated automatically after each add.
- For prototyping, use `--preset emptysvg` to create placeholder icons.
- The user imports icons like: `import { IconHeart } from "@/assets/icons";`
7 changes: 6 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,12 @@ fn load_global_config(
) -> anyhow::Result<Option<LoadedConfigFile<GlobalConfigFile>>> {
let mut candidates = Vec::<PathBuf>::new();

if let Some(config_dir) = dirs::config_dir() {
// Use $XDG_CONFIG_HOME if set, otherwise ~/.config — consistent across all platforms.
let config_dir = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| dirs::home_dir().map(|h| h.join(".config")));

if let Some(config_dir) = config_dir {
candidates.push(config_dir.join("iconmate.jsonc"));
candidates.push(config_dir.join("iconmate.json"));
candidates.push(config_dir.join("iconmate").join("config.jsonc"));
Expand Down
95 changes: 86 additions & 9 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ enum Commands {
folder: Option<PathBuf>,
},

/// List all icons currently exported in the icons folder.
#[command(visible_alias = "ls")]
List {
/// Pathname of the folder where all the icons are saved.
#[arg(long)]
folder: Option<PathBuf>,
},

/// Query Iconify collections, search results, and raw SVGs.
Iconify {
#[command(subcommand)]
Expand Down Expand Up @@ -529,11 +537,7 @@ async fn run_app(config: AppConfig) -> anyhow::Result<()> {

if index_ts_path.exists() {
let existing_index = fs::read_to_string(&index_ts_path)?;
validate_new_export_conflicts(
&existing_index,
&rendered_export_statement,
&index_ts_path,
)?;
validate_new_export_conflicts(&existing_index, &rendered_export_statement, &index_ts_path)?;
}

if svg_file_path.exists() {
Expand Down Expand Up @@ -801,6 +805,37 @@ fn remove_selected_exports_from_index(contents: &str, selected_icons: &[IconEntr
updated
}

fn resolve_list_folder<'a>(
cli: &'a CliArgs,
command_folder: Option<&'a PathBuf>,
) -> Option<&'a PathBuf> {
command_folder.or(cli.folder.as_ref())
}

fn run_list_mode(cli: &CliArgs, command_folder: Option<&PathBuf>) -> anyhow::Result<()> {
let folder = resolve_list_folder(cli, command_folder)
.cloned()
.unwrap_or_else(|| PathBuf::from(config::DEFAULT_FOLDER));
Comment on lines +816 to +818

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor configured folder defaults for list

The list command always falls back to config::DEFAULT_FOLDER when no --folder flag is passed, and it never consults the project config loader (e.g., iconmate.config.json) that the rest of the app uses for defaults. In projects that set a non-default folder in config, iconmate list will look in src/assets/icons and print “No icons found” even though icons exist in the configured folder unless users remember to pass --folder every time. This makes the new command inconsistent with configured defaults and will confuse users who rely on config.

Useful? React with 👍 / 👎.

let index_ts_path = folder.join("index.ts");

if !index_ts_path.exists() {
println!("No icons found in {}", index_ts_path.display());
return Ok(());
}

let icons = crate::utils::get_existing_icons(folder.to_string_lossy().as_ref())?;
if icons.is_empty() {
println!("No icons found in {}", index_ts_path.display());
return Ok(());
}

for icon in icons {
println!("{}\t{}", icon.name, icon.file_path);
}

Ok(())
}

/// Interactive mode: deleting an icon from a select list of icons.
fn resolve_delete_folder<'a>(
cli: &'a CliArgs,
Expand Down Expand Up @@ -922,6 +957,7 @@ async fn main() -> anyhow::Result<()> {
Some(Commands::Delete { ref folder }) => {
run_delete_prompt_mode(&args, folder.as_ref()).await
}
Some(Commands::List { ref folder }) => run_list_mode(&args, folder.as_ref()),
Some(Commands::Iconify { command }) => run_iconify_command(command).await,
None => {
let resolved = config::resolve_tui_config(
Expand Down Expand Up @@ -1036,6 +1072,41 @@ mod tests {
assert_eq!(resolved, Some(&cli_folder));
}

#[test]
fn resolve_list_folder_prefers_subcommand_folder() {
let cli_folder = PathBuf::from("src/assets/icons");
let command_folder = PathBuf::from("icons/from/list");
let cli = CliArgs {
command: None,
folder: Some(cli_folder),
preset: None,
name: None,
icon: None,
filename: None,
output_line_template: None,
};

let resolved = resolve_list_folder(&cli, Some(&command_folder));
assert_eq!(resolved, Some(&command_folder));
}

#[test]
fn resolve_list_folder_falls_back_to_global_folder() {
let cli_folder = PathBuf::from("src/assets/icons");
let cli = CliArgs {
command: None,
folder: Some(cli_folder.clone()),
preset: None,
name: None,
icon: None,
filename: None,
output_line_template: None,
};

let resolved = resolve_list_folder(&cli, None);
assert_eq!(resolved, Some(&cli_folder));
}

#[test]
fn validate_new_export_conflicts_rejects_duplicate_alias() {
let existing = "export { default as IconHeart } from './heart.svg';\n";
Expand All @@ -1046,7 +1117,11 @@ mod tests {
)
.expect_err("duplicate alias should fail");

assert!(error.to_string().contains("Icon alias 'IconHeart' already exists"));
assert!(
error
.to_string()
.contains("Icon alias 'IconHeart' already exists")
);
}

#[test]
Expand All @@ -1059,9 +1134,11 @@ mod tests {
)
.expect_err("duplicate target should fail");

assert!(error
.to_string()
.contains("Export target './heart.svg' already exists"));
assert!(
error
.to_string()
.contains("Export target './heart.svg' already exists")
);
}

#[test]
Expand Down
Loading
Loading