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 |
# 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.
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
ppicons (Prisma PHP and Caspian).
cargo-rahti-icons (Rahti).
https://ppicons.tsnc.tech. The Rahti installer shells out to curl, falling back to PowerShell on Windows, rather than carrying a TLS stack.
Installation
Global or local setup
npm install -g ppicons
npm install -D ppicons
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
npx ppicons add search
npx ppicons add search user settings
npx ppicons add --all
npx ppicons update
npx ppicons add search --force
npx ppicons add --all --force
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:
caspian.config.json exists, it uses Python mode.
prisma-php.json exists, it uses 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.
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.
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>
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>
""")
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>
}
<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".
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.
{
"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.
.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.
GET https://ppicons.tsnc.tech/icons?icon=all
GET https://ppicons.tsnc.tech/icons?icon=search
icon=all endpoint returns a JSON array of icon objects. The single-icon endpoint returns one JSON object.
{
"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.