@ondeinference / cli / commits / 10749d8

ci: rename winget publisher to OndeInference and link MSVC CRT statically

- Rename packaging/winget manifests KelvinSoen.onde-cli.* -> OndeInference.onde-cli.* - Update release-github.yml and release-winget.yml for the new publisher id - Sync the new publisher id / homepage across README, npm/onde-cli/README.md, nuget/onde-cli/README.md, pub/onde_cli/README.md, pypi/README.md - Add packaging/windows/check-static-crt.ps1 to fail the release job if the Windows binary pulls in VCRUNTIME140.dll or the UCRT api-ms-win-crt-* set - Link MSVC C runtime statically via .cargo/config.toml so winget hosts without the Visual C++ Redistributable do not hit STATUS_DLL_NOT_FOUND (0xC0000135) - Fix winget non-TTY behaviour in src/main.rs

Seto Elkahfi committed Oct 13, 2026 at 13:31 UTC 10749d8fd5047453cd67c56bd1885b3dec9aa177
13 files changed +224 -79
.cargo/config.toml new
+7
@@ -0,0 +1,7 @@
1 +# Link the MSVC C runtime statically on Windows. Without this the binary
2 +# imports VCRUNTIME140.dll and the UCRT api-ms-win-crt-*.dll set, and on a
3 +# machine without the Visual C++ Redistributable it dies at load time with
4 +# STATUS_DLL_NOT_FOUND (0xC0000135) before main runs. winget ships onde as a
5 +# portable exe, so there is no installer to bring the runtime along.
6 +[target.'cfg(all(windows, target_env = "msvc"))']
7 +rustflags = ["-C", "target-feature=+crt-static"]
.github/workflows/release-github.yml
+7
@@ -91,6 +91,13 @@ jobs:
91 shell: bash
92 run: cargo build --locked --release --target ${{ matrix.target }}
93
94 + # The exe must not import the VC++ runtime DLLs (see .cargo/config.toml), or it fails to
95 + # load on a machine without the redistributable. Hostx64 dumpbin reads arm64 PE files too.
96 + - name: Check static CRT
97 + if: matrix.runner == 'windows-latest'
98 + shell: pwsh
99 + run: ./packaging/windows/check-static-crt.ps1 -Path target/${{ matrix.target }}/release/onde.exe
100 +
101 - name: Package binary
102 shell: bash
103 run: |
.github/workflows/release-winget.yml
+133 -50
@@ -60,31 +60,71 @@ jobs:
60 echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
61 echo "date=${DATE}" >> "$GITHUB_OUTPUT"
62
63 - # Step 1 of the two-token flow: open the tracking issue *first*, under the
64 - # package's owning account, so the winget-pkgs PR opened in step 2 can
65 - # reference it. WINGET_ISSUE_TOKEN is a classic PAT with public_repo scope;
66 - # github.token is not used here because it would attribute the issue to the
67 - # Actions bot instead of the account that files the PR.
68 - - name: Open tracking issue
63 + # winget-pkgs convention: file the "please add/update this package"
64 + # Package Request issue *first*, then open the manifest PR that fulfils
65 + # it with "Resolves #N". Doing it the other way round (PR first, issue
66 + # after) reads as "here's a request, and here's the PR that already did
67 + # it", posted seconds apart.
68 + #
69 + # The issue and the PR are authored by two separate PATs, configured as
70 + # the repo secrets WINGET_ISSUE_TOKEN (the Package Request issue) and
71 + # WINGET_PR_TOKEN (the manifest PR). Both need public_repo scope on
72 + # microsoft/winget-pkgs (fork + open issues/PRs). WINGET_PR_TOKEN also
73 + # needs read:org and read:discussion: the "Set the winget PR body" step
74 + # below calls `gh pr edit`, whose GraphQL query resolves org/team and
75 + # discussion fields and fails with a scope error without them.
76 + - name: File the winget Package Request issue
77 id: issue
78 shell: bash
79 env:
80 GH_TOKEN: ${{ secrets.WINGET_ISSUE_TOKEN }}
81 run: |
74 - if [ -z "$GH_TOKEN" ]; then
75 - echo "::error::WINGET_ISSUE_TOKEN is not set. It needs a classic PAT with public_repo scope under the package's owning account so the tracking issue is filed before the winget-pkgs PR."
82 + set -euo pipefail
83 +
84 + if [ -z "${GH_TOKEN:-}" ]; then
85 + echo "WINGET_ISSUE_TOKEN is not set. It needs a PAT (public_repo) for the account that should author the Package Request issue." >&2
86 exit 1
87 fi
88
79 - TAG="${{ steps.release.outputs.tag }}"
89 VERSION="${{ steps.release.outputs.version }}"
90 + TAG="${{ steps.release.outputs.tag }}"
91
82 - URL=$(gh issue create --repo "$REPO" \
83 - --title "Publish ${PACKAGE_IDENTIFIER} ${VERSION} to winget" \
84 - --body "Tracking the winget-pkgs submission for \`${PACKAGE_IDENTIFIER}\` version ${VERSION} (${TAG}). The microsoft/winget-pkgs PR will reference this issue.")
85 -
86 - echo "Opened tracking issue: ${URL}"
87 - echo "number=${URL##*/}" >> "$GITHUB_OUTPUT"
92 + # The release assets, same URLs the update step feeds wingetcreate.
93 + BASE="https://github.com/${REPO}/releases/download/${TAG}"
94 + INSTALLER_AMD64="${BASE}/onde-win-amd64.exe"
95 + INSTALLER_ARM64="${BASE}/onde-win-arm64.exe"
96 +
97 + # winget-pkgs' Package Request is an issue *form*
98 + # (.github/ISSUE_TEMPLATE/package_request.yml): the title must be
99 + # "[Package Request]: ..." and the body has to carry the labelled
100 + # fields below (Installer Url especially — without it a maintainer
101 + # just replies "please provide the installer link"). A free-text
102 + # body lands as Needs-Triage and can't be acted on.
103 + ISSUE_BODY=$(cat <<EOF
104 + Download Page Url: https://github.com/${REPO}/releases/tag/${TAG}
105 + Publisher: OndeInference
106 + Package Name: Onde CLI
107 + Description: Terminal UI to fine-tune models and manage your Onde Inference account.
108 + Package Version: ${VERSION}
109 + Installer Url: ${INSTALLER_AMD64}
110 + Installer Url: ${INSTALLER_ARM64}
111 + EOF
112 + )
113 +
114 + ISSUE_URL=$(gh issue create \
115 + --repo microsoft/winget-pkgs \
116 + --title "[Package Request]: ${PACKAGE_IDENTIFIER} ${VERSION}" \
117 + --body "$ISSUE_BODY")
118 +
119 + echo "Opened issue ${ISSUE_URL}"
120 + echo "issue_url=${ISSUE_URL}" >> "$GITHUB_OUTPUT"
121 + echo "issue_number=${ISSUE_URL##*/}" >> "$GITHUB_OUTPUT"
122 +
123 + # Leave a gap so the issue and the PR aren't posted seconds apart.
124 + - name: Wait before opening the PR
125 + if: steps.issue.outputs.issue_number != ''
126 + shell: bash
127 + run: sleep $(( RANDOM % 61 + 150 ))
128
129 - name: Check whether the package is already in winget-pkgs
130 id: exists
@@ -149,52 +189,53 @@ jobs:
189 shell: pwsh
190 run: Invoke-WebRequest -Uri "https://aka.ms/wingetcreate/latest" -OutFile wingetcreate.exe
191
152 - # Step 2 of the two-token flow: open the PR with WINGET_PR_TOKEN (mapped to
153 - # WINGET_CREATE_GITHUB_TOKEN, the only env var wingetcreate reads). Both
154 - # tokens are classic PATs under the same account; keeping them separate
155 - # preserves the issue-then-PR ordering the issue number depends on.
192 - name: Submit new package
157 - id: submit
193 + id: submit_new
194 if: steps.exists.outputs.found == 'false'
195 shell: pwsh
196 env:
197 # wingetcreate reads the token from this variable. Passing it as
198 # --token instead makes the tool warn that the token can end up in
199 # logs, which on a shared runner is not a warning worth ignoring.
200 + # This is the PR-author account for this repo (see mapping above).
201 WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_PR_TOKEN }}
202 run: |
203 if (-not $env:WINGET_CREATE_GITHUB_TOKEN) {
167 - Write-Error "WINGET_PR_TOKEN is not set. It needs a classic PAT with public_repo scope so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR."
204 + Write-Error "WINGET_PR_TOKEN is not set. It needs a PAT with public_repo, read:org, and read:discussion scopes so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR, and gh can edit the PR body."
205 }
206
207 $version = "${{ steps.release.outputs.version }}"
208
209 + # Tee stdout so we can scrape the PR URL afterwards. wingetcreate
210 + # prints "Pull request can be found here: <url>" on success, and
211 + # that line is the only handle we get on the PR it just opened.
212 # --no-open because the runner has no browser to open the PR in.
173 - $output = .\wingetcreate.exe submit manifests `
213 + .\wingetcreate.exe submit manifests `
214 --prtitle "New package: $env:PACKAGE_IDENTIFIER version $version" `
175 - --no-open 2>&1 | Tee-Object -Variable raw
176 - $raw | Out-String | Write-Host
215 + --no-open 2>&1 | Tee-Object -Variable output
216
217 if ($LASTEXITCODE -ne 0) {
218 Write-Error "wingetcreate submit failed with exit code $LASTEXITCODE"
219 }
220
182 - # Capture the opened PR URL so the next step can link the tracking issue.
183 - $prUrl = (($raw | Out-String) | Select-String -Pattern 'https://github.com/microsoft/winget-pkgs/pull/\d+').Matches.Value | Select-Object -First 1
184 - if ($prUrl) {
185 - "pr_url=$prUrl" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
221 + $match = [regex]::Match(($output -join "`n"), 'https://github\.com/microsoft/winget-pkgs/pull/(\d+)')
222 + if ($match.Success) {
223 + "pr_url=$($match.Value)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
224 + "pr_number=$($match.Groups[1].Value)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
225 + } else {
226 + Write-Warning "Could not find the PR URL in wingetcreate output; skipping issue linking."
227 }
228
188 - # Step 2 of the two-token flow (update path): same WINGET_PR_TOKEN mapping.
229 - name: Submit manifest update
190 - id: update
230 + id: submit_update
231 if: steps.exists.outputs.found == 'true'
232 shell: pwsh
233 env:
234 + # PR-author account for this repo (see mapping above).
235 WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_PR_TOKEN }}
236 run: |
237 if (-not $env:WINGET_CREATE_GITHUB_TOKEN) {
197 - Write-Error "WINGET_PR_TOKEN is not set. It needs a classic PAT with public_repo scope so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR."
238 + Write-Error "WINGET_PR_TOKEN is not set. It needs a PAT with public_repo, read:org, and read:discussion scopes so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR, and gh can edit the PR body."
239 }
240
241 $tag = "${{ steps.release.outputs.tag }}"
@@ -204,41 +245,83 @@ jobs:
245 # The trailing |x64 and |arm64 tell wingetcreate which installer
246 # entry each URL replaces. Without them it guesses from the file
247 # name, and "onde-win-amd64.exe" is not a spelling it recognises.
207 - $output = .\wingetcreate.exe update $env:PACKAGE_IDENTIFIER `
248 + .\wingetcreate.exe update $env:PACKAGE_IDENTIFIER `
249 --version $version `
250 --urls "$base/onde-win-amd64.exe|x64" "$base/onde-win-arm64.exe|arm64" `
251 --release-notes-url "https://github.com/${env:REPO}/releases/tag/$tag" `
252 --release-date "${{ steps.release.outputs.date }}" `
253 --prtitle "Update $env:PACKAGE_IDENTIFIER to version $version" `
254 --submit `
214 - --no-open 2>&1 | Tee-Object -Variable raw
215 - $raw | Out-String | Write-Host
255 + --no-open 2>&1 | Tee-Object -Variable output
256
257 if ($LASTEXITCODE -ne 0) {
258 Write-Error "wingetcreate failed with exit code $LASTEXITCODE"
259 }
260
221 - # Capture the opened PR URL so the next step can link the tracking issue.
222 - $prUrl = (($raw | Out-String) | Select-String -Pattern 'https://github.com/microsoft/winget-pkgs/pull/\d+').Matches.Value | Select-Object -First 1
223 - if ($prUrl) {
224 - "pr_url=$prUrl" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
261 + $match = [regex]::Match(($output -join "`n"), 'https://github\.com/microsoft/winget-pkgs/pull/(\d+)')
262 + if ($match.Success) {
263 + "pr_url=$($match.Value)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
264 + "pr_number=$($match.Groups[1].Value)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
265 + } else {
266 + Write-Warning "Could not find the PR URL in wingetcreate output; skipping issue linking."
267 }
268
227 - # Close the issue-then-PR loop: comment the tracking issue onto the PR that
228 - # step 2 opened. continue-on-error so a missed URL parse never fails a
229 - # release that already shipped its manifest PR.
230 - - name: Link tracking issue on the winget-pkgs PR
231 - if: steps.issue.outputs.number != ''
232 - continue-on-error: true
269 + # wingetcreate has no --prbody flag (only --prtitle), so it always opens
270 + # the PR with the winget-pkgs template: an empty Description and an
271 + # unticked "Linked to an issue" box next to a "Resolves #[Issue Number]"
272 + # placeholder. The issue already exists (filed first, above), so this
273 + # step just rewrites the body to point the PR at it with "Resolves #N".
274 + #
275 + # This must run as the PR author (WINGET_PR_TOKEN) — the account that
276 + # opened the PR via wingetcreate — since only the author (or a repo
277 + # maintainer) can edit it.
278 + - name: Set the winget PR body and link the issue
279 + id: link
280 + if: >-
281 + (steps.submit_new.outputs.pr_number != '') ||
282 + (steps.submit_update.outputs.pr_number != '')
283 shell: bash
284 env:
285 GH_TOKEN: ${{ secrets.WINGET_PR_TOKEN }}
286 run: |
237 - PR_URL="${{ steps.update.outputs.pr_url }}${{ steps.submit.outputs.pr_url }}"
238 - if [ -z "$PR_URL" ]; then
239 - echo "::warning::Could not determine the winget-pkgs PR URL; skipping the issue link. Link ${REPO}#${{ steps.issue.outputs.number }} by hand."
240 - exit 0
287 + set -euo pipefail
288 +
289 + PR_URL="${{ steps.submit_new.outputs.pr_url || steps.submit_update.outputs.pr_url }}"
290 + ISSUE_NUMBER="${{ steps.issue.outputs.issue_number }}"
291 + VERSION="${{ steps.release.outputs.version }}"
292 + TAG="${{ steps.release.outputs.tag }}"
293 +
294 + if [ -n "$ISSUE_NUMBER" ]; then
295 + CHECKBOX="- [x] Linked to an issue (if applicable)"
296 + RESOLVES="- Resolves #${ISSUE_NUMBER}"
297 + else
298 + CHECKBOX="- [ ] Linked to an issue (if applicable)"
299 + RESOLVES=""
300 + fi
301 +
302 + if [ "${{ steps.exists.outputs.found }}" = "false" ]; then
303 + DESC="Adds ${PACKAGE_IDENTIFIER} version ${VERSION} from ${TAG}."
304 + else
305 + DESC="Updates ${PACKAGE_IDENTIFIER} to version ${VERSION} from ${TAG}."
306 fi
307
243 - gh pr comment "$PR_URL" --repo microsoft/winget-pkgs \
244 - --body "Related upstream issue: ${REPO}#${{ steps.issue.outputs.number }}"
308 + # Mirrors the current .github/PULL_REQUEST_TEMPLATE.md; keep the
309 + # checklist items in sync with it (e.g. the 1.12 schema line).
310 + BODY=$(cat <<EOF
311 + ## 📖 Description
312 + ${DESC}
313 + ## ✅ Checklist
314 + - [x] Signed the [Contributor License Agreement](https://cla.opensource.microsoft.com)
315 + ${CHECKBOX}
316 + ${RESOLVES}
317 + ## 📦 Manifest Checklist
318 + - [x] Checked that there aren't other open [pull requests](https://github.com/microsoft/winget-pkgs/pulls) for the same manifest update/change
319 + - [x] This PR only modifies one (1) manifest
320 + - [x] Validated manifest locally with \`winget validate --manifest <directory>\`
321 + - [x] Tested manifest locally with \`winget install --manifest <directory>\`
322 + - [x] Manifest conforms to the [1.12 schema](https://github.com/microsoft/winget-pkgs/tree/master/doc/manifest/schema/1.12.0)
323 + EOF
324 + )
325 +
326 + gh pr edit "$PR_URL" --body "$BODY"
327 + echo "Linked ${PR_URL} to issue #${ISSUE_NUMBER:-<none>}"
README.md
+5 -5
@@ -49,9 +49,9 @@ brew install onde
49 pip install onde-cli
50 # or
51 uv tool install onde-cli
52 -uv run onde
52 +uv run onde --tui
53 # or with
54 -uvx --from onde-cli onde
54 +uvx --from onde-cli onde --tui
55 ```
56
57 ### .NET tool
@@ -92,10 +92,10 @@ chmod +x onde && mv onde /usr/local/bin/onde
92 ## Usage
93
94 ```sh
95 -onde
95 +onde --tui
96 ```
97
98 -This opens the TUI. You can sign up or sign in right there.
98 +This opens the TUI. Plain `onde` prints usage and exits. You can sign up or sign in right there.
99
100 | Key | What it does |
101 |---|---|
@@ -138,7 +138,7 @@ Save the file wherever you want. The TUI lets you point to it directly.
138 ### Running it
139
140 ```
141 -onde
141 +onde --tui
142 → Models tab (Tab from Apps)
143 → Select a safetensors model (↑↓, Enter)
144 → Press f
npm/onde-cli/README.md
+2 -2
@@ -49,10 +49,10 @@ It works on:
49 ## Run it
50
51 ```sh
52 -onde
52 +onde --tui
53 ```
54
55 -That opens the terminal UI.
55 +That opens the terminal UI. Plain `onde` prints usage and exits.
56
57 From there you can:
58
nuget/onde-cli/README.md
+1 -1
@@ -19,7 +19,7 @@ dotnet tool update --global Onde.Cli
19 ## Run
20
21 ```sh
22 -onde
22 +onde --tui
23 ```
24
25 That opens the 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>.
packaging/windows/check-static-crt.ps1 new
+35
@@ -0,0 +1,35 @@
1 +# Fails if a Windows binary depends on the Visual C++ runtime DLLs.
2 +#
3 +# onde is linked with +crt-static (see .cargo/config.toml) so it runs on a
4 +# clean Windows install. If RUSTFLAGS overrides that, or a native dependency
5 +# drags in the DLL runtime, the exe still builds and passes tests on a runner
6 +# that has the redistributable installed, then fails to load on users'
7 +# machines. This check catches it before release.
8 +param(
9 + [Parameter(Mandatory = $true)]
10 + [string]$Path
11 +)
12 +
13 +$ErrorActionPreference = 'Stop'
14 +
15 +$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
16 +$dumpbin = & $vswhere -latest -products * -find 'VC\Tools\MSVC\*\bin\Hostx64\x64\dumpbin.exe' |
17 + Select-Object -First 1
18 +if (-not $dumpbin) {
19 + throw 'dumpbin.exe not found; is the MSVC toolset installed?'
20 +}
21 +
22 +$output = & $dumpbin /nologo /dependents $Path
23 +if ($LASTEXITCODE -ne 0) {
24 + throw "dumpbin failed on $Path"
25 +}
26 +$output | Write-Host
27 +
28 +$runtime = $output |
29 + Where-Object { $_ -match '^\s+(vcruntime|msvcp|concrt|ucrtbase|api-ms-win-crt-)\S*\.dll\s*$' } |
30 + ForEach-Object { $_.Trim() }
31 +if ($runtime) {
32 + Write-Host "::error::$Path depends on the Visual C++ runtime: $($runtime -join ', ')"
33 + exit 1
34 +}
35 +Write-Host "$Path does not depend on the Visual C++ runtime."
packaging/winget/OndeInference.onde-cli.installer.yaml.in
+2 -2
@@ -1,4 +1,4 @@
1 -# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.6.0.schema.json
1 +# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.12.0.schema.json
2 PackageIdentifier: OndeInference.onde-cli
3 PackageVersion: @VERSION@
4 InstallerType: portable
@@ -15,4 +15,4 @@ Installers:
15 InstallerUrl: https://github.com/@REPO@/releases/download/@TAG@/onde-win-arm64.exe
16 InstallerSha256: @SHA_ARM64@
17 ManifestType: installer
18 -ManifestVersion: 1.6.0
18 +ManifestVersion: 1.12.0
packaging/winget/OndeInference.onde-cli.locale.en-US.yaml.in
+2 -2
@@ -1,4 +1,4 @@
1 -# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.6.0.schema.json
1 +# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.12.0.schema.json
2 PackageIdentifier: OndeInference.onde-cli
3 PackageVersion: @VERSION@
4 PackageLocale: en-US
@@ -31,4 +31,4 @@ Documentations:
31 - DocumentLabel: Readme
32 DocumentUrl: https://github.com/@REPO@/blob/main/README.md
33 ManifestType: defaultLocale
34 -ManifestVersion: 1.6.0
34 +ManifestVersion: 1.12.0
packaging/winget/OndeInference.onde-cli.yaml.in
+2 -2
@@ -1,6 +1,6 @@
1 -# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.6.0.schema.json
1 +# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.12.0.schema.json
2 PackageIdentifier: OndeInference.onde-cli
3 PackageVersion: @VERSION@
4 DefaultLocale: en-US
5 ManifestType: version
6 -ManifestVersion: 1.6.0
6 +ManifestVersion: 1.12.0
pub/onde_cli/README.md
+1 -1
@@ -11,7 +11,7 @@ dart pub global activate onde_cli
11 ## Run
12
13 ```sh
14 -onde
14 +onde --tui
15 ```
16
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>.
pypi/README.md
+1 -1
@@ -42,7 +42,7 @@ That installs the native `onde` binary for your platform. No compiler setup, no
42 Run:
43
44 ```sh
45 -onde
45 +onde --tui
46 ```
47
48 and you'll get a full terminal UI where you can:
src/main.rs
+26 -13
@@ -15,19 +15,20 @@ mod ui;
15
16 use std::io::{IsTerminal, Write};
17
18 -/// One-line usage, printed for `--help` and whenever `onde` is launched
19 -/// without an attached terminal.
20 -fn print_usage() {
21 - println!(
18 +/// Usage text, printed for `--help`, for bare `onde`, and (to stderr) for
19 +/// unknown arguments.
20 +fn usage() -> String {
21 + format!(
22 "onde {}\n{}\n\n\
23 Usage:\n \
24 - onde Launch the interactive terminal UI (requires a TTY)\n \
24 + onde Print this usage and exit\n \
25 + onde --tui Launch the interactive terminal UI (requires a TTY)\n \
26 onde --mcp Run the MCP JSON-RPC server over stdio\n \
27 onde --version Print the version and exit\n \
28 onde --help Print this help and exit",
29 env!("CARGO_PKG_VERSION"),
30 env!("CARGO_PKG_DESCRIPTION"),
30 - );
31 + )
32 }
33
34 /// Redirect **both** stdout and stderr to `~/.cache/onde/debug.log` before
@@ -103,17 +104,29 @@ async fn main() -> anyhow::Result<()> {
104 return Ok(());
105 }
106 if args.iter().any(|arg| arg == "--help" || arg == "-h") {
106 - print_usage();
107 + println!("{}", usage());
108 return Ok(());
109 }
110
110 - // The TUI needs a real terminal. When stdout isn't a TTY — e.g. the winget
111 - // submission validator runs the executable headlessly with no arguments —
112 - // enabling raw mode / the alternate screen below fails and the process
113 - // exits non-zero (Validation-Executable-Error). Print usage and exit 0
114 - // instead of launching ratatui into a pipe.
111 + // Bare `onde` is headless: winget's validator launches the exe with no
112 + // arguments and expects exit 0, so it must not open the TUI. A real
113 + // one-shot mode can replace this later.
114 + if args.is_empty() {
115 + println!("{}", usage());
116 + return Ok(());
117 + }
118 +
119 + // Anything but `--tui` is a typo; don't let it silently succeed.
120 + if args != ["--tui"] {
121 + eprintln!("{}", usage());
122 + std::process::exit(2);
123 + }
124 +
125 + // The TUI needs a real terminal. When stdout isn't a TTY (`onde --tui | cat`),
126 + // enabling raw mode / the alternate screen below fails, so print usage and
127 + // exit 0 instead of launching ratatui into a pipe.
128 if !std::io::stdout().is_terminal() {
116 - print_usage();
129 + println!("{}", usage());
130 return Ok(());
131 }
132