@ondeinference / cli / commits / 6e68bb6

Switch pub.dev package to thin launcher

keypair34 committed May 3, 2026 at 15:03 UTC 6e68bb6073b9cf668d31772e7bca89efac39c16b
7 files changed +160 -156
.agents/skills/distribution/SKILL.md
+14 -16
@@ -82,14 +82,15 @@ Good fit for .NET developers and CI environments that already use `dotnet tool i
82
83 ### pub.dev
84
85 -Use a Dart package with a command entrypoint and a library that launches the bundled native binary.
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 -- use `Isolate.resolvePackageUri` to find the installed package path
90 - keep the runtime-id mapping explicit
92 -- do not overcomplicate the wrapper with platform-specific Dart APIs unless necessary
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
95 Good fit for Dart and Flutter developers who want the same CLI through familiar tooling.
96
@@ -141,14 +142,17 @@ Safer checks:
142
143 pub.dev behaves differently from npm and NuGet in one important way:
144
144 -- files ignored by git are generally excluded from publish input
145 +- large bundled binaries quickly hit package size limits
146
146 -That means staged native binaries for `pub/onde_cli/native/` must be available to the package at publish time.
147 +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.
148
148 -Two practical consequences:
149 +Practical consequences:
150
150 -- do not ignore `pub/onde_cli/native/` in the root `.gitignore` if CI stages binaries there
151 -- a `.pubignore` can hide local tool noise, but do not hide the native payload you intend to publish
151 +- `pub/onde_cli/native/` should stay out of the published package
152 +- do not set `publish_to: https://pub.dev` in `pubspec.yaml`; omit `publish_to` for normal pub.dev publishing
153 +- use package-level ignore rules so local native staging does not leak into publish input
154 +- keep release asset names aligned with the GitHub Release workflow, because the launcher depends on them
155 +- treat `~/.onde/cli` as the local cache root for downloaded binaries
156
157 Run these during verification:
158
@@ -156,12 +160,6 @@ Run these during verification:
160 - `dart analyze pub/onde_cli`
161 - `dart pub publish --dry-run --directory pub/onde_cli`
162
159 -One more gotcha:
160 -
161 -- pub.dev validation expects a clean git state
162 -- if CI stages native binaries into `pub/onde_cli/native/`, make a temporary local commit after staging those files and syncing `pubspec.yaml`
163 -- staging alone is not enough, because `dart pub publish --dry-run` will warn on modified checked-in files and fail the job
164 -
163 ## NuGet-specific notes
164
165 NuGet packaging is sensitive to path layout.
@@ -217,8 +215,8 @@ At minimum, check:
215 - manually bumping the committed npm manifest and assuming that is what npm publishes
216 - staged native binaries land in the wrong directory before packaging
217 - package builds successfully but installs without the native binary
220 -- pub.dev package omits staged assets because gitignore excludes them
221 -- pub.dev dry-run fails because CI staged native files but never created a clean local commit
218 +- pub.dev launcher points at the wrong GitHub Release asset name or runtime id
219 +- pub.dev package still includes bundled binaries and blows the upload size limit
220 - smoke test invokes a TUI-first binary in a way that does not make sense for CI
221 - README badges and install commands drift out of sync with real package names
222
.github/workflows/release-pub.yml
-122
@@ -16,98 +16,11 @@ permissions:
16 env:
17 CARGO_TERM_COLOR: always
18 PUB_PACKAGE_NAME: onde_cli
19 - ONDE_APP_ID: ${{ secrets.ONDE_APP_ID }}
20 - ONDE_APP_SECRET: ${{ secrets.ONDE_APP_SECRET }}
21 - GRESIQ_API_KEY: ${{ secrets.GRESIQ_API_KEY }}
22 - GRESIQ_API_SECRET: ${{ secrets.GRESIQ_API_SECRET }}
23 - HF_TOKEN: ${{ secrets.HF_TOKEN }}
19
20 jobs:
26 - build-native-binaries:
27 - name: Build native binary (${{ matrix.build.NAME }})
28 - runs-on: ${{ matrix.build.OS }}
29 - strategy:
30 - fail-fast: false
31 - matrix:
32 - build:
33 - - {
34 - NAME: linux-x64,
35 - OS: ubuntu-latest,
36 - TARGET: x86_64-unknown-linux-gnu,
37 - }
38 - - {
39 - NAME: linux-arm64,
40 - OS: ubuntu-24.04-arm,
41 - TARGET: aarch64-unknown-linux-gnu,
42 - }
43 - - {
44 - NAME: windows-x64,
45 - OS: windows-2022,
46 - TARGET: x86_64-pc-windows-msvc,
47 - }
48 - - {
49 - NAME: windows-arm64,
50 - OS: windows-2022,
51 - TARGET: aarch64-pc-windows-msvc,
52 - }
53 - - {
54 - NAME: darwin-x64,
55 - OS: macos-26-intel,
56 - TARGET: x86_64-apple-darwin,
57 - }
58 - - { NAME: darwin-arm64, OS: macos-26, TARGET: aarch64-apple-darwin }
59 - steps:
60 - - name: Checkout
61 - uses: actions/checkout@v6
62 - with:
63 - ref: ${{ github.event.inputs.tag || github.ref }}
64 -
65 - - name: Read Rust toolchain
66 - shell: bash
67 - run: |
68 - rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
69 - if [ -z "$rust_toolchain" ]; then
70 - echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
71 - exit 1
72 - fi
73 - echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
74 -
75 - - name: Install Rust toolchain
76 - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
77 - with:
78 - toolchain: ${{ env.RUST_TOOLCHAIN }}
79 - targets: ${{ matrix.build.TARGET }}
80 -
81 - - name: Setup Rust cache
82 - uses: Swatinem/rust-cache@v2
83 - with:
84 - key: pub-${{ matrix.build.TARGET }}
85 -
86 - - name: Build binary
87 - shell: bash
88 - run: cargo build --locked --release --target ${{ matrix.build.TARGET }}
89 -
90 - - name: Prepare artifact
91 - shell: bash
92 - run: |
93 - executable_name="onde"
94 - if [[ "${{ matrix.build.OS }}" == "windows-2022" ]]; then
95 - executable_name="onde.exe"
96 - fi
97 -
98 - mkdir -p "pub-artifacts/native/${{ matrix.build.NAME }}"
99 - cp "target/${{ matrix.build.TARGET }}/release/${executable_name}" "pub-artifacts/native/${{ matrix.build.NAME }}/${executable_name}"
100 -
101 - - name: Upload native artifact
102 - uses: actions/upload-artifact@v4
103 - with:
104 - name: pub-native-${{ matrix.build.NAME }}
105 - path: pub-artifacts/native
106 -
21 verify-package:
22 name: Verify Dart package
23 runs-on: ubuntu-latest
110 - needs: build-native-binaries
24 steps:
25 - name: Checkout
26 uses: actions/checkout@v6
@@ -125,13 +38,6 @@ jobs:
38 fi
39 echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
40
128 - - name: Download native artifacts
129 - uses: actions/download-artifact@v4
130 - with:
131 - pattern: pub-native-*
132 - path: pub/onde_cli/native
133 - merge-multiple: true
134 -
41 - name: Setup Dart SDK
42 uses: dart-lang/setup-dart@v1
43
@@ -150,16 +56,6 @@ jobs:
56 pubspec.write_text('\n'.join(lines) + '\n')
57 PY
58
153 - - name: Create temporary clean git state
154 - shell: bash
155 - run: |
156 - git config user.name "github-actions[bot]"
157 - git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
158 - git add -f pub/onde_cli/native pub/onde_cli/pubspec.yaml
159 - if ! git diff --cached --quiet; then
160 - git commit -m "Stage pub.dev release assets for ${RELEASE_VERSION}"
161 - fi
162 -
59 - name: Get package dependencies
60 working-directory: pub/onde_cli
61 run: dart pub get
@@ -193,13 +89,6 @@ jobs:
89 fi
90 echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
91
196 - - name: Download native artifacts
197 - uses: actions/download-artifact@v4
198 - with:
199 - pattern: pub-native-*
200 - path: pub/onde_cli/native
201 - merge-multiple: true
202 -
92 - name: Setup Dart SDK
93 uses: dart-lang/setup-dart@v1
94
@@ -230,17 +119,6 @@ jobs:
119 echo "exists=false" >> "$GITHUB_OUTPUT"
120 fi
121
233 - - name: Create temporary clean git state
234 - if: steps.pub-check.outputs.exists != 'true'
235 - shell: bash
236 - run: |
237 - git config user.name "github-actions[bot]"
238 - git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
239 - git add -f pub/onde_cli/native pub/onde_cli/pubspec.yaml
240 - if ! git diff --cached --quiet; then
241 - git commit -m "Stage pub.dev release assets for ${RELEASE_VERSION}"
242 - fi
243 -
122 - name: Set up pub.dev credentials
123 if: steps.pub-check.outputs.exists != 'true'
124 shell: bash
pub/onde_cli/.gitignore
+1
@@ -1,2 +1,3 @@
1 .dart_tool/
2 pubspec.lock
3 +native/
pub/onde_cli/.pubignore
+1
@@ -1,2 +1,3 @@
1 .dart_tool/
2 pubspec.lock
3 +native/
pub/onde_cli/README.md
+13 -3
@@ -1,6 +1,6 @@
1 # onde_cli
2
3 -Dart wrapper for the official [Onde Inference](https://ondeinference.com) CLI.
3 +Dart launcher for the official [Onde Inference](https://ondeinference.com) CLI.
4
5 ## Install
6
@@ -14,13 +14,13 @@ dart pub global activate onde_cli
14 onde
15 ```
16
17 -That opens the Onde terminal UI for account management, app setup, and local model fine-tuning. For package details, platform notes, and the full install matrix, see <https://ondeinference.com/cli>.
17 +That opens the Onde terminal UI for account management, app setup, and local model fine-tuning. On first run, the launcher downloads the right native binary into `~/.onde/cli`. After that it reuses the local copy. For package details, platform notes, and the full install matrix, see <https://ondeinference.com/cli>.
18
19 If you want a quick background read on the inference side, Onde has a short note on the [forward pass](https://ondeinference.com/forward-pass).
20
21 ## Platform support
22
23 -This package bundles native `onde` binaries for:
23 +This package downloads native `onde` binaries for:
24
25 - macOS `arm64`, `x64`
26 - Linux `arm64`, `x64` (glibc)
@@ -33,6 +33,16 @@ This package bundles native `onde` binaries for:
33 - .NET tool: `dotnet tool install --global Onde.Cli`
34 - Homebrew: `brew install ondeinference/homebrew-tap/onde`
35
36 +## Cache location
37 +
38 +The launcher stores downloaded binaries under:
39 +
40 +```sh
41 +~/.onde/cli
42 +```
43 +
44 +Versioned downloads live under that directory, so different CLI versions can coexist without stepping on each other.
45 +
46 ## Source
47
48 - Repository: <https://github.com/ondeinference/onde-cli>
pub/onde_cli/lib/onde_cli.dart
+130 -13
@@ -26,29 +26,128 @@ Future<int> runOndeCli(List<String> arguments) async {
26 }
27
28 Future<String> resolveExecutablePath() async {
29 - final packageUri = await Isolate.resolvePackageUri(
30 - Uri.parse('package:onde_cli/onde_cli.dart'),
29 + final version = await packageVersion();
30 + final cacheDirectory = await cliCacheDirectory(version);
31 + final executablePath = [
32 + cacheDirectory.path,
33 + runtimeIdentifier(),
34 + executableName(),
35 + ].join(Platform.pathSeparator);
36 +
37 + final executableFile = File(executablePath);
38 + if (!await executableFile.exists()) {
39 + await downloadExecutable(version, executableFile);
40 + }
41 +
42 + return executablePath;
43 +}
44 +
45 +Future<void> downloadExecutable(String version, File executableFile) async {
46 + final parent = executableFile.parent;
47 + if (!await parent.exists()) {
48 + await parent.create(recursive: true);
49 + }
50 +
51 + final tempFile = File('${executableFile.path}.download');
52 + if (await tempFile.exists()) {
53 + await tempFile.delete();
54 + }
55 +
56 + final client = HttpClient();
57 + try {
58 + final request = await client.getUrl(downloadUri(version));
59 + final response = await request.close();
60 +
61 + if (response.statusCode != HttpStatus.ok) {
62 + throw OndeCliException(
63 + 'Failed to download onde ${version} for ${runtimeIdentifier()} '
64 + '(HTTP ${response.statusCode}).',
65 + );
66 + }
67 +
68 + final sink = tempFile.openWrite();
69 + await response.pipe(sink);
70 + await sink.close();
71 +
72 + if (await executableFile.exists()) {
73 + await executableFile.delete();
74 + }
75 +
76 + await tempFile.rename(executableFile.path);
77 + } on OndeCliException {
78 + rethrow;
79 + } catch (error) {
80 + throw OndeCliException('Failed to download the native onde binary: $error');
81 + } finally {
82 + client.close(force: true);
83 + }
84 +}
85 +
86 +Uri downloadUri(String version) {
87 + return Uri.parse(
88 + 'https://github.com/ondeinference/onde-cli/releases/download/'
89 + 'v$version/${releaseAssetName()}',
90 );
91 +}
92
33 - if (packageUri == null) {
34 - throw OndeCliException(
35 - 'Failed to resolve the installed Dart package path.',
36 - );
93 +Future<Directory> cliCacheDirectory(String version) async {
94 + final home = homeDirectoryPath();
95 + return Directory(
96 + [home, '.onde', 'cli', version].join(Platform.pathSeparator),
97 + );
98 +}
99 +
100 +String homeDirectoryPath() {
101 + final env = Platform.environment;
102 + final home = env['HOME'];
103 + if (home != null && home.isNotEmpty) {
104 + return home;
105 + }
106 +
107 + final userProfile = env['USERPROFILE'];
108 + if (userProfile != null && userProfile.isNotEmpty) {
109 + return userProfile;
110 }
111
39 - final packageRootUri = packageUri.resolve('../');
40 - final executableUri = packageRootUri.resolve(
41 - 'native/${runtimeIdentifier()}/${executableName()}',
112 + final homeDrive = env['HOMEDRIVE'];
113 + final homePath = env['HOMEPATH'];
114 + if (homeDrive != null && homePath != null) {
115 + return '$homeDrive$homePath';
116 + }
117 +
118 + throw OndeCliException('Could not resolve the home directory for onde.');
119 +}
120 +
121 +Future<String> packageVersion() async {
122 + final packageRoot = await resolvePackageRoot();
123 + final pubspecFile = File(packageRoot.resolve('pubspec.yaml').toFilePath());
124 +
125 + if (!await pubspecFile.exists()) {
126 + throw OndeCliException('Could not read pubspec.yaml for onde_cli.');
127 + }
128 +
129 + final lines = await pubspecFile.readAsLines();
130 + for (final line in lines) {
131 + if (line.startsWith('version: ')) {
132 + return line.substring('version: '.length).trim().replaceAll('"', '');
133 + }
134 + }
135 +
136 + throw OndeCliException('Could not find a version in pubspec.yaml.');
137 +}
138 +
139 +Future<Uri> resolvePackageRoot() async {
140 + final packageUri = await Isolate.resolvePackageUri(
141 + Uri.parse('package:onde_cli/onde_cli.dart'),
142 );
43 - final executablePath = executableUri.toFilePath();
143
45 - if (!await File(executablePath).exists()) {
144 + if (packageUri == null) {
145 throw OndeCliException(
47 - "The native onde executable for '${runtimeIdentifier()}' is not bundled in this package.",
146 + 'Failed to resolve the installed Dart package path.',
147 );
148 }
149
51 - return executablePath;
150 + return packageUri.resolve('../');
151 }
152
153 String runtimeIdentifier() {
@@ -83,6 +182,24 @@ String architectureName() {
182 );
183 }
184
185 +String releaseAssetName() {
186 + final architecture = architectureName() == 'x64' ? 'amd64' : 'arm64';
187 +
188 + if (Platform.isMacOS) {
189 + return 'onde-macos-$architecture';
190 + }
191 + if (Platform.isLinux) {
192 + return 'onde-linux-$architecture';
193 + }
194 + if (Platform.isWindows) {
195 + return 'onde-win-$architecture.exe';
196 + }
197 +
198 + throw OndeCliException(
199 + 'onde does not support this operating system through the Dart package.',
200 + );
201 +}
202 +
203 String executableName() => Platform.isWindows ? 'onde.exe' : 'onde';
204
205 Future<void> ensureExecutablePermissions(String executablePath) async {
pub/onde_cli/pubspec.yaml
+1 -2
@@ -1,11 +1,10 @@
1 name: onde_cli
2 -description: Dart wrapper for the Onde Inference CLI with bundled native binaries for macOS, Linux, and Windows.
2 +description: Dart launcher for the Onde Inference CLI that downloads the right native binary on first run.
3 version: 0.3.1
4 homepage: https://ondeinference.com
5 repository: https://github.com/ondeinference/onde-cli
6 issue_tracker: https://github.com/ondeinference/onde-cli/issues
7 documentation: https://ondeinference.com
8 -publish_to: https://pub.dev
8
9 environment:
10 sdk: ">=3.8.0 <4.0.0"