JSPM

@blackasteroid/riu

0.3.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 3
  • Score
    100M100P100Q76648F
  • License MIT

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.

Package Exports

    This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@blackasteroid/riu) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

    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 git@github.com: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