| name | distribution |
| description | Use when adding, fixing, or documenting package distribution for Onde CLI. Covers wrapper-package design across npm, PyPI, NuGet, pub.dev, GitHub Releases, and Homebrew, plus version alignment, artifact staging, README surfaces, and smoke-test strategy. |
Onde CLI distribution
This skill captures how onde-cli is distributed today and what to watch when adding new channels.
Distribution model
onde-cli is a native Rust CLI first.
Everything else is packaging.
That means:
- build the Rust binary once per target
- publish thin wrappers that install or launch that binary
- keep channel-specific logic small and predictable
This design keeps behavior consistent across ecosystems and avoids re-implementing CLI logic in each language.
Channel map
Native source of truth
- Rust crate:
onde-cli
- binary name:
onde
- root manifest:
Cargo.toml
Wrapper channels
- npm: JavaScript launcher + optional platform packages
- PyPI:
maturin binary wheels
- NuGet: .NET global tool launcher
- pub.dev: Dart launcher package
Native artifact channels
- crates.io
- GitHub Releases
- Homebrew
Wrapper design patterns
npm
Use a very small Node launcher that:
- detects OS and architecture
- resolves the installed platform package
- spawns the binary with inherited stdio
Good fit for users who already live in npm tooling.
Important release detail:
- the committed
npm/onde-cli/package.json is a working file, not the publish-time source of truth
- the release workflow rewrites the base package manifest with
npm/scripts/render-main-package.cjs
- platform package manifests are generated on the fly with
npm/scripts/render-platform-package.cjs
- the actual npm version comes from the release tag via
RELEASE_VERSION, not from manually editing the committed npm manifest before publish
PyPI
Use maturin with bindings = "bin" so the native executable is what gets installed.
Good fit for Python users who want the CLI but not a Python reimplementation.
NuGet
Use a .NET global tool wrapper.
Important details:
- keep the package as a launcher, not a managed rewrite
- place native binaries under the tool payload
- resolve the runtime identifier at startup
- set Unix execute bits before launch
Good fit for .NET developers and CI environments that already use dotnet tool install.
pub.dev
Use a Dart package with a command entrypoint and a library that launches the native binary.
Important details:
- executable command should stay
onde
- keep the runtime-id mapping explicit
- download the correct binary from GitHub Releases on first run
- cache downloads under
~/.onde/cli
- keep the wrapper thin, the Rust binary is still the product
- publish the pub.dev package only after the matching GitHub Release exists
Good fit for Dart and Flutter developers who want the same CLI through familiar tooling.
Runtime identifier mapping
Keep these names aligned anywhere wrappers or workflows refer to native assets:
darwin-x64
darwin-arm64
linux-x64
linux-arm64
windows-x64
windows-arm64
If a wrapper uses one naming scheme and a workflow uploads another, the installed package will pass packaging but fail at runtime.
Release workflow principles
One tag trigger, then fan out
release-crates.yml is the only workflow that reacts to a tag push. It verifies the
tag against Cargo.toml and pubspec.yaml, publishes the crate, then dispatches
release-github, release-npm, release-nuget, and release-pypi with the same tag.
release-github in turn dispatches release-homebrew and release-pub after the
release assets exist.
tag push
└─ release-crates (verify -> publish -> dispatch)
├─ release-github ─┬─ release-homebrew
│ └─ release-pub
├─ release-npm
├─ release-nuget
└─ release-pypi
This mirrors smbcloud-cli. Two properties matter:
- crates.io is the gate. If verify or publish fails, no wrapper publishes a version
that has no crate behind it.
- every dispatch passes
ref: <tag>, not ref: main, so downstream workflows run the
workflow definition from the release state instead of whatever main happens to be.
Do not add push: tags: to a wrapper workflow. Several workflows racing on the same tag
is what lets the pub.dev launcher publish before its GitHub Release assets exist.
Build first, package second
Always build native binaries in a target matrix first.
Then let the wrapper channel jobs package those artifacts.
This avoids rebuilding the product multiple times and makes failures easier to reason about.
Idempotency
Every publish workflow should check whether the target version already exists before publishing.
Examples:
- npm:
npm view
- PyPI: package JSON endpoint
- NuGet: flat container index
- pub.dev: package API endpoint
Smoke tests should match the product shape
Because onde is TUI-first, avoid smoke tests that assume a traditional CLI help flow unless you know that code path is stable and non-interactive.
Safer checks:
- install succeeds
- bundled native binary exists where expected
- package manager validation passes
pub.dev-specific notes
pub.dev behaves differently from npm and NuGet in one important way:
- large bundled binaries quickly hit package size limits
That makes the safest design a thin Dart launcher that downloads the right native binary from GitHub Releases at runtime instead of shipping every platform binary inside the package.
Practical consequences:
pub/onde_cli/native/ should stay out of the published package
- do not set
publish_to: https://pub.dev in pubspec.yaml; omit publish_to for normal pub.dev publishing
- use package-level ignore rules so local native staging does not leak into publish input
- keep release asset names aligned with the GitHub Release workflow, because the launcher depends on them
- treat
~/.onde/cli as the local cache root for downloaded binaries
- do not let the pub.dev workflow race ahead of GitHub Releases; dispatch it after
release-github.yml has created the assets
Run these during verification:
dart pub get --directory pub/onde_cli
dart analyze pub/onde_cli
dart pub publish --dry-run --directory pub/onde_cli
NuGet-specific notes
NuGet packaging is sensitive to path layout.
The safest pattern is:
- copy native files into the publish output
- let the .NET tool pack process carry them into the final package
Avoid custom PackagePath patterns that can duplicate path segments or create nested native folders by accident.
If the tool installs but cannot find the native binary, inspect the .nupkg contents before debugging the launcher.
README surfaces
Every user-facing channel should document the same product with channel-specific installation instructions.
Main files:
README.md
npm/onde-cli/README.md
pypi/README.md
nuget/onde-cli/README.md
pub/onde_cli/README.md
Keep these aligned on:
- product description
- supported platforms
- install method
- license and copyright
- relevant supporting links like the forward-pass explainer
Do not let one channel sound like a different product.
Version alignment
When cutting a release, update every user-facing distribution version that should move with the CLI.
At minimum, check:
- Rust crate version
- npm release templates and workflow assumptions
- npm lockfile version if committed
- PyPI release version source
- NuGet package version at pack time
- pub.dev package version in
pubspec.yaml
- workflow example tags and docs that mention concrete versions
Common failure modes
- wrapper version does not match the Rust release version
- manually bumping the committed npm manifest and assuming that is what npm publishes
- staged native binaries land in the wrong directory before packaging
- package builds successfully but installs without the native binary
- pub.dev launcher points at the wrong GitHub Release asset name or runtime id
- pub.dev workflow publishes before the matching GitHub Release assets exist
- pub.dev package still includes bundled binaries and blows the upload size limit
- smoke test invokes a TUI-first binary in a way that does not make sense for CI
- README badges and install commands drift out of sync with real package names
Rule of thumb
If the native binary works and the wrappers are boring, distribution stays manageable.
If a wrapper starts acting like its own product, complexity grows fast.
Keep it thin.
Keep it aligned.
Keep the Rust binary as the source of truth.