CLI Reference

Installing icons

One catalog, three targets. Instead of copying SVG markup by hand, an installer downloads icon definitions from https://ppicons.tsnc.tech, normalizes the SVG, writes a framework-native component file into the project's own source tree, and refreshes AI-aware metadata that documents the installed set.

Two installers read that catalog. ppicons is an npm CLI that writes PHP for Prisma PHP and Python for Caspian. cargo-rahti-icons is a cargo subcommand that writes Rust for Rahti. They share the icon set and nothing else — separate toolchains, separate commands, separate manifests.

Target Installer Writes
Prisma PHP npx ppicons src/Lib/PPIcons/<Name>.php
Caspian npx ppicons src/lib/ppicons/<Name>.py
Rahti cargo rahti-icons src/components/rahti_icons/<name>.rs
Quick Examples Terminal
# Prisma PHP and Caspian
npx ppicons add search
npx ppicons add anchor globe rocket
npx ppicons add --all
npx ppicons update

# Rahti
cargo rahti-icons add search
cargo rahti-icons update

Why Use It

Turn icons into first-class components

UI icon usage gets messy fast when SVGs are copied inline across templates and components. ppicons keeps the icon layer predictable by installing normalized, reusable components into the project.

One source of truth per installed icon.
Consistent SVG normalization and attribute handling.
Predictable file names and import paths across supported frameworks.
Easier refactoring, reuse, and search in the editor.
Machine-readable and AI-readable project metadata for tooling.

Features

What the CLI covers

Capability What you get
Single icon generation ppicons add <name> downloads and writes one component file.
Bulk generation ppicons add --all generates the full icon catalog in one run.
Update installed icons ppicons update refreshes only the icons already installed in the project.
Remove installed icons cargo rahti-icons remove <icon> deletes the file. The npm CLI has no remove — delete the generated file yourself.
Browse the catalog cargo rahti-icons list --search arrow and --installed. The npm CLI has no list — query the catalog API directly.
Multi-language support PHP for Prisma PHP, Python for Caspian, Rust for Rahti.
Auto-detect target ppicons detects caspian.config.json or prisma-php.json. cargo rahti-icons finds rahti.config.json.
Force overwrite --force overwrites existing generated files in both installers.
AI-aware outputs ppicons.json plus its instructions file and AGENTS.md block; rahti-icons.json plus .github/instructions/rahti-icons.instructions.md.
Catalog discovery Documents and exposes the remote icon catalog endpoints used by the CLI.

Requirements

Runtime prerequisites

Node.js 18+ for ppicons (Prisma PHP and Caspian).
Rust 1.88+ and edition 2024 for cargo-rahti-icons (Rahti).
Network access to https://ppicons.tsnc.tech. The Rahti installer shells out to curl, falling back to PowerShell on Windows, rather than carrying a TLS stack.
A Prisma PHP, Caspian, or Rahti project when using the generated components in a real app.

Installation

Global or local setup

Global install npm
npm install -g ppicons
Local dev dependency npm
npm install -D ppicons
Rahti cargo
Two crates. rahti-icons is the runtime the generated files call into; cargo-rahti-icons is the installer. They release on one version, and a file written by one version of the installer is read by the runtime of the same version.
cargo add rahti-icons
cargo install cargo-rahti-icons

Commands

Common CLI workflows

Add one icon CLI
npx ppicons add search
Add multiple icons CLI
npx ppicons add search user settings
Add the full icon set CLI
npx ppicons add --all
Update installed icons CLI
npx ppicons update
Force overwrite CLI
npx ppicons add search --force
npx ppicons add --all --force
Rahti cargo
Same verbs, plus remove and list. update takes no names — it re-fetches everything installed, and naming icons is add --force instead.
cargo rahti-icons add search
cargo rahti-icons add search user settings
cargo rahti-icons add --all
cargo rahti-icons add search --force
cargo rahti-icons update
cargo rahti-icons remove search
cargo rahti-icons list --search arrow
cargo rahti-icons list --installed
cargo rahti-icons add --all installs over 1,600 files and a long compile. Install what a page uses instead.

Flags

Supported switches

Flag Description
--all Generate every icon from the remote catalog.
--force Overwrite existing generated icon files.
--lang php Force Prisma PHP and PHPX output.
--lang py Force Caspian Python output.

cargo rahti-icons

The Rahti installer has no --lang: a Rahti project is the only thing it writes into. It takes a --dir instead, and refuses a path outside src/components/.

Flag Description
--all, -a Every icon in the catalog. Takes no names alongside it.
--force, -f Re-fetch icons already installed. Without it they are left alone.
--dir <path> Move icons within src/components/. Outside it the file is declared by no generated mod.rs and is callable from nowhere, so the flag is refused.
--search <text> Narrow what list prints.
--installed Make list read this project instead of the catalog.

Language Selection

How mode detection works

If you do not pass --lang, ppicons auto-detects the target mode using the project root:

If caspian.config.json exists, it uses Python mode.
Else if prisma-php.json exists, it uses PHP mode.
Else it falls back to PHP mode.

cargo rahti-icons does not take part in this. It looks for rahti.config.json in the current directory or any directory above it, and stops with an error when there is none — there is no fallback mode, because there is only one language it writes.

Explicit examples CLI
npx ppicons add search --lang php
npx ppicons add search --lang py

Generated Output Locations

Where the components land

By default, ppicons writes icon components into src based on the selected language mode.

Mode Output directory
Prisma PHP and PHPX src/Lib/PPIcons/<ComponentName>.php
Caspian Python src/lib/ppicons/<ComponentName>.py
Rahti Rust src/components/rahti_icons/<module_name>.rs

ppicons converts icon names to PascalCase file names: search becomes Search.php or Search.py, and chevron-right becomes ChevronRight.php or ChevronRight.py.

Rahti needs a Rust module name, so the file is snake_case: chevron-right becomes chevron_right.rs, exporting a ChevronRight component. Two shapes that would not compile are handled at install time rather than at the far end of a cargo build — a leading digit takes an underscore, and a Rust keyword takes an _icon suffix:

Catalog name File Component
search search.rs Search
arrow-right arrow_right.rs ArrowRight
box box_icon.rs Box
1st-place-medal _1st_place_medal.rs 1stPlaceMedal

Both names are derived from the catalog slug, never from its componentName — the catalog is wrong for a handful of entries (bot-off is served as BotMessageSquare), and deriving is what keeps two icons from colliding on a name belonging to neither.

Using Generated Icons

Importing and rendering

Prisma PHP and Caspian both render icons as HTML-first x- tags, and both require the component to be imported in the owning file first — the import is what makes the tag resolve. Rahti does not use x- tags at all: an icon is a Rust component called by its PascalCase name.

Prisma PHP and PHPX Usage
Generated PHP icons are class-based PHPX components. Import the classes, then render the x- tag. Grouped imports are preferred when several icons come from the same namespace.
<?php

use Lib\PPIcons\{ArrowRight, Search};

?>

<div>
    <x-search />
    <x-arrow-right class="size-4" />
</div>
Caspian Usage
Generated Caspian icons are Python components, one per module. Import each one from the module that authors it, then use the x- tag inside html(r"""..."""). Caspian has no HTML-sidecar or comment import syntax.
from casp.component_decorator import component, html
from src.lib.ppicons.Search import Search
from src.lib.ppicons.ArrowRight import ArrowRight


@component
def icon_actions():
    return html(r"""
    <div>
      <x-search />
      <x-arrow-right class="size-4" />
    </div>
    """)
Rahti Usage
Generated Rahti icons are Rust components under crate::components::rahti_icons, where the Rahti build already looks — there is no wiring to do after installing one. The tag is the PascalCase component name, not an x- tag.
use crate::components::rahti_icons::arrow_right::ArrowRight;
use crate::components::rahti_icons::search::Search;

html! {
    <button class="inline-flex items-center gap-2 rounded-md px-3 py-2">
        <Search class="size-4" />
        "Search"
        <ArrowRight class="size-4" />
    </button>
}
A Rahti icon renders one <svg> element with nothing around it — no wrapper and no fragment markers — so button > svg and [&>svg]:size-4 reach it.

Rahti Props

What a Rahti icon accepts

Every prop is optional, so a tag names what it changes and nothing else. Values are Rust expressions, which is why a number is written as a string — size="20", not size=20 — the same as every other Rahti prop that takes a &str.

Prop Does
class Adds classes to the icon's own.
size Sets width and height together.
width, height Sets one of them. Naming either beats naming size.
stroke_width Sets stroke-width.
color Paints the icon on whichever of stroke and fill currently reads currentColor — both, if both do.
fill Sets fill outright.
label The accessible name, as role="img" and aria-label.
attrs An Attrs set — any other attribute, and removals.
<User size="20" color="#4f46e5" label="Your profile" />
<User class="size-4 opacity-70" attrs=@{Attrs::new().set("id", "avatar").unset("stroke-width")} />
<User class="size-4" onclick={openMenu()} />

Props are applied over the icon's own attributes in a fixed order: size, then stroke_width, color, fill, class, label, then attrs, and finally any client {...} binding, which wins outright — a class={...} binding replaces the class rather than joining it. An icon that nothing above gave an accessible name, a role, or an aria-hidden of its own is decoration, and renders aria-hidden="true".

src/components/rahti_icons/search.rs Generated
What lands in the repository is data and one icon! invocation. Everything deciding how an icon behaves lives in the rahti-icons runtime, so upgrading the crate changes behavior without re-fetching a thing. The file is generated: it carries no hand-written code, and cargo rahti-icons update overwrites it.
// @generated by cargo-rahti-icons 0.0.4 - do not edit.
//
// The `search` icon, fetched from https://ppicons.tsnc.tech/icons.
// Re-fetch it with `cargo rahti-icons update`; delete this file to remove it.
//
// Props: class, size, width, height, stroke_width, color, fill, label, attrs -
// plus any `{...}` binding written on the tag. See the `rahti-icons` crate.

::rahti_icons::icon! {
    /// The `search` icon.
    Search {
        name: "search",
        attrs: [
            ("xmlns", "http://www.w3.org/2000/svg"),
            ("width", "24"),
            ("height", "24"),
            ("viewBox", "0 0 24 24"),
            ("fill", "none"),
            ("stroke", "currentColor"),
            ("stroke-width", "2"),
            ("stroke-linecap", "round"),
            ("stroke-linejoin", "round"),
            ("class", "lucide lucide-search"),
        ],
        body: "<circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/>",
    }
}

AI-aware Project Files

What gets refreshed on add or update

Every successful add or update refreshes project metadata files that help editors, scripts, and AI tools understand the icon setup.

ppicons.json Manifest
This file is the core machine-readable inventory. It includes project type and language mode, framework config metadata, icon output directories, supported commands, remote catalog API endpoints, installed icon inventory, and import entry metadata for the current project mode.
{
  "schemaVersion": 7,
  "generatedAt": "2026-08-29T00:00:00.000Z",
  "project": {
    "type": "prisma-php",
    "framework": "prisma-php",
    "language": "php",
    "detectedBy": "prisma-php.json",
    "rootDirectory": ".",
    "sourceDirectory": "src",
    "configFile": "prisma-php.json",
    "manifestFile": "ppicons.json",
    "componentsDirectory": "src/Lib/PPIcons",
    "iconsDirectory": "src/Lib/PPIcons",
    "copilotInstructionsFile": ".github/instructions/ppicons.instructions.md",
    "agentsFile": "AGENTS.md"
  },
  "commands": {
    "addOne": "npx ppicons add <icon-name>",
    "addMany": "npx ppicons add <icon-a> <icon-b>",
    "addAll": "npx ppicons add --all",
    "updateInstalled": "npx ppicons update"
  },
  "catalogApi": {
    "listAll": {
      "method": "GET",
      "url": "https://ppicons.tsnc.tech/icons?icon=all",
      "returns": "IconRecord[]",
      "purpose": "List all available icons that can be installed."
    },
    "getOne": {
      "method": "GET",
      "urlTemplate": "https://ppicons.tsnc.tech/icons?icon=<icon-name>",
      "exampleUrl": "https://ppicons.tsnc.tech/icons?icon=search",
      "returns": "IconRecord",
      "purpose": "Fetch one icon by name before installing it."
    },
    "responseFields": {
      "id": "number",
      "name": "string",
      "componentName": "string",
      "svg": "string",
      "createdAt": "number",
      "updatedAt": "number"
    }
  },
  "usage": {
    "componentType": "class",
    "entryStyle": "namespace",
    "entry": "Lib\\PPIcons",
    "filePattern": "src/Lib/PPIcons/<ComponentName>.php",
    "syntax": "jsx-like component tags"
  },
  "icons": [
    {
      "name": "search",
      "componentName": "Search",
      "file": "src/Lib/PPIcons/Search.php"
    }
  ]
}

.github/instructions/ppicons.instructions.md

This dedicated instruction file is generated for GitHub Copilot. It includes install commands for new icons, icon discovery API guidance, project-specific usage examples, and notes about the current icon directory and import entry.

AGENTS.md

ppicons writes the same managed AI context here for agents that look for repository guidance at the root. Anything outside the <!-- ppicons:start --> and <!-- ppicons:end --> markers is preserved.

.github/copilot-instructions.md

This top-level file is left available for project-owned, always-on Copilot notes. ppicons does not generate or overwrite it.

rahti-icons.json Manifest
The Rahti installer keeps its own manifest at the project root, alongside .github/instructions/rahti-icons.instructions.md. Every command that changes what is installed rewrites both, so whatever reads the project — a person, a script, a coding agent — is told what is there and how to add more. Nothing generated carries a timestamp: these files change when the icon set changes and not otherwise.
{
  "schema": 1,
  "generatedWith": "0.0.4",
  "project": {
    "config": "rahti.config.json",
    "iconsDirectory": "src/components/rahti_icons",
    "modulePath": "crate::components::rahti_icons",
    "runtimeCrate": "rahti-icons"
  },
  "commands": {
    "addOne": "cargo rahti-icons add <icon>",
    "addMany": "cargo rahti-icons add <icon-a> <icon-b>",
    "addAll": "cargo rahti-icons add --all",
    "update": "cargo rahti-icons update",
    "remove": "cargo rahti-icons remove <icon>",
    "list": "cargo rahti-icons list"
  },
  "usage": {
    "import": "use crate::components::rahti_icons::<module>::<Component>;",
    "tag": "<Component class=\"size-4\" />",
    "props": [
      "class", "size", "width", "height", "stroke_width",
      "color", "fill", "label", "attrs"
    ],
    "bindings": "Client `{...}` expressions written on the tag land on the <svg>."
  },
  "icons": [
    {
      "name": "search",
      "component": "Search",
      "module": "crate::components::rahti_icons::search",
      "file": "src/components/rahti_icons/search.rs"
    }
  ]
}

Icon Discovery API

Inspect the remote catalog directly

If you need to know which icon names are available before installation, use the remote catalog API. Both installers read these same two endpoints, so an icon name that works for npx ppicons add works for cargo rahti-icons add as well. In a Rahti project, cargo rahti-icons list --search <text> queries the catalog for you.

Fetch all icons GET
GET https://ppicons.tsnc.tech/icons?icon=all
Fetch one icon by name GET
GET https://ppicons.tsnc.tech/icons?icon=search
The icon=all endpoint returns a JSON array of icon objects. The single-icon endpoint returns one JSON object.
Example response JSON
{
  "id": 166531,
  "name": "search",
  "componentName": "Search",
  "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-search\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg>",
  "createdAt": 1774923647142,
  "updatedAt": 1774923647142
}
Field Meaning
id Numeric record id.
name Icon name used by ppicons add <name>.
componentName PascalCase component name that will be generated.
svg Raw SVG markup returned by the catalog.
createdAt Creation timestamp.
updatedAt Update timestamp.

Troubleshooting

Common recovery paths

I wanted PHP mode but Caspian was detected

Force PHP output explicitly:

npx ppicons add search --lang php

Existing files were not overwritten

Use --force:

npx ppicons add search --force

ppicons update says no icon components were found

This means the target icon directory exists but no generated icon files matching the current language mode were found yet. Run an add command first:

npx ppicons add search

Network fetch failed

Check that internet access is available.

Check that your firewall or proxy allows requests to https://ppicons.tsnc.tech.

Check that the icon name exists in the remote catalog.

For Rahti, check that curl is on PATH; on Windows the installer falls back to PowerShell.

cargo rahti-icons cannot find the project

It looks for rahti.config.json in the current directory and every directory above it. Run the command from inside the Rahti application, and confirm what is installed with:

cargo rahti-icons list --installed

--dir was refused

Icons have to stay under src/components/, because that is the tree the Rahti build walks. A file elsewhere is declared by no generated mod.rs and is callable from nowhere. Move it within that tree instead:

cargo rahti-icons add search --dir src/components/icons

A Rahti icon stopped compiling after an upgrade

rahti and rahti-icons release in lockstep and must be held at the pair released together. Cargo reads a 0.0.z requirement as its own incompatible range, so mixing versions pulls two copies of rahti into the graph and the generated icons stop compiling — typically as the trait bound rahti_icons::Attrs: Attributes is not satisfied, an error naming neither version. Move both crates together.

Contributing

Pull requests and issues are welcome. If you change generation behavior, update the tests and keep the README aligned with the actual CLI behavior.

License

Released under the MIT License.

Author

The Steel Ninja Code

thesteelninjacode@gmail.com