@tendee/cli
v0.5.0
Published
CLI for vendoring Tendee icon bundles (React, Vue, Svelte, React Native, Swift, Jetpack Compose, and more) into a repository.
Maintainers
Readme
@tendee/cli
Vendors a curated icon collection from Tendee into your repository as plain source files you own and commit — no runtime dependency, no icon font, no bundle of SVGs loaded at runtime.
npx @tendee/cli sync my-collection --target react --out components/iconsThat writes one component per icon, a shared types module, and a lock file. From then on the icons are ordinary files in your project.
Install
Nothing to install — npx @tendee/cli <command> works. To pin it:
npm install --save-dev @tendee/cliRequires Node 20 or newer.
Authenticate
tendee loginOpens a browser and waits for you to approve. Tokens are stored in
~/.config/tendee/config.json and are scoped to one organization.
In CI, issue a token in the dashboard and pass it in the environment instead:
TENDEE_TOKEN=tnd_... tendee sync my-collection --target react --frozen-lockfileNever pass a token as a command-line argument — it lands in your shell history and in the process list.
Two directions
Installing a collection — someone has curated a collection and you want it in your code:
tendee collections # find the slug
tendee sync <collection> --target react --out src/icons --dry-run
tendee sync <collection> --target react --out src/iconsBuilding a collection — your project already has icons scattered around it and you are consolidating them:
tendee scan # read-only; writes .tendee/scan.json
tendee push <collection> --dry-run # preview uploads of the SVG files it found
tendee push <collection> --yes
tendee link <collection> --dry-run # preview catalog matches for icon-library imports
tendee link <collection> --yessync needs neither scan nor push nor link. You can be in the first workflow without
ever touching the second.
Adding a missing icon from the catalog
If a project needs an icon that already exists in Tendee but the project hasn't imported it yet, add it directly without having to mock up a fake import first:
tendee search heart # find the icon
tendee add my-collection heart --provider lucide-icons # add it, confirmed with --yes
tendee sync my-collection --target react # pull it back into codesearch is read-only and shows icons from the catalog with their metadata. add places an icon
into your collection; without --yes, it previews the plan first and you confirm. Both commands
require --provider — look it up in the search output. After add, run sync to materialize
it alongside the rest of your collection.
Commands
| Command | What it does |
|---|---|
| tendee login | Browser login. Requires a human |
| tendee login --stdin | Read an already-issued token from stdin (CI-friendly) |
| tendee collections [--json] | List collections in your organization |
| tendee scan [--dir .] [--json] | Read-only project scan → .tendee/scan.json |
| tendee push <collection> [--dry-run] [--yes] [--json] | Upload the SVG files the scan found |
| tendee link <collection> [--dry-run] [--yes] [--json] | Link catalog icons matching the library imports the scan found |
| tendee search <query> [--provider <org-slug>] [--style <style>] [--limit 20] [--json] | Search the Tendee catalog for icons (read-only, no --yes) |
| tendee add <collection> <name...> --provider <org-slug> [--style <style>] [--dry-run] [--yes] [--json] | Add icons from the catalog to your collection without requiring them to exist in your project |
| tendee audit <collection> [--json] | Check if the collection is a single visual family |
| tendee sync <collection> --target <target> [--out DIR] | Materialize a collection into source files |
| tendee agent init | Install a skill file so a coding agent knows how to drive this CLI |
| tendee --version | Print the installed version |
Run tendee sync --help for the full flag list. scan, push, link, search, add, collections and audit
accept --json — parse that rather than scraping the human-readable output, which is free to
change. --json works on --dry-run as well as the real invocation (except sync, which has no --json at all; its file list and rename report are text only).
JSON on the decision step
--dry-run --json exists precisely for the step where you decide whether to --yes. Before committing icons to your collection, you need the exact matching logic and any unresolved names:
{"dry_run":true,"collection":"my-set","plan":[{"input":"Trash2","base_name":"trash-2","matched_by":"digit_boundary"},{"input":"X","base_name":"x","matched_by":"exact"}],"unresolved":[{"library":"lucide-react","input":"NoSuchIcon","reason":"no_match"}],"counts":{"plan":2,"unresolved":1,"non_exact":1}}Key fields:
matched_by— how the icon name resolved:exact(perfect match),suffix_icon(removed trailingIcon),digit_boundary(split on letter→digit boundaries),alias(cross-referenced upstream aliases). Only the first kind requires no human review; the rest mean "close but check."unresolved— names that could not be matched to the catalog, with a reason (no_match,unknown_library, etc.).counts.non_exact— how many items in the plan matched by anything other than exact. Unresolved count iscounts.unresolved.
This structure exists on the preview step (above) and on the real --yes call too — so you can parse JSON consistently without switching to text scraping once you decide to proceed. The decision point is JSON; the execution is JSON.
Two things this CLI deliberately does not do. There is no delete — removing icons or collections happens in the dashboard. And there is no Figma import: pulling icons out of a Figma file is a dashboard flow too. Both are product decisions, not missing flags.
Auditing a collection
tendee audit my-collection
tendee audit my-collection --jsonAnswers the question: is this collection a single visual family? Reads metadata only (never
svg_content), so it's cheap and meant to run repeatedly as you curate. Read-only operation:
no --yes or --dry-run flags.
dogfood-run-2026-08-21: 97 icons
Render styles: outline 94, filled 3
Mixed styles in one set — usually the thing to fix before measuring anything else.
Stroke width: median 2, 94 sampled.
~ custom-bolt 6 (off by 4)
3 outline icon(s) carry no stroke measurement, so they were not compared.
Exact duplicates: none.The report prints:
- Icon count — total in the collection.
- Render styles — breakdown by
outline,filled,duotone, etc. If more than one style is present, a warning that mixed styles usually need fixing first. - Upstream sources — if more than one, notes that icons from different repositories are mixed by construction.
- Stroke width — median, sample size, and a list of outliers (only for outline icons; filled and duotone have no strokes by construction). Anchors to the actual icon slug so you can look it up in the dashboard.
- Coverage note — how many icons carry visual metadata. Without it, "no outliers" and "nothing to measure" read the same.
- Exact duplicates — groups of icons with byte-for-byte identical SVG content.
--json parses as strict JSON (audit_report object), suitable for scripting or agent use.
Targets
react, vue, svelte, web-component, react-native, swift, compose, sprite.
Default output directories, overridden by --out:
| target | default |
|---|---|
| react, vue, svelte, web-component, react-native | src/icons |
| swift | TendeeIcons |
| compose | src/main/kotlin/io/tendee/icons |
| sprite | public/icons |
A repository without a src/ directory almost certainly wants an explicit --out.
What lands on disk
For the React target:
- one
<PascalName>.tsxper icon, tendee-icon.ts, exporting theTendeeIconPropsandTendeeIcontypes,tendee.lock.jsonat the repository root,- a manifest under
.tendee/.
Pass --as-package to also get an index.ts barrel and a generated package.json.
Updating after the collection changes
Re-run the same sync. It reports what moved, and — this is the part that matters — names the
files that still import an icon which has left the collection:
Changes since the last sync:
- RotateCcwClock.tsx
+ Sparkles.tsx
~ Camera.tsx
Still imported, but no longer in the collection:
RotateCcwClock
src/components/HistoryPanel.tsx
src/components/Timeline.tsx
Point these at another icon, or put the icon back in the collection.Without that list, removing an icon in the dashboard turns into a build error somewhere else with nothing connecting the two events.
Commit the generated files and the lock. tendee.lock.json is what lets the next person
who clones the repository re-materialize the exact same icons — it is the direct analogue of
package-lock.json, not build output. .tendee/ is the opposite case: its manifest records
an absolute output path and is machine-local, so ignore it.
Do not hand-edit a generated file. Each one starts with
Generated by Tendee. Do not edit directly. That marker is load-bearing: it is how sync
decides a file is safe to overwrite, and safe to delete once its icon leaves the collection.
An edit is lost on the next sync, and a file that loses the marker is never cleaned up. To
change an icon, change the collection and sync again.
Component names are not your old library's export names
This is the most common way a migration breaks. Names come from the icon's canonical name in the registry:
| what your old library exported | what Tendee generates |
|---|---|
| AlertCircle | CircleAlert |
| Loader2 | LoaderCircle |
| History | RotateCcwClock |
Run --dry-run first. It prints the exact file list, plus a rename map. If you have run
tendee scan in this project, the map is keyed on the names in your code:
3 names in your code map to a differently named component:
AlertCircle -> CircleAlert
Loader2 -> LoaderCircle
PanelLeftIcon -> PanelLeftRewrite exactly those imports; every other binding keeps its name.
Without a scan there is nothing to key on, so the report falls back to what upstream records —
CircleAlert <- AlertCircle, read right to left. That form is weaker: it lists old upstream
names whether or not your project ever used them, and it cannot see conventions your library
invented, such as the trailing Icon in PanelLeftIcon. Run tendee scan first and you get
the better one.
Drift checking in CI
tendee sync <collection> --target react --frozen-lockfileWrites nothing. Exits non-zero if the registry has moved away from the committed lock, so a pull request can fail on icons that changed underneath it.
Already up to date means the collection has not changed since your last sync. That is a
success, not a warning.
Coding agents
tendee agent initWrites a skill file describing this CLI — .claude/skills/tendee/SKILL.md for Claude Code,
.cursor/rules/tendee.mdc for Cursor, or an AGENTS.md block otherwise.
Detection works off agent config that already exists (.claude/, .cursor/, AGENTS.md), so
a project that has none gets AGENTS.md and says so. Pass --target claude|cursor|codex to
choose.
License
MIT.
