{"_id":"@blackasteroid/riu","_rev":"3-de062a43b8b40482a4947ee481e4cc58","name":"@blackasteroid/riu","dist-tags":{"latest":"0.4.0"},"versions":{"0.2.0":{"name":"@blackasteroid/riu","version":"0.2.0","keywords":["rust","facepunch","steam","workshop","skins","cli"],"author":{"name":"Pablo Morales"},"license":"MIT","_id":"@blackasteroid/riu@0.2.0","maintainers":[{"name":"pablofmorales","email":"pablofmorales@gmail.com"}],"homepage":"https://github.com/BlackAsteroid/rust-icon-uploader#readme","bugs":{"url":"https://github.com/BlackAsteroid/rust-icon-uploader/issues"},"bin":{"riu":"dist/cli.js"},"dist":{"shasum":"c17c3bf8518fc0813cbfe1ae5b1cee23dbb7969a","tarball":"https://registry.npmjs.org/@blackasteroid/riu/-/riu-0.2.0.tgz","fileCount":61,"integrity":"sha512-o8qF4qK8pi9BaSEGs9RX3SLgscXQ4aq/mSwumCrqPLXM3PHKFsbLqgiKGFEyVnKezB/7l+Grzva6HtFMwQFZXA==","signatures":[{"sig":"MEQCIHY4LKYwdTwdMtTkwk6QuO8DcFSDudAFftJ0WJpZIZiPAiAS7zS7VTRxoHhTQ35uUvYh7JrsRBnNm7CP+8O4gVLhhA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":223918},"type":"module","engines":{"node":">=20"},"gitHead":"ae5bdb120219ceb0a0d6b2b5a0c54bdeb40f8895","scripts":{"dev":"tsx src/cli.tsx","test":"node --test --import tsx 'src/**/__tests__/*.test.ts'","build":"tsc","start":"node dist/cli.js","typecheck":"tsc --noEmit","prepublishOnly":"npm run build"},"_npmUser":{"name":"pablofmorales","email":"pablofmorales@gmail.com"},"repository":{"url":"git+https://github.com/BlackAsteroid/rust-icon-uploader.git","type":"git"},"_npmVersion":"10.9.7","description":"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.","directories":{},"_nodeVersion":"22.22.2","dependencies":{"ink":"^5.0.1","zod":"^3.23.8","conf":"^13.0.1","react":"^18.3.1","sharp":"^0.33.5","commander":"^12.1.0","ink-table":"^3.1.0","ink-spinner":"^5.0.0","ink-text-input":"^6.0.0","update-notifier":"^7.3.1","ink-select-input":"^6.0.0","steamworks-ffi-node":"^0.10.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.1","typescript":"^5.6.2","@types/node":"^22.7.4","@types/react":"^18.3.11","@types/update-notifier":"^6.0.8"},"_npmOperationalInternal":{"tmp":"tmp/riu_0.2.0_1775673421929_0.7199522482129492","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@blackasteroid/riu","version":"0.3.0","keywords":["rust","facepunch","steam","workshop","skins","cli"],"author":{"name":"Pablo Morales"},"license":"MIT","_id":"@blackasteroid/riu@0.3.0","maintainers":[{"name":"pablofmorales","email":"pablofmorales@gmail.com"}],"homepage":"https://github.com/BlackAsteroid/rust-icon-uploader#readme","bugs":{"url":"https://github.com/BlackAsteroid/rust-icon-uploader/issues"},"bin":{"riu":"dist/cli.js"},"dist":{"shasum":"976767abd9cf9177c71387b7bd1b80f6b0158214","tarball":"https://registry.npmjs.org/@blackasteroid/riu/-/riu-0.3.0.tgz","fileCount":65,"integrity":"sha512-HyAYYmWeI3X0mATQflnzjMd2EfVCesWjySLa4HT1f6kFF/S0Kila1X2Mo2ltMynrrBeBlD6ZOqP1otr4Q6n6Ew==","signatures":[{"sig":"MEUCIQD5hYR7knXVKjzW5yhat72iv8ZIndK9F+PVF/cYh7hP9AIgGeXhfiS5lggZnL0b9FsrOrsf84FpZVsm7xf1Ilo9WBM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":271800},"type":"module","engines":{"node":">=20"},"gitHead":"6707e5e60ff495297533cd7f45b76cb3b27a5366","scripts":{"dev":"tsx src/cli.tsx","test":"node --test --import tsx 'src/**/__tests__/*.test.ts'","build":"tsc","start":"node dist/cli.js","typecheck":"tsc --noEmit","prepublishOnly":"npm run build"},"_npmUser":{"name":"pablofmorales","email":"pablofmorales@gmail.com"},"repository":{"url":"git+https://github.com/BlackAsteroid/rust-icon-uploader.git","type":"git"},"_npmVersion":"10.9.7","description":"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.","directories":{},"_nodeVersion":"22.22.2","dependencies":{"ink":"^5.0.1","zod":"^3.23.8","conf":"^13.0.1","react":"^18.3.1","sharp":"^0.33.5","commander":"^12.1.0","ink-table":"^3.1.0","ink-spinner":"^5.0.0","ink-text-input":"^6.0.0","update-notifier":"^7.3.1","ink-select-input":"^6.0.0","steamworks-ffi-node":"^0.10.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.1","typescript":"^5.6.2","@types/node":"^22.7.4","@types/react":"^18.3.11","@types/update-notifier":"^6.0.8"},"_npmOperationalInternal":{"tmp":"tmp/riu_0.3.0_1775674665642_0.11402296745618368","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@blackasteroid/riu","version":"0.4.0","description":"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.","type":"module","bin":{"riu":"dist/cli.js"},"publishConfig":{"access":"public"},"scripts":{"dev":"tsx src/cli.tsx","build":"tsc","start":"node dist/cli.js","typecheck":"tsc --noEmit","test":"node --test --import tsx 'src/**/__tests__/*.test.ts'","prepublishOnly":"npm run build"},"engines":{"node":">=20"},"keywords":["rust","facepunch","steam","workshop","skins","cli"],"author":{"name":"Pablo Morales"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/BlackAsteroid/rust-icon-uploader.git"},"homepage":"https://github.com/BlackAsteroid/rust-icon-uploader#readme","bugs":{"url":"https://github.com/BlackAsteroid/rust-icon-uploader/issues"},"dependencies":{"commander":"^12.1.0","conf":"^13.0.1","ink":"^5.0.1","ink-select-input":"^6.0.0","ink-spinner":"^5.0.0","ink-table":"^3.1.0","ink-text-input":"^6.0.0","react":"^18.3.1","sharp":"^0.33.5","steamworks-ffi-node":"^0.10.2","update-notifier":"^7.3.1","zod":"^3.23.8"},"devDependencies":{"@types/node":"^22.7.4","@types/react":"^18.3.11","@types/update-notifier":"^6.0.8","tsx":"^4.19.1","typescript":"^5.6.2"},"_id":"@blackasteroid/riu@0.4.0","gitHead":"ac34c11f58d5fb75e21f296d21c72ac3a0ef5d07","_nodeVersion":"22.22.2","_npmVersion":"10.9.7","dist":{"integrity":"sha512-SXabD2QgKLad1zTFSpJRwt53WAZe6Wu2JZf5XERazHoMftMKY14NZcWvPcibwDsGcTDsJWmUM3nsW8+K+zNWCA==","shasum":"1702141d795fcaa719638ee2e55101d59d7092c1","tarball":"https://registry.npmjs.org/@blackasteroid/riu/-/riu-0.4.0.tgz","fileCount":67,"unpackedSize":299497,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCfr+VOBPNkjcq0kkZ7OV9J0LC/9C/ESJLEl4dQPxZmiwIgbXr8FEY6oLUgsumXX5khMS/FuxYh9vCmIRzkWDB2ml4="}]},"_npmUser":{"name":"pablofmorales","email":"pablofmorales@gmail.com"},"directories":{},"maintainers":[{"name":"pablofmorales","email":"pablofmorales@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/riu_0.4.0_1775695472532_0.9889479747882644"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-08T18:37:01.799Z","modified":"2026-04-09T00:44:32.859Z","0.2.0":"2026-04-08T18:37:02.091Z","0.3.0":"2026-04-08T18:57:45.830Z","0.4.0":"2026-04-09T00:44:32.714Z"},"bugs":{"url":"https://github.com/BlackAsteroid/rust-icon-uploader/issues"},"author":{"name":"Pablo Morales"},"license":"MIT","homepage":"https://github.com/BlackAsteroid/rust-icon-uploader#readme","keywords":["rust","facepunch","steam","workshop","skins","cli"],"repository":{"type":"git","url":"git+https://github.com/BlackAsteroid/rust-icon-uploader.git"},"description":"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.","maintainers":[{"name":"pablofmorales","email":"pablofmorales@gmail.com"}],"readme":"# rust-icon-uploader (`riu`)\n\nAutomate uploading **icon-only Rust workshop skins** from the command line.\n\n`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`.\n\n---\n\n## Why this exists\n\nMost 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.\n\nDoing 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.\n\n---\n\n## Install\n\n```bash\nnpm install -g @blackasteroid/riu\n```\n\nThat's it — `riu` is now on your PATH. Requires **Node.js 20+**.\n\nTo upgrade later:\n\n```bash\nriu upgrade\n```\n\n`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.\n\n### From source (development)\n\n```bash\ngit clone git@github.com:BlackAsteroid/rust-icon-uploader.git\ncd rust-icon-uploader\nnpm install\nnpm run build\nnpm link            # makes the `riu` command available globally from this checkout\n```\n\n---\n\n## Prerequisites for real uploads\n\n`--dry-run` works on any machine. For real uploads you need:\n\n1. **Steam client running**, logged into the account that owns Rust (AppID 252490).\n2. **`libsteam_api.dylib`** — `riu 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.\n\nIf you don't have Rust installed locally, you can also:\n- Install Spacewar (free, AppID 480) which ships the SDK reference binaries\n- Pass `--from <path>` to `riu install-sdk` with a manually-acquired dylib\n- Register as a Steamworks partner (free) and download the SDK from <https://partner.steamgames.com>\n\nRun `riu doctor` after setup to verify everything is wired up.\n\n---\n\n## Quick start\n\n```bash\n# 1. one-time setup — auto-detects libsteam_api.dylib from your Rust install\nriu init                           # interactive: only asks for SteamID\n# or headless:\nriu init --json '{\"authorId\":\"76561198xxxxxxxxx\"}' --force\n\n# 2. verify the environment\nriu doctor\n\n# 3. generate a skin folder from one of the example icons\nriu new\n\n# 4. upload it (use --dry-run first to confirm without touching Steam)\nriu upload ./skins/my-uav-skin --dry-run\nriu upload ./skins/my-uav-skin\n\n# 5. see what you've uploaded\nriu list\n```\n\n---\n\n## Command reference\n\nAll commands accept `--json [payload]` for headless / agent use and `--dry-run` to use the mock Steam client.\n\n### `riu init`\n\nOne-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).\n\n```bash\n# interactive\nriu init\n\n# headless (for agents / CI)\nriu init --json '{\n  \"steamSdkPath\": \"/Users/me/steamworks_sdk\",\n  \"authorId\": \"76561198194158447\",\n  \"defaultTags\": [\"Version3\", \"Skin\"]\n}' --force\n\n# show current config\nriu init --show\nriu init --show --json\n```\n\nFlags:\n- `--show` print current config\n- `--force` overwrite existing config\n- `--dry-run` validate without writing\n- `--json [payload]` headless mode (payload required for write, omit for `--show`)\n\n### `riu install-sdk`\n\nAuto-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.\n\n```bash\nriu install-sdk                    # auto-locate and install\nriu install-sdk --dry-run --json   # show what would be installed\nriu install-sdk --from /path/to/libsteam_api.dylib   # explicit source\nriu install-sdk --force            # reinstall over existing\n```\n\nThis command is run automatically by `riu init` if no `steamSdkPath` is provided. Useful as a standalone after installing Rust on a new machine.\n\n### `riu upgrade`\n\nUpgrade riu to the latest published version on npm.\n\n```bash\nriu upgrade                # full upgrade\nriu upgrade --check        # check only, don't install\nriu upgrade --check --json # structured output for scripting\nriu upgrade --dry-run      # show the npm install command without running it\n```\n\nBackground 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.\n\n### `riu doctor`\n\nDiagnostic check: verifies config exists, dylib is present, Steam is running, SteamID format is valid, AppID is set. Exits non-zero if any check fails.\n\n```bash\nriu doctor\nriu doctor --json\n```\n\n### `riu new`\n\nGenerates a complete skin folder (`manifest.txt` + `icon.png`).\n\n```bash\n# interactive Ink wizard\nriu new\n\n# headless\nriu new --json '{\n  \"itemType\": \"Grenade\",\n  \"title\": \"UAV Custom Icon\",\n  \"description\": \"BusinessCore UAV signal\",\n  \"tags\": [\"Version3\", \"Skin\"],\n  \"iconPath\": \"/path/to/uav.png\",\n  \"outputDir\": \"./skins/uav\"\n}'\n\n# validate without writing\nriu new --json '{...}' --dry-run\n```\n\n**Icon validation** (sharp): PNG, square, dimensions in {256, 512, 1024, 2048}, ≤1MB. RGBA preferred.\n\n**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.\n\n### `riu upload [skin-dir]`\n\nUploads a single skin folder to the workshop.\n\n```bash\n# positional arg\nriu upload ./skins/uav\n\n# with overrides\nriu upload ./skins/uav --title \"Custom UAV v2\" --tags \"Version3,Skin,UAV\"\n\n# headless JSON\nriu upload --json '{\n  \"skinDir\": \"./skins/uav\",\n  \"title\": \"UAV Custom Icon\",\n  \"description\": \"...\",\n  \"tags\": [\"Version3\",\"Skin\"]\n}'\n\n# dry run with mock client (no Steam contact)\nriu upload ./skins/uav --dry-run\n```\n\nInteractive mode shows a staged Ink progress UI (validating → creating → configuring → uploading → submitting → done). JSON mode skips the UI and emits a single result document.\n\n### `riu upload-all <parent-dir>`\n\nBulk-uploads every skin folder under a parent directory in sequence.\n\n```bash\nriu upload-all ./skins\nriu upload-all ./skins --dry-run --json\nriu upload-all ./skins --filter Grenade --limit 5\n```\n\n**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.\n\n**JSON mode**: emits an NDJSON event stream so agents can consume progress live:\n\n```jsonl\n{\"event\":\"start\",\"total\":3,\"dryRun\":true}\n{\"event\":\"item\",\"index\":1,\"title\":\"uav\",\"status\":\"uploading\"}\n{\"event\":\"item\",\"index\":1,\"title\":\"uav\",\"status\":\"success\",\"publishedFileId\":\"9000000001\",\"url\":\"...\"}\n{\"event\":\"item\",\"index\":2,\"title\":\"patrol\",\"status\":\"skipped\",\"reason\":\"already published\"}\n{\"event\":\"summary\",\"total\":3,\"success\":2,\"skipped\":1,\"failed\":0}\n```\n\nFlags:\n- `--filter <itemType>` only upload skins matching the category\n- `--limit <n>` cap number of uploads (testing)\n- `--continue-on-error` keep going if one fails (default: true)\n- `--dry-run` mock client\n- `--json` NDJSON output\n\n### `riu list`\n\nShow skins uploaded via this tool (read from local cache).\n\n```bash\nriu list\nriu list --json\nriu list --filter Grenade\nriu list --include-dry-run\n```\n\nBy default, dry-run uploads are filtered out so the list reflects real workshop items only.\n\n### `riu update <published-file-id>`\n\nRe-upload the icon for an existing workshop item.\n\n```bash\n# uses cached skinDir if available\nriu update 3248306153 --note \"icon refresh\"\n\n# or specify the folder explicitly\nriu update 3248306153 --skin-dir ./skins/uav --note \"tweaked colors\"\n\n# headless\nriu update --json '{\n  \"publishedFileId\": \"3248306153\",\n  \"skinDir\": \"./skins/uav\",\n  \"note\": \"icon refresh\"\n}'\n```\n\n---\n\n## Agent / scripting integration\n\n`--json` makes every command machine-consumable. The pattern most agents will want is:\n\n```bash\nriu init --force --json '{\"steamSdkPath\":\"...\",\"authorId\":\"...\"}'\nriu new --json '{\"itemType\":\"Grenade\",\"title\":\"...\",\"iconPath\":\"...\",\"outputDir\":\"./skin-x\"}'\nriu upload ./skin-x --json\n```\n\nOr all-at-once from a folder of pre-generated skins:\n\n```bash\nriu upload-all ./generated-skins --json | while read line; do\n  echo \"$line\" | jq -r '. | \"\\(.event): \\(.title // \"\")\"'\ndone\n```\n\n**Exit codes**:\n- `0` success\n- `1` runtime error (missing config, Steam init failed, network)\n- `2` validation error (bad input, malformed JSON, schema failure)\n\n**Error shape** in JSON mode:\n```json\n{\"status\":\"error\",\"error\":{\"message\":\"...\",\"field\":\"steamSdkPath\"}}\n```\n\n---\n\n## Mock / dry-run mode\n\n`--dry-run` swaps the real Steam client for a `MockSteamClient` that:\n- Does **not** load `libsteam_api.dylib` (works on any machine)\n- Does **not** require Steam to be running\n- Returns synthetic `9_xxx_xxx_xxx` PublishedFileIds\n- Writes cache entries with `dryRun: true` so `riu list` filters them by default\n\nThis means **`scripts/e2e-dry-run.sh` works on any machine, in CI, anywhere** — no Steam SDK required. Useful for:\n- CI smoke tests\n- Agent tool development\n- Validating manifests before a real run\n- Demos\n\n---\n\n## How the icon-only skin trick works\n\nA 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.\n\nFor 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:\n\n```json\n{\n  \"Version\": 3,\n  \"ItemType\": \"Grenade\",\n  \"Groups\": [{\n    \"Textures\": {},     ← empty, no texture overrides\n    \"Floats\": {...},\n    \"Colors\": {...}\n  }]\n}\n```\n\n…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.\n\n---\n\n## Project layout\n\n```\nsrc/\n├── cli.tsx                 # commander entrypoint\n├── commands/               # one file per CLI command\n│   ├── init.tsx\n│   ├── doctor.tsx\n│   ├── new.tsx\n│   ├── upload.tsx\n│   ├── list.tsx\n│   ├── update.tsx\n│   └── upload-all.tsx\n├── ui/                     # Ink components\n│   ├── InitWizard.tsx\n│   ├── NewSkinWizard.tsx\n│   ├── UploadProgress.tsx\n│   └── BulkUploadProgress.tsx\n├── steam/                  # Steam UGC layer (mockable)\n│   ├── types.ts\n│   ├── client.ts           # real, wraps steamworks-ffi-node\n│   └── mock.ts             # fake, used by --dry-run\n├── manifest/               # pure functions, unit tested\n│   ├── itemTypes.ts\n│   ├── builder.ts\n│   ├── writer.ts\n│   └── __tests__/\n├── config/                 # conf-backed user config + Zod schema\n├── cache/                  # uploads.json local cache\n└── utils/                  # iconValidator, slugify\n```\n\n---\n\n## Development\n\n```bash\nnpm run dev -- new           # run via tsx without building\nnpm run build                # tsc → dist/\nnpm run typecheck            # tsc --noEmit\nnpm test                     # node:test unit tests\nbash scripts/e2e-dry-run.sh  # full dry-run smoke test\n```\n\nTest config gets stored in a temp dir via `RIU_CONFIG_DIR` so it never touches your real config.\n\n---\n\n## Troubleshooting\n\n**`Steamworks SDK not ready: no Steam library found`**\nThe path you gave `riu init` doesn't contain `libsteam_api.dylib`. Run `riu doctor` for a clear report.\n\n**`Steam init failed`**\nSteam 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`.\n\n**`createItem returned null`**\nSteam 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.\n\n**`icon validation failed: must be square`**\nResize your PNG to 256/512/1024/2048 px. Sharp can do it: `npx sharp-cli resize 512 512 < bad.png > good.png`.\n\n---\n\n## License\n\nMIT\n","readmeFilename":"README.md"}