npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@blackasteroid/riu

v0.4.0

Published

Automate uploading icon-only Rust workshop skins from the command line. Generate manifests, validate icons, and push to Steam Workshop — interactively or via JSON for agents.

Downloads

56

Readme

rust-icon-uploader (riu)

Automate uploading icon-only Rust workshop skins from the command line.

riu generates the right manifest.txt, validates your icon PNG, and pushes the workshop item to Steam — interactively (Ink TUI) or headless via JSON (agent-friendly). Every command supports --dry-run against a mock Steam client so you can exercise the whole pipeline without libsteam_api.dylib.


Why this exists

Most Rust items aren't on Facepunch's official skinnable list (UAV, Patrol Signal, etc.), so you can't make traditional texture skins for them. But there's a workaround: you can upload a workshop item with an empty Textures block and Rust will use the workshop preview image as the in-game inventory icon.

Doing this by hand is tedious. There's a Windows-only Unity tool called Rust Custom Icons that automates the upload, but if you're on macOS or want to script it from CI / an agent, you're stuck. riu is the cross-platform replacement — Node.js, no Bun, no Zig, no Windows.


Install

npm install -g @blackasteroid/riu

That's it — riu is now on your PATH. Requires Node.js 20+.

To upgrade later:

riu upgrade

riu checks npm for a newer version once a day in the background and prints a one-line notice to stderr when one is available (skipped automatically in --json mode so agents don't see noise). You can also run riu upgrade --check to query manually.

From source (development)

git clone [email protected]:BlackAsteroid/rust-icon-uploader.git
cd rust-icon-uploader
npm install
npm run build
npm link            # makes the `riu` command available globally from this checkout

Prerequisites for real uploads

--dry-run works on any machine. For real uploads you need:

  1. Steam client running, logged into the account that owns Rust (AppID 252490).
  2. libsteam_api.dylibriu init and riu install-sdk will auto-detect and copy this from your local Rust install (it ships as RustClient.app/Contents/PlugIns/libsteam_api.bundle, a flat universal Mach-O). You don't need to download anything from Valve.

If you don't have Rust installed locally, you can also:

  • Install Spacewar (free, AppID 480) which ships the SDK reference binaries
  • Pass --from <path> to riu install-sdk with a manually-acquired dylib
  • Register as a Steamworks partner (free) and download the SDK from https://partner.steamgames.com

Run riu doctor after setup to verify everything is wired up.


Quick start

# 1. one-time setup — auto-detects libsteam_api.dylib from your Rust install
riu init                           # interactive: only asks for SteamID
# or headless:
riu init --json '{"authorId":"76561198xxxxxxxxx"}' --force

# 2. verify the environment
riu doctor

# 3. generate a skin folder from one of the example icons
riu new

# 4. upload it (use --dry-run first to confirm without touching Steam)
riu upload ./skins/my-uav-skin --dry-run
riu upload ./skins/my-uav-skin

# 5. see what you've uploaded
riu list

Command reference

All commands accept --json [payload] for headless / agent use and --dry-run to use the mock Steam client.

riu init

One-time setup wizard. Asks for SDK path, SteamID64, default tags, etc., and writes them to ~/Library/Application Support/rust-icon-uploader-nodejs/config.json (macOS).

# interactive
riu init

# headless (for agents / CI)
riu init --json '{
  "steamSdkPath": "/Users/me/steamworks_sdk",
  "authorId": "76561198194158447",
  "defaultTags": ["Version3", "Skin"]
}' --force

# show current config
riu init --show
riu init --show --json

Flags:

  • --show print current config
  • --force overwrite existing config
  • --dry-run validate without writing
  • --json [payload] headless mode (payload required for write, omit for --show)

riu install-sdk

Auto-locates libsteam_api.dylib (or .bundle) from a local Steam game install and copies it to ~/.riu/sdk/libsteam_api.dylib. Updates the existing config to point at the managed location.

riu install-sdk                    # auto-locate and install
riu install-sdk --dry-run --json   # show what would be installed
riu install-sdk --from /path/to/libsteam_api.dylib   # explicit source
riu install-sdk --force            # reinstall over existing

This command is run automatically by riu init if no steamSdkPath is provided. Useful as a standalone after installing Rust on a new machine.

riu upgrade

Upgrade riu to the latest published version on npm.

riu upgrade                # full upgrade
riu upgrade --check        # check only, don't install
riu upgrade --check --json # structured output for scripting
riu upgrade --dry-run      # show the npm install command without running it

Background update checks run automatically on every command (cached 1 day) and print a stderr notice if a newer version is available. Skipped automatically when --json / --version / --help is used so output stays clean for agents.

riu doctor

Diagnostic check: verifies config exists, dylib is present, Steam is running, SteamID format is valid, AppID is set. Exits non-zero if any check fails.

riu doctor
riu doctor --json

riu new

Generates a complete skin folder (manifest.txt + icon.png).

# interactive Ink wizard
riu new

# headless
riu new --json '{
  "itemType": "Grenade",
  "title": "UAV Custom Icon",
  "description": "BusinessCore UAV signal",
  "tags": ["Version3", "Skin"],
  "iconPath": "/path/to/uav.png",
  "outputDir": "./skins/uav"
}'

# validate without writing
riu new --json '{...}' --dry-run

Icon validation (sharp): PNG, square, dimensions in {256, 512, 1024, 2048}, ≤1MB. RGBA preferred.

Known item types: Grenade (UAV / Patrol Signal), Rock, Pistol, Rifle, Shotgun, SMG, Bow, Sword, Spear, Knife, Axe, Hammer, Tool, Hat, Shirt, Pants, Boots, Gloves, Container, SleepingBag, Furniture. Pass "allowCustomItemType": true in the payload to use a string outside this list.

riu upload [skin-dir]

Uploads a single skin folder to the workshop.

# positional arg
riu upload ./skins/uav

# with overrides
riu upload ./skins/uav --title "Custom UAV v2" --tags "Version3,Skin,UAV"

# headless JSON
riu upload --json '{
  "skinDir": "./skins/uav",
  "title": "UAV Custom Icon",
  "description": "...",
  "tags": ["Version3","Skin"]
}'

# dry run with mock client (no Steam contact)
riu upload ./skins/uav --dry-run

Interactive mode shows a staged Ink progress UI (validating → creating → configuring → uploading → submitting → done). JSON mode skips the UI and emits a single result document.

riu upload-all <parent-dir>

Bulk-uploads every skin folder under a parent directory in sequence.

riu upload-all ./skins
riu upload-all ./skins --dry-run --json
riu upload-all ./skins --filter Grenade --limit 5

Resume support: on success, the returned PublishedFileId is written back into manifest.txt. On re-run, any folder whose manifest already has PublishedFileId is skipped without contacting Steam — so partial-failure recovery is automatic.

JSON mode: emits an NDJSON event stream so agents can consume progress live:

{"event":"start","total":3,"dryRun":true}
{"event":"item","index":1,"title":"uav","status":"uploading"}
{"event":"item","index":1,"title":"uav","status":"success","publishedFileId":"9000000001","url":"..."}
{"event":"item","index":2,"title":"patrol","status":"skipped","reason":"already published"}
{"event":"summary","total":3,"success":2,"skipped":1,"failed":0}

Flags:

  • --filter <itemType> only upload skins matching the category
  • --limit <n> cap number of uploads (testing)
  • --continue-on-error keep going if one fails (default: true)
  • --dry-run mock client
  • --json NDJSON output

riu list

Show skins uploaded via this tool (read from local cache).

riu list
riu list --json
riu list --filter Grenade
riu list --include-dry-run

By default, dry-run uploads are filtered out so the list reflects real workshop items only.

riu update <published-file-id>

Re-upload the icon for an existing workshop item.

# uses cached skinDir if available
riu update 3248306153 --note "icon refresh"

# or specify the folder explicitly
riu update 3248306153 --skin-dir ./skins/uav --note "tweaked colors"

# headless
riu update --json '{
  "publishedFileId": "3248306153",
  "skinDir": "./skins/uav",
  "note": "icon refresh"
}'

Agent / scripting integration

--json makes every command machine-consumable. The pattern most agents will want is:

riu init --force --json '{"steamSdkPath":"...","authorId":"..."}'
riu new --json '{"itemType":"Grenade","title":"...","iconPath":"...","outputDir":"./skin-x"}'
riu upload ./skin-x --json

Or all-at-once from a folder of pre-generated skins:

riu upload-all ./generated-skins --json | while read line; do
  echo "$line" | jq -r '. | "\(.event): \(.title // "")"'
done

Exit codes:

  • 0 success
  • 1 runtime error (missing config, Steam init failed, network)
  • 2 validation error (bad input, malformed JSON, schema failure)

Error shape in JSON mode:

{"status":"error","error":{"message":"...","field":"steamSdkPath"}}

Mock / dry-run mode

--dry-run swaps the real Steam client for a MockSteamClient that:

  • Does not load libsteam_api.dylib (works on any machine)
  • Does not require Steam to be running
  • Returns synthetic 9_xxx_xxx_xxx PublishedFileIds
  • Writes cache entries with dryRun: true so riu list filters them by default

This means scripts/e2e-dry-run.sh works on any machine, in CI, anywhere — no Steam SDK required. Useful for:

  • CI smoke tests
  • Agent tool development
  • Validating manifests before a real run
  • Demos

How the icon-only skin trick works

A normal Rust workshop skin replaces an item's textures (diffuse, normal, etc.) by setting non-empty entries in the manifest's Textures dictionary. Rust's workshop client downloads those textures, applies them to the item's material at runtime, and the item looks different in-world.

For items NOT on Facepunch's skinnable list, that texture-replacement path doesn't work — Rust has no skinnable material for them. But the workshop client still downloads the preview image of every workshop item the player is subscribed to and uses it as the inventory icon. So if you upload a workshop item with:

{
  "Version": 3,
  "ItemType": "Grenade",
  "Groups": [{
    "Textures": {},     ← empty, no texture overrides
    "Floats": {...},
    "Colors": {...}
  }]
}

…Rust will recognize the workshop item, see no textures to apply, and just use the preview image as the in-game icon for items of that ItemType. That's the entire trick.


Project layout

src/
├── cli.tsx                 # commander entrypoint
├── commands/               # one file per CLI command
│   ├── init.tsx
│   ├── doctor.tsx
│   ├── new.tsx
│   ├── upload.tsx
│   ├── list.tsx
│   ├── update.tsx
│   └── upload-all.tsx
├── ui/                     # Ink components
│   ├── InitWizard.tsx
│   ├── NewSkinWizard.tsx
│   ├── UploadProgress.tsx
│   └── BulkUploadProgress.tsx
├── steam/                  # Steam UGC layer (mockable)
│   ├── types.ts
│   ├── client.ts           # real, wraps steamworks-ffi-node
│   └── mock.ts             # fake, used by --dry-run
├── manifest/               # pure functions, unit tested
│   ├── itemTypes.ts
│   ├── builder.ts
│   ├── writer.ts
│   └── __tests__/
├── config/                 # conf-backed user config + Zod schema
├── cache/                  # uploads.json local cache
└── utils/                  # iconValidator, slugify

Development

npm run dev -- new           # run via tsx without building
npm run build                # tsc → dist/
npm run typecheck            # tsc --noEmit
npm test                     # node:test unit tests
bash scripts/e2e-dry-run.sh  # full dry-run smoke test

Test config gets stored in a temp dir via RIU_CONFIG_DIR so it never touches your real config.


Troubleshooting

Steamworks SDK not ready: no Steam library found The path you gave riu init doesn't contain libsteam_api.dylib. Run riu doctor for a clear report.

Steam init failed Steam isn't running, or the account isn't logged in, or it doesn't own AppID 252490 (Rust). The steam_appid.txt file is automatically managed in cwd — leftover copies are deleted on riu shutdown.

createItem returned null Steam refused to create the workshop item. Common cause: the account hasn't accepted the workshop legal agreement. Visit any workshop submission page in a browser to accept it.

icon validation failed: must be square Resize your PNG to 256/512/1024/2048 px. Sharp can do it: npx sharp-cli resize 512 512 < bad.png > good.png.


License

MIT