| 1 | --- |
| 2 | name: distribution |
| 3 | 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. |
| 4 | --- |
| 5 | |
| 6 | # Onde CLI distribution |
| 7 | |
| 8 | This skill captures how `onde-cli` is distributed today and what to watch when adding new channels. |
| 9 | |
| 10 | ## Distribution model |
| 11 | |
| 12 | `onde-cli` is a native Rust CLI first. |
| 13 | |
| 14 | Everything else is packaging. |
| 15 | |
| 16 | That means: |
| 17 | |
| 18 | - build the Rust binary once per target |
| 19 | - publish thin wrappers that install or launch that binary |
| 20 | - keep channel-specific logic small and predictable |
| 21 | |
| 22 | This design keeps behavior consistent across ecosystems and avoids re-implementing CLI logic in each language. |
| 23 | |
| 24 | ## Channel map |
| 25 | |
| 26 | ### Native source of truth |
| 27 | |
| 28 | - Rust crate: `onde-cli` |
| 29 | - binary name: `onde` |
| 30 | - root manifest: `Cargo.toml` |
| 31 | |
| 32 | ### Wrapper channels |
| 33 | |
| 34 | - npm: JavaScript launcher + optional platform packages |
| 35 | - PyPI: `maturin` binary wheels |
| 36 | - NuGet: .NET global tool launcher |
| 37 | - pub.dev: Dart launcher package |
| 38 | |
| 39 | ### Native artifact channels |
| 40 | |
| 41 | - crates.io |
| 42 | - GitHub Releases |
| 43 | - Homebrew |
| 44 | |
| 45 | ## Wrapper design patterns |
| 46 | |
| 47 | ### npm |
| 48 | |
| 49 | Use a very small Node launcher that: |
| 50 | |
| 51 | - detects OS and architecture |
| 52 | - resolves the installed platform package |
| 53 | - spawns the binary with inherited stdio |
| 54 | |
| 55 | Good fit for users who already live in npm tooling. |
| 56 | |
| 57 | Important release detail: |
| 58 | |
| 59 | - the committed `npm/onde-cli/package.json` is a working file, not the publish-time source of truth |
| 60 | - the release workflow rewrites the base package manifest with `npm/scripts/render-main-package.cjs` |
| 61 | - platform package manifests are generated on the fly with `npm/scripts/render-platform-package.cjs` |
| 62 | - the actual npm version comes from the release tag via `RELEASE_VERSION`, not from manually editing the committed npm manifest before publish |
| 63 | |
| 64 | ### PyPI |
| 65 | |
| 66 | Use `maturin` with `bindings = "bin"` so the native executable is what gets installed. |
| 67 | |
| 68 | Good fit for Python users who want the CLI but not a Python reimplementation. |
| 69 | |
| 70 | ### NuGet |
| 71 | |
| 72 | Use a `.NET global tool` wrapper. |
| 73 | |
| 74 | Important details: |
| 75 | |
| 76 | - keep the package as a launcher, not a managed rewrite |
| 77 | - place native binaries under the tool payload |
| 78 | - resolve the runtime identifier at startup |
| 79 | - set Unix execute bits before launch |
| 80 | |
| 81 | Good fit for .NET developers and CI environments that already use `dotnet tool install`. |
| 82 | |
| 83 | ### pub.dev |
| 84 | |
| 85 | Use a Dart package with a command entrypoint and a library that launches the native binary. |
| 86 | |
| 87 | Important details: |
| 88 | |
| 89 | - executable command should stay `onde` |
| 90 | - keep the runtime-id mapping explicit |
| 91 | - download the correct binary from GitHub Releases on first run |
| 92 | - cache downloads under `~/.onde/cli` |
| 93 | - keep the wrapper thin, the Rust binary is still the product |
| 94 | - publish the pub.dev package only after the matching GitHub Release exists |
| 95 | |
| 96 | Good fit for Dart and Flutter developers who want the same CLI through familiar tooling. |
| 97 | |
| 98 | ## Runtime identifier mapping |
| 99 | |
| 100 | Keep these names aligned anywhere wrappers or workflows refer to native assets: |
| 101 | |
| 102 | - `darwin-x64` |
| 103 | - `darwin-arm64` |
| 104 | - `linux-x64` |
| 105 | - `linux-arm64` |
| 106 | - `windows-x64` |
| 107 | - `windows-arm64` |
| 108 | |
| 109 | If a wrapper uses one naming scheme and a workflow uploads another, the installed package will pass packaging but fail at runtime. |
| 110 | |
| 111 | ## Release workflow principles |
| 112 | |
| 113 | ### One tag trigger, then fan out |
| 114 | |
| 115 | `release-crates.yml` is the only workflow that reacts to a tag push. It verifies the |
| 116 | tag against `Cargo.toml` and `pubspec.yaml`, publishes the crate, then dispatches |
| 117 | `release-github`, `release-npm`, `release-nuget`, and `release-pypi` with the same tag. |
| 118 | `release-github` in turn dispatches `release-homebrew` and `release-pub` after the |
| 119 | release assets exist. |
| 120 | |
| 121 | ``` |
| 122 | tag push |
| 123 | └─ release-crates (verify -> publish -> dispatch) |
| 124 | ├─ release-github ─┬─ release-homebrew |
| 125 | │ └─ release-pub |
| 126 | ├─ release-npm |
| 127 | ├─ release-nuget |
| 128 | └─ release-pypi |
| 129 | ``` |
| 130 | |
| 131 | This mirrors `smbcloud-cli`. Two properties matter: |
| 132 | |
| 133 | - crates.io is the gate. If verify or publish fails, no wrapper publishes a version |
| 134 | that has no crate behind it. |
| 135 | - every dispatch passes `ref: <tag>`, not `ref: main`, so downstream workflows run the |
| 136 | workflow definition from the release state instead of whatever `main` happens to be. |
| 137 | |
| 138 | Do not add `push: tags:` to a wrapper workflow. Several workflows racing on the same tag |
| 139 | is what lets the pub.dev launcher publish before its GitHub Release assets exist. |
| 140 | |
| 141 | ### Build first, package second |
| 142 | |
| 143 | Always build native binaries in a target matrix first. |
| 144 | |
| 145 | Then let the wrapper channel jobs package those artifacts. |
| 146 | |
| 147 | This avoids rebuilding the product multiple times and makes failures easier to reason about. |
| 148 | |
| 149 | ### Idempotency |
| 150 | |
| 151 | Every publish workflow should check whether the target version already exists before publishing. |
| 152 | |
| 153 | Examples: |
| 154 | |
| 155 | - npm: `npm view` |
| 156 | - PyPI: package JSON endpoint |
| 157 | - NuGet: flat container index |
| 158 | - pub.dev: package API endpoint |
| 159 | |
| 160 | ### Smoke tests should match the product shape |
| 161 | |
| 162 | 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. |
| 163 | |
| 164 | Safer checks: |
| 165 | |
| 166 | - install succeeds |
| 167 | - bundled native binary exists where expected |
| 168 | - package manager validation passes |
| 169 | |
| 170 | ## pub.dev-specific notes |
| 171 | |
| 172 | pub.dev behaves differently from npm and NuGet in one important way: |
| 173 | |
| 174 | - large bundled binaries quickly hit package size limits |
| 175 | |
| 176 | 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. |
| 177 | |
| 178 | Practical consequences: |
| 179 | |
| 180 | - `pub/onde_cli/native/` should stay out of the published package |
| 181 | - do not set `publish_to: https://pub.dev` in `pubspec.yaml`; omit `publish_to` for normal pub.dev publishing |
| 182 | - use package-level ignore rules so local native staging does not leak into publish input |
| 183 | - keep release asset names aligned with the GitHub Release workflow, because the launcher depends on them |
| 184 | - treat `~/.onde/cli` as the local cache root for downloaded binaries |
| 185 | - do not let the pub.dev workflow race ahead of GitHub Releases; dispatch it after `release-github.yml` has created the assets |
| 186 | |
| 187 | Run these during verification: |
| 188 | |
| 189 | - `dart pub get --directory pub/onde_cli` |
| 190 | - `dart analyze pub/onde_cli` |
| 191 | - `dart pub publish --dry-run --directory pub/onde_cli` |
| 192 | |
| 193 | ## NuGet-specific notes |
| 194 | |
| 195 | NuGet packaging is sensitive to path layout. |
| 196 | |
| 197 | The safest pattern is: |
| 198 | |
| 199 | - copy native files into the publish output |
| 200 | - let the .NET tool pack process carry them into the final package |
| 201 | |
| 202 | Avoid custom `PackagePath` patterns that can duplicate path segments or create nested native folders by accident. |
| 203 | |
| 204 | If the tool installs but cannot find the native binary, inspect the `.nupkg` contents before debugging the launcher. |
| 205 | |
| 206 | ## README surfaces |
| 207 | |
| 208 | Every user-facing channel should document the same product with channel-specific installation instructions. |
| 209 | |
| 210 | Main files: |
| 211 | |
| 212 | - `README.md` |
| 213 | - `npm/onde-cli/README.md` |
| 214 | - `pypi/README.md` |
| 215 | - `nuget/onde-cli/README.md` |
| 216 | - `pub/onde_cli/README.md` |
| 217 | |
| 218 | Keep these aligned on: |
| 219 | |
| 220 | - product description |
| 221 | - supported platforms |
| 222 | - install method |
| 223 | - license and copyright |
| 224 | - relevant supporting links like the forward-pass explainer |
| 225 | |
| 226 | Do not let one channel sound like a different product. |
| 227 | |
| 228 | ## Version alignment |
| 229 | |
| 230 | When cutting a release, update every user-facing distribution version that should move with the CLI. |
| 231 | |
| 232 | At minimum, check: |
| 233 | |
| 234 | - Rust crate version |
| 235 | - npm release templates and workflow assumptions |
| 236 | - npm lockfile version if committed |
| 237 | - PyPI release version source |
| 238 | - NuGet package version at pack time |
| 239 | - pub.dev package version in `pubspec.yaml` |
| 240 | - workflow example tags and docs that mention concrete versions |
| 241 | |
| 242 | ## Common failure modes |
| 243 | |
| 244 | - wrapper version does not match the Rust release version |
| 245 | - manually bumping the committed npm manifest and assuming that is what npm publishes |
| 246 | - staged native binaries land in the wrong directory before packaging |
| 247 | - package builds successfully but installs without the native binary |
| 248 | - pub.dev launcher points at the wrong GitHub Release asset name or runtime id |
| 249 | - pub.dev workflow publishes before the matching GitHub Release assets exist |
| 250 | - pub.dev package still includes bundled binaries and blows the upload size limit |
| 251 | - smoke test invokes a TUI-first binary in a way that does not make sense for CI |
| 252 | - README badges and install commands drift out of sync with real package names |
| 253 | |
| 254 | ## Rule of thumb |
| 255 | |
| 256 | If the native binary works and the wrappers are boring, distribution stays manageable. |
| 257 | |
| 258 | If a wrapper starts acting like its own product, complexity grows fast. |
| 259 | |
| 260 | Keep it thin. |
| 261 | Keep it aligned. |
| 262 | Keep the Rust binary as the source of truth. |