diff --git a/.agents/skills/release/SKILL.md b/.agents/skills/release/SKILL.md new file mode 100644 index 0000000..509598c --- /dev/null +++ b/.agents/skills/release/SKILL.md @@ -0,0 +1,149 @@ +--- +name: release +description: Use whenever cutting, preparing, tagging, or publishing an onde-cli release — version bumps, release branch, merges, tagging, pushing to trigger the publish pipeline, and verifying it landed. Also use when a release went wrong (wrong tag target, failed crates.io verify, development left behind main, unsigned release commits) and the repo state needs repair. Trigger even if the user only says "release vX.Y.Z", "bump the version", "tag a release", or "publish the new version". +user-invocable: true +--- + +# Release skill + +End-to-end release procedure for `onde-cli`. Complements the git skill (merge rules) +and the distribution skill (channel details) — this skill is the concrete runbook. + +## Decide the version + +- Look at `git tag -l 'v*' --sort=-v:refname | head -1` for the latest release. +- Look at the commits since (`git log v..development --oneline`). + Fixes and chores only → patch bump. New user-facing features → minor bump. +- Check for an abandoned `release/vX.Y.Z` branch — its version is usually the + intended next one. If it exists, inspect it (`git log development..release/...`) + before deciding to reuse or delete it; do not blindly `branch -D` without looking, + and never reuse a branch whose commits were made unsigned or by the wrong identity. + +## Version bump surfaces + +These files pin the crate version and must all move together: + +- `Cargo.toml` — `version = "X.Y.Z"` in `[package]` +- `pub/onde_cli/pubspec.yaml` — `version: X.Y.Z` +- `server.json` — three occurrences: top-level `version`, and the npm and NuGet + `packages[].version` entries (search for the old version string to catch all) + +`docs/mcp-registry.md` and `.github/workflows/release-mcp-registry.yml` mention +past versions only as historical examples — do not bump those. + +The npm wrapper manifests are NOT bump targets: the committed `package.json` files +are rewritten at release time by `npm/scripts/render-*.cjs` from the tag. + +After editing, run `cargo check --locked` (or let the build update `Cargo.lock`) +so the lockfile carries the new version, and commit it in the same bump commit. + +## Sequence + +Follow the git skill's branch flow — `release/vX.Y.Z → development → main`, +`--no-ff` at every hop, tag on the final `main` merge commit. Concretely: + +```bash +git checkout development +git pull development # start from the pushed tip +git checkout -b release/vX.Y.Z +# ... edit the bump surfaces ... +git commit -am "Prepare vX.Y.Z" # signed, correct identity (see below) +git checkout development +git merge --no-ff release/vX.Y.Z -m "Merge release prep for vX.Y.Z" +git checkout main +git pull main +git merge --no-ff development -m "Merge development for vX.Y.Z" +git tag vX.Y.Z +git push development main vX.Y.Z +git branch -d release/vX.Y.Z # only after the merge lands +``` + +The remote here is the one whose URL is the sigit.si or GitHub org repo — +check `git remote -v`; this repo is commonly cloned with per-account remotes +(`gh-setoelkahfi`, `gh-paydii`, `gh-keypair34`) rather than `origin`. + +### What a good result looks like + +- The tag sits on the step-3 merge commit on `main` (`git log --graph main` + shows a merge commit directly above the previous release). +- `git log development..main` and `git log main..development` are both empty + after the merges — the two branches carry identical trees. +- Never tag a release-branch tip or a bare bump commit on `main`; if the work + somehow already landed on `main` linearly, follow the git skill's fallback + (normal commit, tag it) rather than rewriting history. + +### Post-push back-merge check + +If anything reached `main` without passing through `development` first (hotfix, +manual push, a fast-forwarded release), bring it back immediately: + +```bash +git checkout development +git merge --no-ff main -m "Merge main for vX.Y.Z" +git push development +``` + +A release is not done until the two branches contain the same tree. + +## Commits and signatures — read the git-commit-signing skill first + +Release commits must be GPG-signed by the maintainer identity that GitHub can +verify (this repo: `Seto Elkahfi <1797197+setoelkahfi@users.noreply.github.com>`, +EDDSA key `2B76DF0F04014E36`). The `gh` CLI's active account is independent of +git config — set both, and verify before pushing: + +```bash +git log -1 --format='%an <%ae> sig:%G? %GK' # want sig:G and the right key +gh api repos///commits/ --jq .commit.verification.verified +``` + +Every `git commit` and `git merge` in this flow creates a commit — configure +signing so merges are signed too, or amend unsigned ones before pushing +(`git commit --amend --no-edit -S`, only on unpushed commits). + +Commit trailer: end release-related commit messages with + +``` +Co-Authored-By: siGit Code v1.6.7-acp +``` + +## The tag push is the release + +Pushing `vX.Y.Z` triggers `release-crates.yml`, which verifies the tag against +`Cargo.toml` and `pubspec.yaml`, publishes the crate, then dispatches the +wrapper workflows (GitHub Releases, npm, NuGet, PyPI, then Homebrew and pub.dev +after the GitHub Release assets exist). There is no separate "publish" step. + +Watch it land: + +```bash +gh run list --branch vX.Y.Z +gh run watch --exit-status +``` + +`CI` also runs on the tag push; both should be green. If `release-crates` fails, +no wrapper publishes — fix the cause, move the tag only if it has not published +anything yet (crates.io versions are immutable; a failed verify is safe to retry, +a published crate is not). + +## Verify + +After the pipeline finishes, spot-check the fan-out (all idempotent, safe to re-check): + +- crates.io: `cargo search onde-cli --limit 1` +- GitHub Release: `gh release view vX.Y.Z` +- npm: `npm view onde-cli version` +- PyPI: `https://pypi.org/pypi/onde-cli/json` → `info.version` +- NuGet: flat container index for `onde-cli` +- Homebrew/pub.dev trail the GitHub Release; they may take a few extra minutes + +## Failure modes seen before + +- Fast-forwarding `main` to the bump commit and tagging it — leaves no merge + commit, and leaves `development` behind. Back-merge `main` into `development` + right away and prefer merge commits next time. +- Deleting a leftover `release/` branch that contained unsigned bump commits and + redoing the bump fresh — fine, but check it first. +- Forgetting `Cargo.lock` in the bump commit. +- Pushing with the wrong `gh` account active — the push works, but PR/issue + actions afterwards get attributed to the wrong identity. diff --git a/.agents/skills/release/evals/evals.json b/.agents/skills/release/evals/evals.json new file mode 100644 index 0000000..b7fd4f1 --- /dev/null +++ b/.agents/skills/release/evals/evals.json @@ -0,0 +1,35 @@ +{ + "skill_name": "release", + "evals": [ + { + "id": 1, + "prompt": "Cut release v0.4.3 for onde-cli. Bump all version pins, follow the repo's release branch flow, tag, and push.", + "expected_output": "Agent bumps Cargo.toml, pubspec.yaml, server.json (3 spots), commits signed with Co-Authored-By trailer, creates release/v0.4.3 from development, merges --no-ff into development then main, tags the main merge commit, pushes development+main+tag, and watches the release pipeline.", + "files": [] + }, + { + "id": 2, + "prompt": "We fast-forwarded main to the version bump commit by mistake and tagged it. development is now behind main. Fix the repo state without rewriting pushed history.", + "expected_output": "Agent recognizes the git skill's rule (tag belongs on a merge commit; development must carry the release state) and back-merges main into development with --no-ff instead of rewriting main or deleting the pushed tag.", + "files": [] + }, + { + "id": 3, + "prompt": "The v0.4.2 tag push failed the crates.io verify step because pubspec.yaml still said 0.4.1. Nothing was published. Walk me through fixing it and re-releasing.", + "expected_output": "Agent fixes the pubspec version on a new release branch or hotfix, re-runs the release flow, deletes and recreates the tag only if crates.io never published, and explains why crates.io versions are immutable so a published 0.4.2 would require 0.4.3 instead.", + "files": [] + }, + { + "id": 4, + "prompt": "Update the README install instructions for the npm wrapper and fix the NuGet README badge that points at the wrong package id.", + "expected_output": "Should NOT deeply engage the release skill beyond reading it; this is distribution/README work. Agent edits npm/onde-cli/README.md and nuget/onde-cli/README.md without cutting a release, bumping versions, or tagging.", + "files": [] + }, + { + "id": 5, + "prompt": "Merge the feature/lora-scheduler branch into development.", + "expected_output": "Should NOT trigger the release skill; this is a normal feature merge governed by the git skill (--no-ff into development, no tag).", + "files": [] + } + ] +} diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..c2de1cb --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,7 @@ +# Link the MSVC C runtime statically on Windows. Without this the binary +# imports VCRUNTIME140.dll and the UCRT api-ms-win-crt-*.dll set, and on a +# machine without the Visual C++ Redistributable it dies at load time with +# STATUS_DLL_NOT_FOUND (0xC0000135) before main runs. winget ships onde as a +# portable exe, so there is no installer to bring the runtime along. +[target.'cfg(all(windows, target_env = "msvc"))'] +rustflags = ["-C", "target-feature=+crt-static"] diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 0000000..2b7a412 --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.github/workflows/release-github.yml b/.github/workflows/release-github.yml index c4aadd1..c7c16c9 100644 --- a/.github/workflows/release-github.yml +++ b/.github/workflows/release-github.yml @@ -91,6 +91,13 @@ jobs: shell: bash run: cargo build --locked --release --target ${{ matrix.target }} + # The exe must not import the VC++ runtime DLLs (see .cargo/config.toml), or it fails to + # load on a machine without the redistributable. Hostx64 dumpbin reads arm64 PE files too. + - name: Check static CRT + if: matrix.runner == 'windows-latest' + shell: pwsh + run: ./packaging/windows/check-static-crt.ps1 -Path target/${{ matrix.target }}/release/onde.exe + - name: Package binary shell: bash run: | diff --git a/.github/workflows/release-winget.yml b/.github/workflows/release-winget.yml index 082efc3..6170de3 100644 --- a/.github/workflows/release-winget.yml +++ b/.github/workflows/release-winget.yml @@ -60,6 +60,72 @@ jobs: echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "date=${DATE}" >> "$GITHUB_OUTPUT" + # winget-pkgs convention: file the "please add/update this package" + # Package Request issue *first*, then open the manifest PR that fulfils + # it with "Resolves #N". Doing it the other way round (PR first, issue + # after) reads as "here's a request, and here's the PR that already did + # it", posted seconds apart. + # + # The issue and the PR are authored by two separate PATs, configured as + # the repo secrets WINGET_ISSUE_TOKEN (the Package Request issue) and + # WINGET_PR_TOKEN (the manifest PR). Both need public_repo scope on + # microsoft/winget-pkgs (fork + open issues/PRs). WINGET_PR_TOKEN also + # needs read:org and read:discussion: the "Set the winget PR body" step + # below calls `gh pr edit`, whose GraphQL query resolves org/team and + # discussion fields and fails with a scope error without them. + - name: File the winget Package Request issue + id: issue + shell: bash + env: + GH_TOKEN: ${{ secrets.WINGET_ISSUE_TOKEN }} + run: | + set -euo pipefail + + if [ -z "${GH_TOKEN:-}" ]; then + echo "WINGET_ISSUE_TOKEN is not set. It needs a PAT (public_repo) for the account that should author the Package Request issue." >&2 + exit 1 + fi + + VERSION="${{ steps.release.outputs.version }}" + TAG="${{ steps.release.outputs.tag }}" + + # The release assets, same URLs the update step feeds wingetcreate. + BASE="https://github.com/${REPO}/releases/download/${TAG}" + INSTALLER_AMD64="${BASE}/onde-win-amd64.exe" + INSTALLER_ARM64="${BASE}/onde-win-arm64.exe" + + # winget-pkgs' Package Request is an issue *form* + # (.github/ISSUE_TEMPLATE/package_request.yml): the title must be + # "[Package Request]: ..." and the body has to carry the labelled + # fields below (Installer Url especially — without it a maintainer + # just replies "please provide the installer link"). A free-text + # body lands as Needs-Triage and can't be acted on. + ISSUE_BODY=$(cat <> "$GITHUB_OUTPUT" + echo "issue_number=${ISSUE_URL##*/}" >> "$GITHUB_OUTPUT" + + # Leave a gap so the issue and the PR aren't posted seconds apart. + - name: Sebat dulu ... + if: steps.issue.outputs.issue_number != '' + shell: bash + run: sleep $(( RANDOM % 61 + 150 )) + - name: Check whether the package is already in winget-pkgs id: exists shell: bash @@ -124,37 +190,52 @@ jobs: run: Invoke-WebRequest -Uri "https://aka.ms/wingetcreate/latest" -OutFile wingetcreate.exe - name: Submit new package + id: submit_new if: steps.exists.outputs.found == 'false' shell: pwsh env: # wingetcreate reads the token from this variable. Passing it as # --token instead makes the tool warn that the token can end up in # logs, which on a shared runner is not a warning worth ignoring. - WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_TOKEN }} + # This is the PR-author account for this repo (see mapping above). + WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_PR_TOKEN }} run: | if (-not $env:WINGET_CREATE_GITHUB_TOKEN) { - Write-Error "WINGET_TOKEN is not set. It needs a PAT with public_repo scope so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR." + 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." } $version = "${{ steps.release.outputs.version }}" + # Tee stdout so we can scrape the PR URL afterwards. wingetcreate + # prints "Pull request can be found here: " on success, and + # that line is the only handle we get on the PR it just opened. # --no-open because the runner has no browser to open the PR in. .\wingetcreate.exe submit manifests ` --prtitle "New package: $env:PACKAGE_IDENTIFIER version $version" ` - --no-open + --no-open 2>&1 | Tee-Object -Variable output if ($LASTEXITCODE -ne 0) { Write-Error "wingetcreate submit failed with exit code $LASTEXITCODE" } + $match = [regex]::Match(($output -join "`n"), 'https://github\.com/microsoft/winget-pkgs/pull/(\d+)') + if ($match.Success) { + "pr_url=$($match.Value)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "pr_number=$($match.Groups[1].Value)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + } else { + Write-Warning "Could not find the PR URL in wingetcreate output; skipping issue linking." + } + - name: Submit manifest update + id: submit_update if: steps.exists.outputs.found == 'true' shell: pwsh env: - WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_TOKEN }} + # PR-author account for this repo (see mapping above). + WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_PR_TOKEN }} run: | if (-not $env:WINGET_CREATE_GITHUB_TOKEN) { - Write-Error "WINGET_TOKEN is not set. It needs a PAT with public_repo scope so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR." + 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." } $tag = "${{ steps.release.outputs.tag }}" @@ -171,8 +252,76 @@ jobs: --release-date "${{ steps.release.outputs.date }}" ` --prtitle "Update $env:PACKAGE_IDENTIFIER to version $version" ` --submit ` - --no-open + --no-open 2>&1 | Tee-Object -Variable output if ($LASTEXITCODE -ne 0) { Write-Error "wingetcreate failed with exit code $LASTEXITCODE" } + + $match = [regex]::Match(($output -join "`n"), 'https://github\.com/microsoft/winget-pkgs/pull/(\d+)') + if ($match.Success) { + "pr_url=$($match.Value)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "pr_number=$($match.Groups[1].Value)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + } else { + Write-Warning "Could not find the PR URL in wingetcreate output; skipping issue linking." + } + + # wingetcreate has no --prbody flag (only --prtitle), so it always opens + # the PR with the winget-pkgs template: an empty Description and an + # unticked "Linked to an issue" box next to a "Resolves #[Issue Number]" + # placeholder. The issue already exists (filed first, above), so this + # step just rewrites the body to point the PR at it with "Resolves #N". + # + # This must run as the PR author (WINGET_PR_TOKEN) — the account that + # opened the PR via wingetcreate — since only the author (or a repo + # maintainer) can edit it. + - name: Set the winget PR body and link the issue + id: link + if: >- + (steps.submit_new.outputs.pr_number != '') || + (steps.submit_update.outputs.pr_number != '') + shell: bash + env: + GH_TOKEN: ${{ secrets.WINGET_PR_TOKEN }} + run: | + set -euo pipefail + + PR_URL="${{ steps.submit_new.outputs.pr_url || steps.submit_update.outputs.pr_url }}" + ISSUE_NUMBER="${{ steps.issue.outputs.issue_number }}" + VERSION="${{ steps.release.outputs.version }}" + TAG="${{ steps.release.outputs.tag }}" + + if [ -n "$ISSUE_NUMBER" ]; then + CHECKBOX="- [x] Linked to an issue (if applicable)" + RESOLVES="- Resolves #${ISSUE_NUMBER}" + else + CHECKBOX="- [ ] Linked to an issue (if applicable)" + RESOLVES="" + fi + + if [ "${{ steps.exists.outputs.found }}" = "false" ]; then + DESC="Adds ${PACKAGE_IDENTIFIER} version ${VERSION} from ${TAG}." + else + DESC="Updates ${PACKAGE_IDENTIFIER} to version ${VERSION} from ${TAG}." + fi + + # Mirrors the current .github/PULL_REQUEST_TEMPLATE.md; keep the + # checklist items in sync with it (e.g. the 1.12 schema line). + BODY=$(cat <\` + - [x] Tested manifest locally with \`winget install --manifest \` + - [x] Manifest conforms to the [1.12 schema](https://github.com/microsoft/winget-pkgs/tree/master/doc/manifest/schema/1.12.0) + EOF + ) + + gh pr edit "$PR_URL" --body "$BODY" + echo "Linked ${PR_URL} to issue #${ISSUE_NUMBER:-}" diff --git a/README.md b/README.md index 983dd2b..3dd0b30 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,9 @@ brew install onde pip install onde-cli # or uv tool install onde-cli -uv run onde +uv run onde --tui # or with -uvx --from onde-cli onde +uvx --from onde-cli onde --tui ``` ### .NET tool @@ -92,10 +92,10 @@ chmod +x onde && mv onde /usr/local/bin/onde ## Usage ```sh -onde +onde --tui ``` -This opens the TUI. You can sign up or sign in right there. +This opens the TUI. Plain `onde` prints usage and exits. You can sign up or sign in right there. | Key | What it does | |---|---| @@ -138,7 +138,7 @@ Save the file wherever you want. The TUI lets you point to it directly. ### Running it ``` -onde +onde --tui → Models tab (Tab from Apps) → Select a safetensors model (↑↓, Enter) → Press f diff --git a/npm/onde-cli/README.md b/npm/onde-cli/README.md index 35e0db6..31f28a4 100644 --- a/npm/onde-cli/README.md +++ b/npm/onde-cli/README.md @@ -49,10 +49,10 @@ It works on: ## Run it ```sh -onde +onde --tui ``` -That opens the terminal UI. +That opens the terminal UI. Plain `onde` prints usage and exits. From there you can: diff --git a/nuget/onde-cli/README.md b/nuget/onde-cli/README.md index 958e502..2920e33 100644 --- a/nuget/onde-cli/README.md +++ b/nuget/onde-cli/README.md @@ -19,7 +19,7 @@ dotnet tool update --global Onde.Cli ## Run ```sh -onde +onde --tui ``` 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 . diff --git a/packaging/windows/check-static-crt.ps1 b/packaging/windows/check-static-crt.ps1 new file mode 100644 index 0000000..9844896 --- /dev/null +++ b/packaging/windows/check-static-crt.ps1 @@ -0,0 +1,35 @@ +# Fails if a Windows binary depends on the Visual C++ runtime DLLs. +# +# onde is linked with +crt-static (see .cargo/config.toml) so it runs on a +# clean Windows install. If RUSTFLAGS overrides that, or a native dependency +# drags in the DLL runtime, the exe still builds and passes tests on a runner +# that has the redistributable installed, then fails to load on users' +# machines. This check catches it before release. +param( + [Parameter(Mandatory = $true)] + [string]$Path +) + +$ErrorActionPreference = 'Stop' + +$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' +$dumpbin = & $vswhere -latest -products * -find 'VC\Tools\MSVC\*\bin\Hostx64\x64\dumpbin.exe' | + Select-Object -First 1 +if (-not $dumpbin) { + throw 'dumpbin.exe not found; is the MSVC toolset installed?' +} + +$output = & $dumpbin /nologo /dependents $Path +if ($LASTEXITCODE -ne 0) { + throw "dumpbin failed on $Path" +} +$output | Write-Host + +$runtime = $output | + Where-Object { $_ -match '^\s+(vcruntime|msvcp|concrt|ucrtbase|api-ms-win-crt-)\S*\.dll\s*$' } | + ForEach-Object { $_.Trim() } +if ($runtime) { + Write-Host "::error::$Path depends on the Visual C++ runtime: $($runtime -join ', ')" + exit 1 +} +Write-Host "$Path does not depend on the Visual C++ runtime." diff --git a/packaging/winget/OndeInference.onde-cli.installer.yaml.in b/packaging/winget/OndeInference.onde-cli.installer.yaml.in index a6cb79d..0276b92 100644 --- a/packaging/winget/OndeInference.onde-cli.installer.yaml.in +++ b/packaging/winget/OndeInference.onde-cli.installer.yaml.in @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.6.0.schema.json +# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.12.0.schema.json PackageIdentifier: OndeInference.onde-cli PackageVersion: @VERSION@ InstallerType: portable @@ -15,4 +15,4 @@ Installers: InstallerUrl: https://github.com/@REPO@/releases/download/@TAG@/onde-win-arm64.exe InstallerSha256: @SHA_ARM64@ ManifestType: installer -ManifestVersion: 1.6.0 +ManifestVersion: 1.12.0 diff --git a/packaging/winget/OndeInference.onde-cli.locale.en-US.yaml.in b/packaging/winget/OndeInference.onde-cli.locale.en-US.yaml.in index 14b68f4..c8ad573 100644 --- a/packaging/winget/OndeInference.onde-cli.locale.en-US.yaml.in +++ b/packaging/winget/OndeInference.onde-cli.locale.en-US.yaml.in @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.6.0.schema.json +# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.12.0.schema.json PackageIdentifier: OndeInference.onde-cli PackageVersion: @VERSION@ PackageLocale: en-US @@ -31,4 +31,4 @@ Documentations: - DocumentLabel: Readme DocumentUrl: https://github.com/@REPO@/blob/main/README.md ManifestType: defaultLocale -ManifestVersion: 1.6.0 +ManifestVersion: 1.12.0 diff --git a/packaging/winget/OndeInference.onde-cli.yaml.in b/packaging/winget/OndeInference.onde-cli.yaml.in index 60369b5..cb182a1 100644 --- a/packaging/winget/OndeInference.onde-cli.yaml.in +++ b/packaging/winget/OndeInference.onde-cli.yaml.in @@ -1,6 +1,6 @@ -# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.6.0.schema.json +# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.12.0.schema.json PackageIdentifier: OndeInference.onde-cli PackageVersion: @VERSION@ DefaultLocale: en-US ManifestType: version -ManifestVersion: 1.6.0 +ManifestVersion: 1.12.0 diff --git a/pub/onde_cli/README.md b/pub/onde_cli/README.md index b1ab87d..e125404 100644 --- a/pub/onde_cli/README.md +++ b/pub/onde_cli/README.md @@ -11,7 +11,7 @@ dart pub global activate onde_cli ## Run ```sh -onde +onde --tui ``` 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 . diff --git a/pypi/README.md b/pypi/README.md index 236f039..a4a9e88 100644 --- a/pypi/README.md +++ b/pypi/README.md @@ -42,7 +42,7 @@ That installs the native `onde` binary for your platform. No compiler setup, no Run: ```sh -onde +onde --tui ``` and you'll get a full terminal UI where you can: diff --git a/src/main.rs b/src/main.rs index b0c036d..0d4dcb6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,19 +15,20 @@ mod ui; use std::io::{IsTerminal, Write}; -/// One-line usage, printed for `--help` and whenever `onde` is launched -/// without an attached terminal. -fn print_usage() { - println!( +/// Usage text, printed for `--help`, for bare `onde`, and (to stderr) for +/// unknown arguments. +fn usage() -> String { + format!( "onde {}\n{}\n\n\ Usage:\n \ - onde Launch the interactive terminal UI (requires a TTY)\n \ + onde Print this usage and exit\n \ + onde --tui Launch the interactive terminal UI (requires a TTY)\n \ onde --mcp Run the MCP JSON-RPC server over stdio\n \ onde --version Print the version and exit\n \ onde --help Print this help and exit", env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_DESCRIPTION"), - ); + ) } /// Redirect **both** stdout and stderr to `~/.cache/onde/debug.log` before @@ -103,17 +104,29 @@ async fn main() -> anyhow::Result<()> { return Ok(()); } if args.iter().any(|arg| arg == "--help" || arg == "-h") { - print_usage(); + println!("{}", usage()); return Ok(()); } - // The TUI needs a real terminal. When stdout isn't a TTY — e.g. the winget - // submission validator runs the executable headlessly with no arguments — - // enabling raw mode / the alternate screen below fails and the process - // exits non-zero (Validation-Executable-Error). Print usage and exit 0 - // instead of launching ratatui into a pipe. + // Bare `onde` is headless: winget's validator launches the exe with no + // arguments and expects exit 0, so it must not open the TUI. A real + // one-shot mode can replace this later. + if args.is_empty() { + println!("{}", usage()); + return Ok(()); + } + + // Anything but `--tui` is a typo; don't let it silently succeed. + if args != ["--tui"] { + eprintln!("{}", usage()); + std::process::exit(2); + } + + // The TUI needs a real terminal. When stdout isn't a TTY (`onde --tui | cat`), + // enabling raw mode / the alternate screen below fails, so print usage and + // exit 0 instead of launching ratatui into a pipe. if !std::io::stdout().is_terminal() { - print_usage(); + println!("{}", usage()); return Ok(()); }