From 45f49c1d2c532d9667c533f380be0b0777e69ae0 Mon Sep 17 00:00:00 2001 From: Seto Elkahfi <1797197+setoelkahfi@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:34:59 +0200 Subject: [PATCH 1/8] chore: bootstrap .sigit agentic resources config Add factory, code-cloud-agent, and code-review config.toml under .sigit/ to align with the standard agentic-resources layout. Also ignore the local .playwright-mcp/ working directory. Co-Authored-By: siGit Code <297239231+sigitc@users.noreply.github.com> --- .sigit/code-cloud-agent/config.toml | 2 ++ .sigit/code-review/config.toml | 3 +++ .sigit/factory/config.toml | 9 +++++++++ 3 files changed, 14 insertions(+) create mode 100644 .sigit/code-cloud-agent/config.toml create mode 100644 .sigit/code-review/config.toml create mode 100644 .sigit/factory/config.toml diff --git a/.sigit/code-cloud-agent/config.toml b/.sigit/code-cloud-agent/config.toml new file mode 100644 index 0000000..291319b --- /dev/null +++ b/.sigit/code-cloud-agent/config.toml @@ -0,0 +1,2 @@ +[agent] +model = "onde-balance" diff --git a/.sigit/code-review/config.toml b/.sigit/code-review/config.toml new file mode 100644 index 0000000..27cdf29 --- /dev/null +++ b/.sigit/code-review/config.toml @@ -0,0 +1,3 @@ +[agent] +login = "sigit-code-review" +display_name = "siGit Code Review" diff --git a/.sigit/factory/config.toml b/.sigit/factory/config.toml new file mode 100644 index 0000000..91044b1 --- /dev/null +++ b/.sigit/factory/config.toml @@ -0,0 +1,9 @@ +[agent] +model = "onde-large" + +[harvest] +state = "closed" +limit = 50 +since = "2025-09-01" +cadence = "weekly" +day_of_week = "Mon" From 42ebbe76a8c3c3a3ea7eaa125020df1fd34b5b1c Mon Sep 17 00:00:00 2001 From: Seto Elkahfi <1797197+setoelkahfi@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:26:26 +0200 Subject: [PATCH 2/8] fix: exit 0 when stdout is not a TTY (winget headless validator) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The winget submission validator runs the binary with no arguments and no attached terminal. The bare `onde` code path immediately calls `enable_raw_mode()` / `EnterAlternateScreen`, which fail without a TTY and exit non-zero — triggering Validation-Executable-Error. Add a no-TTY guard (via `IsTerminal`) before `redirect_output()` so onde prints a usage summary and exits 0 when stdout is not a terminal. Also add explicit `--version`/`-V` and `--help`/`-h` handlers that work the same way, since the validator also exercises those flags. Closes #11 Co-Authored-By: siGit Code v1.6.7-acp --- src/main.rs | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 221ecb8..b0c036d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,22 @@ mod project; mod token; mod ui; -use std::io::Write; +use std::io::{IsTerminal, Write}; + +/// One-line usage, printed for `--help` and whenever `onde` is launched +/// without an attached terminal. +fn print_usage() { + println!( + "onde {}\n{}\n\n\ + Usage:\n \ + onde 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 /// ratatui takes over the alternate screen. @@ -81,6 +96,27 @@ async fn main() -> anyhow::Result<()> { return mcp::serve().await; } + // Bare `--version`/`--help` must work without a terminal. + let args: Vec = std::env::args().skip(1).collect(); + if args.iter().any(|arg| arg == "--version" || arg == "-V") { + println!("onde {}", env!("CARGO_PKG_VERSION")); + return Ok(()); + } + if args.iter().any(|arg| arg == "--help" || arg == "-h") { + print_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. + if !std::io::stdout().is_terminal() { + print_usage(); + return Ok(()); + } + let tty = redirect_output(); // Build a writer that targets the real terminal. When From dab4da3e83c674b20081289bd4df3adda511fd34 Mon Sep 17 00:00:00 2001 From: Seto Elkahfi <1797197+setoelkahfi@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:04:32 +0200 Subject: [PATCH 3/8] Prepare v0.4.2 Co-Authored-By: siGit Code <297239231+sigitc@users.noreply.github.com> Co-Authored-By: siGit Code v1.6.7-acp --- Cargo.lock | 2 +- Cargo.toml | 2 +- pub/onde_cli/pubspec.yaml | 2 +- server.json | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8cda5ec..57f7fdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3682,7 +3682,7 @@ dependencies = [ [[package]] name = "onde-cli" -version = "0.4.1" +version = "0.4.2" dependencies = [ "anyhow", "candle-core", diff --git a/Cargo.toml b/Cargo.toml index adccc9e..8e63f58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "onde-cli" -version = "0.4.1" +version = "0.4.2" edition = "2024" description = "Terminal UI to fine-tune model and manage your Onde Inference account." license = "MIT OR Apache-2.0" diff --git a/pub/onde_cli/pubspec.yaml b/pub/onde_cli/pubspec.yaml index 55c71e4..81cb110 100644 --- a/pub/onde_cli/pubspec.yaml +++ b/pub/onde_cli/pubspec.yaml @@ -1,6 +1,6 @@ name: onde_cli description: Dart launcher for the Onde Inference CLI that downloads the right native binary on first run. -version: 0.4.1 +version: 0.4.2 homepage: https://ondeinference.com repository: https://github.com/ondeinference/onde-cli issue_tracker: https://github.com/ondeinference/onde-cli/issues diff --git a/server.json b/server.json index 43ef549..564176e 100644 --- a/server.json +++ b/server.json @@ -3,7 +3,7 @@ "name": "io.github.ondeinference/onde-cli", "title": "Onde Inference CLI: Fine-tune & Deploy Local LLMs", "description": "MCP server to fine-tune LoRA, export GGUF, and deploy local LLMs on Onde Inference.", - "version": "0.4.1", + "version": "0.4.2", "websiteUrl": "https://ondeinference.com/cli", "repository": { "url": "https://github.com/ondeinference/onde-cli", @@ -15,7 +15,7 @@ "registryType": "npm", "registryBaseUrl": "https://registry.npmjs.org", "identifier": "@ondeinference/cli", - "version": "0.4.1", + "version": "0.4.2", "runtimeHint": "npx", "transport": { "type": "stdio" @@ -33,7 +33,7 @@ "registryType": "nuget", "registryBaseUrl": "https://api.nuget.org/v3/index.json", "identifier": "Onde.Cli", - "version": "0.4.1", + "version": "0.4.2", "runtimeHint": "dnx", "transport": { "type": "stdio" From d50cb1e33d1d6cfc4a3ebe9ee311a279177376f9 Mon Sep 17 00:00:00 2001 From: Seto Elkahfi <1797197+setoelkahfi@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:05:54 +0200 Subject: [PATCH 4/8] ci: file the winget tracking issue before opening the PR Split the winget submission into an issue-then-PR two-token flow. The tracking issue is opened first, under the package's owning account, so the winget-pkgs PR that follows can reference it. WINGET_ISSUE_TOKEN files the issue and WINGET_PR_TOKEN opens the PR and comments the issue link back onto it. The tokens stay separate because the ordering depends on the issue number. Capture the winget-pkgs PR URL from wingetcreate's output on both the new-package and update paths, and downgrade a missed parse to a warning rather than failing a release whose manifest PR already shipped. Co-Authored-By: siGit Code v1.6.7-acp Co-Authored-By: SplitFire v1.1.1-acp --- .github/workflows/release-winget.yml | 82 +++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-winget.yml b/.github/workflows/release-winget.yml index 082efc3..7ac84ad 100644 --- a/.github/workflows/release-winget.yml +++ b/.github/workflows/release-winget.yml @@ -60,6 +60,32 @@ jobs: echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "date=${DATE}" >> "$GITHUB_OUTPUT" + # Step 1 of the two-token flow: open the tracking issue *first*, under the + # package's owning account, so the winget-pkgs PR opened in step 2 can + # reference it. WINGET_ISSUE_TOKEN is a classic PAT with public_repo scope; + # github.token is not used here because it would attribute the issue to the + # Actions bot instead of the account that files the PR. + - name: Open tracking issue + id: issue + shell: bash + env: + GH_TOKEN: ${{ secrets.WINGET_ISSUE_TOKEN }} + run: | + if [ -z "$GH_TOKEN" ]; then + 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." + exit 1 + fi + + TAG="${{ steps.release.outputs.tag }}" + VERSION="${{ steps.release.outputs.version }}" + + URL=$(gh issue create --repo "$REPO" \ + --title "Publish ${PACKAGE_IDENTIFIER} ${VERSION} to winget" \ + --body "Tracking the winget-pkgs submission for \`${PACKAGE_IDENTIFIER}\` version ${VERSION} (${TAG}). The microsoft/winget-pkgs PR will reference this issue.") + + echo "Opened tracking issue: ${URL}" + echo "number=${URL##*/}" >> "$GITHUB_OUTPUT" + - name: Check whether the package is already in winget-pkgs id: exists shell: bash @@ -123,38 +149,52 @@ jobs: shell: pwsh run: Invoke-WebRequest -Uri "https://aka.ms/wingetcreate/latest" -OutFile wingetcreate.exe + # Step 2 of the two-token flow: open the PR with WINGET_PR_TOKEN (mapped to + # WINGET_CREATE_GITHUB_TOKEN, the only env var wingetcreate reads). Both + # tokens are classic PATs under the same account; keeping them separate + # preserves the issue-then-PR ordering the issue number depends on. - name: Submit new package + id: submit 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 }} + 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 classic PAT with public_repo scope so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR." } $version = "${{ steps.release.outputs.version }}" # --no-open because the runner has no browser to open the PR in. - .\wingetcreate.exe submit manifests ` + $output = .\wingetcreate.exe submit manifests ` --prtitle "New package: $env:PACKAGE_IDENTIFIER version $version" ` - --no-open + --no-open 2>&1 | Tee-Object -Variable raw + $raw | Out-String | Write-Host if ($LASTEXITCODE -ne 0) { Write-Error "wingetcreate submit failed with exit code $LASTEXITCODE" } + # Capture the opened PR URL so the next step can link the tracking issue. + $prUrl = (($raw | Out-String) | Select-String -Pattern 'https://github.com/microsoft/winget-pkgs/pull/\d+').Matches.Value | Select-Object -First 1 + if ($prUrl) { + "pr_url=$prUrl" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + } + + # Step 2 of the two-token flow (update path): same WINGET_PR_TOKEN mapping. - name: Submit manifest update + id: update if: steps.exists.outputs.found == 'true' shell: pwsh env: - WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_TOKEN }} + 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 classic PAT with public_repo scope so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR." } $tag = "${{ steps.release.outputs.tag }}" @@ -164,15 +204,41 @@ jobs: # The trailing |x64 and |arm64 tell wingetcreate which installer # entry each URL replaces. Without them it guesses from the file # name, and "onde-win-amd64.exe" is not a spelling it recognises. - .\wingetcreate.exe update $env:PACKAGE_IDENTIFIER ` + $output = .\wingetcreate.exe update $env:PACKAGE_IDENTIFIER ` --version $version ` --urls "$base/onde-win-amd64.exe|x64" "$base/onde-win-arm64.exe|arm64" ` --release-notes-url "https://github.com/${env:REPO}/releases/tag/$tag" ` --release-date "${{ steps.release.outputs.date }}" ` --prtitle "Update $env:PACKAGE_IDENTIFIER to version $version" ` --submit ` - --no-open + --no-open 2>&1 | Tee-Object -Variable raw + $raw | Out-String | Write-Host if ($LASTEXITCODE -ne 0) { Write-Error "wingetcreate failed with exit code $LASTEXITCODE" } + + # Capture the opened PR URL so the next step can link the tracking issue. + $prUrl = (($raw | Out-String) | Select-String -Pattern 'https://github.com/microsoft/winget-pkgs/pull/\d+').Matches.Value | Select-Object -First 1 + if ($prUrl) { + "pr_url=$prUrl" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + } + + # Close the issue-then-PR loop: comment the tracking issue onto the PR that + # step 2 opened. continue-on-error so a missed URL parse never fails a + # release that already shipped its manifest PR. + - name: Link tracking issue on the winget-pkgs PR + if: steps.issue.outputs.number != '' + continue-on-error: true + shell: bash + env: + GH_TOKEN: ${{ secrets.WINGET_PR_TOKEN }} + run: | + PR_URL="${{ steps.update.outputs.pr_url }}${{ steps.submit.outputs.pr_url }}" + if [ -z "$PR_URL" ]; then + echo "::warning::Could not determine the winget-pkgs PR URL; skipping the issue link. Link ${REPO}#${{ steps.issue.outputs.number }} by hand." + exit 0 + fi + + gh pr comment "$PR_URL" --repo microsoft/winget-pkgs \ + --body "Related upstream issue: ${REPO}#${{ steps.issue.outputs.number }}" From caff1839215055abd07ee6cbccfb71450dda6de0 Mon Sep 17 00:00:00 2001 From: Seto Elkahfi <1797197+setoelkahfi@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:05:58 +0200 Subject: [PATCH 5/8] chore: add release skill to .agents/skills Add the end-to-end release runbook for onde-cli: version-bump surfaces, the release/vX.Y.Z -> development -> main flow with the tag on the final main merge commit, the signing requirement, and the post-push back-merge check. Includes evals covering the previously seen failure modes (fast-forwarded main, unsigned bump commits, a failed crates.io verify). Co-Authored-By: siGit Code v1.6.7-acp Co-Authored-By: SplitFire v1.1.1-acp --- .agents/skills/release/SKILL.md | 149 ++++++++++++++++++++++++ .agents/skills/release/evals/evals.json | 35 ++++++ 2 files changed, 184 insertions(+) create mode 100644 .agents/skills/release/SKILL.md create mode 100644 .agents/skills/release/evals/evals.json 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": [] + } + ] +} From 10749d8fd5047453cd67c56bd1885b3dec9aa177 Mon Sep 17 00:00:00 2001 From: Seto Elkahfi <1797197+setoelkahfi@users.noreply.github.com> Date: Tue, 13 Oct 2026 13:31:04 +0200 Subject: [PATCH 6/8] 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 --- .cargo/config.toml | 7 + .github/workflows/release-github.yml | 7 + .github/workflows/release-winget.yml | 183 +++++++++++++----- README.md | 10 +- npm/onde-cli/README.md | 4 +- nuget/onde-cli/README.md | 2 +- packaging/windows/check-static-crt.ps1 | 35 ++++ .../OndeInference.onde-cli.installer.yaml.in | 4 +- ...ndeInference.onde-cli.locale.en-US.yaml.in | 4 +- .../winget/OndeInference.onde-cli.yaml.in | 4 +- pub/onde_cli/README.md | 2 +- pypi/README.md | 2 +- src/main.rs | 39 ++-- 13 files changed, 224 insertions(+), 79 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 packaging/windows/check-static-crt.ps1 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/.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 7ac84ad..840d5c3 100644 --- a/.github/workflows/release-winget.yml +++ b/.github/workflows/release-winget.yml @@ -60,31 +60,71 @@ jobs: echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "date=${DATE}" >> "$GITHUB_OUTPUT" - # Step 1 of the two-token flow: open the tracking issue *first*, under the - # package's owning account, so the winget-pkgs PR opened in step 2 can - # reference it. WINGET_ISSUE_TOKEN is a classic PAT with public_repo scope; - # github.token is not used here because it would attribute the issue to the - # Actions bot instead of the account that files the PR. - - name: Open tracking issue + # 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: | - if [ -z "$GH_TOKEN" ]; then - 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." + 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 - TAG="${{ steps.release.outputs.tag }}" VERSION="${{ steps.release.outputs.version }}" + TAG="${{ steps.release.outputs.tag }}" - URL=$(gh issue create --repo "$REPO" \ - --title "Publish ${PACKAGE_IDENTIFIER} ${VERSION} to winget" \ - --body "Tracking the winget-pkgs submission for \`${PACKAGE_IDENTIFIER}\` version ${VERSION} (${TAG}). The microsoft/winget-pkgs PR will reference this issue.") - - echo "Opened tracking issue: ${URL}" - echo "number=${URL##*/}" >> "$GITHUB_OUTPUT" + # 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: Wait before opening the PR + 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 @@ -149,52 +189,53 @@ jobs: shell: pwsh run: Invoke-WebRequest -Uri "https://aka.ms/wingetcreate/latest" -OutFile wingetcreate.exe - # Step 2 of the two-token flow: open the PR with WINGET_PR_TOKEN (mapped to - # WINGET_CREATE_GITHUB_TOKEN, the only env var wingetcreate reads). Both - # tokens are classic PATs under the same account; keeping them separate - # preserves the issue-then-PR ordering the issue number depends on. - name: Submit new package - id: submit + 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. + # 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_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." + 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. - $output = .\wingetcreate.exe submit manifests ` + .\wingetcreate.exe submit manifests ` --prtitle "New package: $env:PACKAGE_IDENTIFIER version $version" ` - --no-open 2>&1 | Tee-Object -Variable raw - $raw | Out-String | Write-Host + --no-open 2>&1 | Tee-Object -Variable output if ($LASTEXITCODE -ne 0) { Write-Error "wingetcreate submit failed with exit code $LASTEXITCODE" } - # Capture the opened PR URL so the next step can link the tracking issue. - $prUrl = (($raw | Out-String) | Select-String -Pattern 'https://github.com/microsoft/winget-pkgs/pull/\d+').Matches.Value | Select-Object -First 1 - if ($prUrl) { - "pr_url=$prUrl" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + $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." } - # Step 2 of the two-token flow (update path): same WINGET_PR_TOKEN mapping. - name: Submit manifest update - id: update + id: submit_update if: steps.exists.outputs.found == 'true' shell: pwsh env: + # 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_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." + 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 }}" @@ -204,41 +245,83 @@ jobs: # The trailing |x64 and |arm64 tell wingetcreate which installer # entry each URL replaces. Without them it guesses from the file # name, and "onde-win-amd64.exe" is not a spelling it recognises. - $output = .\wingetcreate.exe update $env:PACKAGE_IDENTIFIER ` + .\wingetcreate.exe update $env:PACKAGE_IDENTIFIER ` --version $version ` --urls "$base/onde-win-amd64.exe|x64" "$base/onde-win-arm64.exe|arm64" ` --release-notes-url "https://github.com/${env:REPO}/releases/tag/$tag" ` --release-date "${{ steps.release.outputs.date }}" ` --prtitle "Update $env:PACKAGE_IDENTIFIER to version $version" ` --submit ` - --no-open 2>&1 | Tee-Object -Variable raw - $raw | Out-String | Write-Host + --no-open 2>&1 | Tee-Object -Variable output if ($LASTEXITCODE -ne 0) { Write-Error "wingetcreate failed with exit code $LASTEXITCODE" } - # Capture the opened PR URL so the next step can link the tracking issue. - $prUrl = (($raw | Out-String) | Select-String -Pattern 'https://github.com/microsoft/winget-pkgs/pull/\d+').Matches.Value | Select-Object -First 1 - if ($prUrl) { - "pr_url=$prUrl" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + $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." } - # Close the issue-then-PR loop: comment the tracking issue onto the PR that - # step 2 opened. continue-on-error so a missed URL parse never fails a - # release that already shipped its manifest PR. - - name: Link tracking issue on the winget-pkgs PR - if: steps.issue.outputs.number != '' - continue-on-error: true + # 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: | - PR_URL="${{ steps.update.outputs.pr_url }}${{ steps.submit.outputs.pr_url }}" - if [ -z "$PR_URL" ]; then - echo "::warning::Could not determine the winget-pkgs PR URL; skipping the issue link. Link ${REPO}#${{ steps.issue.outputs.number }} by hand." - exit 0 + 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 - gh pr comment "$PR_URL" --repo microsoft/winget-pkgs \ - --body "Related upstream issue: ${REPO}#${{ steps.issue.outputs.number }}" + # 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(()); } From 5f81e2e41412eaec5cfdb4a3dfdab59fad16d0b9 Mon Sep 17 00:00:00 2001 From: Seto Elkahfi <1797197+setoelkahfi@users.noreply.github.com> Date: Tue, 13 Oct 2026 13:44:52 +0200 Subject: [PATCH 7/8] chore: add .claude/skills -> ../.agents/skills symlink So Claude Code discovers the canonical skills tree without duplicating it. --- .claude/skills | 1 + 1 file changed, 1 insertion(+) create mode 120000 .claude/skills 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 From ac36b2d3d55e567843721ce1e42125b954854fda Mon Sep 17 00:00:00 2001 From: Seto Elkahfi <1797197+setoelkahfi@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:06:41 +0200 Subject: [PATCH 8/8] ci(winget): rename the post-issue step to a friendlier 'Sebat dulu ...' Cosmetic only: changes the GitHub Actions step display name in release-winget.yml so the run UI shows 'Sebat dulu ...' instead of the default 'Wait before opening the PR'. The sleep that follows is unchanged. --- .github/workflows/release-winget.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-winget.yml b/.github/workflows/release-winget.yml index 840d5c3..6170de3 100644 --- a/.github/workflows/release-winget.yml +++ b/.github/workflows/release-winget.yml @@ -121,7 +121,7 @@ jobs: echo "issue_number=${ISSUE_URL##*/}" >> "$GITHUB_OUTPUT" # Leave a gap so the issue and the PR aren't posted seconds apart. - - name: Wait before opening the PR + - name: Sebat dulu ... if: steps.issue.outputs.issue_number != '' shell: bash run: sleep $(( RANDOM % 61 + 150 ))