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"