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