diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 7a31c56..fcc4c0d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "storebound", "source": ".", "description": "Take a mobile app or game from nothing to Submit for review on both stores: skills for every store step and questionnaire plus the storebound CLI and MCP server.", - "version": "0.8.2", + "version": "0.8.77", "category": "development", "tags": [ "mobile", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index d6f8582..4b980c5 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "storebound", - "version": "0.8.2", + "version": "0.8.77", "description": "Take a mobile app or game from nothing to Submit for review on Google Play and the App Store: app records, listings in every language, in-app products and subscriptions with regional prices, every questionnaire (Data safety, App access and demo accounts, account deletion, IARC, target audience, ads, App Privacy, age rating, export compliance), privacy, icons, screenshots and previews, validate, TestFlight, release management, and after launch: crash vitals, review replies, price sheets, store analytics, Product Page Optimization. Agent Skills plus a Bun CLI and MCP server (storebound); you press Submit. CRTkafa's own toolchain, open-sourced.", "author": { "name": "CRTkafa" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index c7b16e8..397705c 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "storebound", - "version": "0.8.2", + "version": "0.8.77", "description": "Take a mobile app or game from nothing to Submit for review on Google Play and the App Store: app records, listings in every language, in-app products and subscriptions with regional prices, every questionnaire (Data safety, App access and demo accounts, account deletion, IARC, target audience, ads, App Privacy, age rating, export compliance), privacy, icons, screenshots and previews, validate, TestFlight, release management, and after launch: crash vitals, review replies, price sheets, store analytics, Product Page Optimization. Agent Skills plus a Bun CLI and MCP server (storebound); you press Submit. CRTkafa's own toolchain, open-sourced.", "author": { "name": "CRTkafa" diff --git a/.cursor/rules/storebound.mdc b/.cursor/rules/storebound.mdc index 5b8121d..2b9d056 100644 --- a/.cursor/rules/storebound.mdc +++ b/.cursor/rules/storebound.mdc @@ -20,6 +20,16 @@ storebound plus you, the AI agent, do the work. The owner is not your pair of ha testers. (Also the owner's: canceling a submission, starting or stopping a store experiment, a console form's final Submit / Create that a console plan lists in `buttons.final`, signing in and granting account permissions.) For these you prepare everything, show the dry run, and the owner presses one button or says yes. + **Updates are agent steps:** a new version of an app that is already on the store (Play: the production track holds + a published release; App Store: a version of the platform is or was on sale) is yours end to end, no owner gate + per update: `storebound play release --production completed --yes`, `storebound play upload --track + production --yes`, `storebound apple submit --build --yes`, `storebound apple release --manual-release --yes` + (docs/bypass.md). Only the **first-ever release** of an app stays the owner's (exit 3 `OWNER_STEP`: after the + owner's yes re-run with `--first-release`), plus removal from sale, live price changes and anything that costs money. + **Bypass mode** (`storebound settings bypass status`): when the owner turned it on, you run these owner steps too: + MCP `owner_step` (dry run first, then `dry_run: false`), `storebound api ... --yes --confirm `, and + console plans' `buttons.final`. Say in one line what you pressed; every action is logged to + `.storebound/owner-actions.log`. Legal agreements, passwords and 2FA stay with the owner if your own rules say so. 3. **Ask the owner only for facts that no code, config or doc can prove:** compliance answers such as Data safety, App Privacy, age rating, export compliance, partial data deletion. Collect ALL of them in ONE batch at the start (`storebound check` and `storebound validate` list them), never one at a time during the run. Reuse answers that @@ -68,6 +78,9 @@ need to ...", do it instead, unless it is one of the owner steps in point 2. Submit, Create, a final Save) and tell the owner exactly what to press, then `storebound console verify
`. Never sign in, accept an agreement or grant a permission (a service account's app access) for the owner. `storebound console fill` is the optional scripted fast path; it never blocks you. +11. **Console pages: always the absolute deep link.** When you send the owner to a console page, give the absolute + deep link (storebound prints it; `storebound console plan` has start URLs), never just "go to App Store Connect" + or "App Store Connect > App Information" alone. ## Tools @@ -91,6 +104,7 @@ need to ...", do it instead, unless it is one of the owner steps in point 2. - `skills/build-and-sign/SKILL.md`: Sign, build and upload a mobile app or game from the terminal - `skills/console-autofill/SKILL.md`: Fill App Store Connect and Google Play Console forms that have no API (New App, App Privacy, Play Create app, IARC content rating, every Play App content card, Play Store settings category, first production release countries and send-for-review). Agent-first by default - `skills/export-compliance/SKILL.md`: Answer App Store export compliance (encryption) for a mobile app or game correctly +- `skills/game-services/SKILL.md`: Set up Game Center (App Store) and Play Games Services (Google Play) for a mobile game from one config section - `skills/listing-localization/SKILL.md`: Translate the store listing of a mobile app or game into more languages - `skills/migrate-from-fastlane/SKILL.md`: Move a mobile app or game from fastlane (deliver, supply, snapshot / screengrab, Appfile, Deliverfile) to storebound with `storebound import fastlane` - `skills/play-first-release/SKILL.md`: Step-by-step first (and later) Google Play release of a mobile app or game diff --git a/.forgejo/actions/ci-run/action.yml b/.forgejo/actions/ci-run/action.yml index 51b31be..6209341 100644 --- a/.forgejo/actions/ci-run/action.yml +++ b/.forgejo/actions/ci-run/action.yml @@ -1,5 +1,4 @@ -# `bun run ci` after .forgejo/actions/linux-setup: the target / timings / Playwright caches, bun install, headless -# Chromium, Node 20 (the npm bundle's oldest supported Node) and the full CI. Shared by ci.yml and release.yml. +# `bun run ci` after .forgejo/actions/linux-setup: the target / timings caches, bun install, Node 20 (the npm bundle's oldest supported Node) and the full CI. Shared by ci.yml and release.yml. name: ci-run description: storebound's full CI (bun run ci) on the Linux runner runs: @@ -21,25 +20,12 @@ runs: run: | bun --version && cargo --version bun install --frozen-lockfile - # Playwright's Chromium and headless shell (~280 MB), keyed by the browser builds the installed playwright-core - # wants (its browsers.json); the system libraries and fonts (--with-deps) are installed on every run, the job - # container starts clean - - uses: actions/cache@v4 - with: - path: ~/.cache/ms-playwright - key: playwright-${{ runner.os }}-${{ hashFiles('node_modules/playwright-core/browsers.json') }} - restore-keys: playwright-${{ runner.os }}- - # the job runs as root in a container: storebound adds --no-sandbox to the Chromium it starts itself there - # (cli/shots/browser.ts sandboxArgs), Playwright's own launches already pass it - - name: Headless Chromium (browser tests, desktop boot check) - shell: bash - run: bunx playwright install --with-deps chromium # the npm bundle runs on Node 20+: tools/ci.ts smoke-tests dist/storebound.mjs with the oldest supported Node - uses: actions/setup-node@v4 with: node-version: '20' - # the runner sets CI=true in the job and it wins over the workflow env above: set CI=full for the command - # itself, or tools/ci.ts skips the desktop Rust tests and clippy when the push did not touch src-tauri + # no browser tests in CI (tests/browser/ runs only with `bun run test:browser`); the changes of this push (against + # its parent; the checkout has fetch-depth 2) decide the Rust steps, as on a laptop against origin/main - name: bun run ci shell: bash - run: CI=full bun run ci + run: STOREBOUND_CI_BASE=HEAD~1 bun run ci diff --git a/.forgejo/actions/linux-setup/action.yml b/.forgejo/actions/linux-setup/action.yml index a16be63..2f0d06c 100644 --- a/.forgejo/actions/linux-setup/action.yml +++ b/.forgejo/actions/linux-setup/action.yml @@ -26,6 +26,7 @@ runs: with: path: | ~/.bun/install/cache + ~/.cache/tauri ~/.cargo/registry/index ~/.cargo/registry/cache ~/.cargo/git/db @@ -35,7 +36,7 @@ runs: shell: bash run: | P="${FORGEJO_PATH:-$GITHUB_PATH}" - curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" >/dev/null + bash tools/ci-install-bun.sh echo "$HOME/.bun/bin" >> "$P" command -v rustup || curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal echo "$HOME/.cargo/bin" >> "$P" diff --git a/.forgejo/workflows/acceptance.yml b/.forgejo/workflows/acceptance.yml new file mode 100644 index 0000000..e6a4b22 --- /dev/null +++ b/.forgejo/workflows/acceptance.yml @@ -0,0 +1,110 @@ +# The release acceptance matrix on demand (the same jobs as release.yml's acceptance + acceptance-report, which gate +# every release): every user-facing command from the packed npm tarball on Linux and Windows x Node 20 / Node 22 / Bun, +# then the markdown matrix (artifact storebound-acceptance). Run it from Actions > acceptance > Run workflow, or with +# the API (POST /repos/{owner}/{repo}/actions/workflows/acceptance.yml/dispatches). macOS: tools/acceptance.ts on the Mac. +name: acceptance + +on: + workflow_dispatch: + +env: + BUN_VERSION: '1.4.0' + +jobs: + acceptance: + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-22.04, platform: Linux, runtime: node, node: '20', name: node20 } + - { os: ubuntu-22.04, platform: Linux, runtime: node, node: '22', name: node22 } + - { os: ubuntu-22.04, platform: Linux, runtime: bun, node: '22', name: bun } + - { os: windows, platform: Windows, runtime: node, node: '20', name: node20 } + - { os: windows, platform: Windows, runtime: node, node: '22', name: node22 } + - { os: windows, platform: Windows, runtime: bun, node: '22', name: bun } + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - name: Bun (Linux) + if: ${{ matrix.platform == 'Linux' }} + shell: bash + run: | + bash tools/ci-install-bun.sh + echo "$HOME/.bun/bin" >> "${FORGEJO_PATH:-$GITHUB_PATH}" + - name: Acceptance matrix (Linux) + if: ${{ matrix.platform == 'Linux' }} + shell: bash + run: | + node --version && bun --version + bun install --frozen-lockfile + bun tools/acceptance.ts run --runtime ${{ matrix.runtime }} --platform Linux --jobs 4 --out acceptance/Linux-${{ matrix.name }}.json + - name: Acceptance matrix (Windows) + if: ${{ matrix.platform == 'Windows' }} + shell: pwsh + run: | + node --version; bun --version + bun install --frozen-lockfile + if ($LASTEXITCODE -ne 0) { throw "bun install failed" } + bun tools/acceptance.ts run --runtime ${{ matrix.runtime }} --platform Windows --jobs 3 --out acceptance/Windows-${{ matrix.name }}.json + if ($LASTEXITCODE -ne 0) { throw "acceptance matrix: red cells" } + - uses: actions/upload-artifact@v3 + if: ${{ always() }} + with: + name: acceptance-${{ matrix.platform }}-${{ matrix.name }} + path: acceptance/ + retention-days: 30 + + acceptance-report: + needs: [acceptance] + if: ${{ always() }} + runs-on: ubuntu-22.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v3 + continue-on-error: true + with: + name: acceptance-Linux-node20 + path: acceptance-runs/Linux-node20 + - uses: actions/download-artifact@v3 + continue-on-error: true + with: + name: acceptance-Linux-node22 + path: acceptance-runs/Linux-node22 + - uses: actions/download-artifact@v3 + continue-on-error: true + with: + name: acceptance-Linux-bun + path: acceptance-runs/Linux-bun + - uses: actions/download-artifact@v3 + continue-on-error: true + with: + name: acceptance-Windows-node20 + path: acceptance-runs/Windows-node20 + - uses: actions/download-artifact@v3 + continue-on-error: true + with: + name: acceptance-Windows-node22 + path: acceptance-runs/Windows-node22 + - uses: actions/download-artifact@v3 + continue-on-error: true + with: + name: acceptance-Windows-bun + path: acceptance-runs/Windows-bun + - name: Matrix (markdown), red cells fail the job + run: | + bash tools/ci-install-bun.sh + export PATH="$HOME/.bun/bin:$PATH" + V=$(bun tools/desktop-version.ts --print) + mkdir -p out + bun tools/acceptance.ts report acceptance-runs docs/acceptance --md "out/storebound_${V}_acceptance.md" --expect 9 --strict + - uses: actions/upload-artifact@v3 + if: ${{ always() }} + with: + name: storebound-acceptance + path: out/ + retention-days: 30 diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 63d25c3..656ac95 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -1,14 +1,24 @@ -# bun run ci on every push to main and every pull request into main: adapters, desktop version, tests, typecheck, -# desktop i18n / typecheck / build, desktop boot check (headless Chromium), desktop Rust tests and clippy, scrub. Runs on the VDS runner (docs/desktop-release.md). -# tools/ci.ts runs the steps as a task graph and the tests as parallel shards (CPU cores - 1, max 8) on the one -# self-hosted runner, so a job matrix would only queue; CI=full makes it run the Rust steps on every push. -# Publishes nothing: site/ is still published by Jenkins; releases come from release.yml (a v* tag). -# release.yml's ci job runs the same steps (the setup is shared: .forgejo/actions/linux-setup). +# Every push to main is a release candidate (docs/desktop-release.md, "Release candidates"). In parallel: +# ci bun run ci (adapters, desktop version, tests, typecheck, desktop i18n / typecheck / build, the boot check +# in headless Chromium, desktop Rust tests and clippy, scrub); tools/ci.ts runs it as a task graph with +# parallel test shards on the one Linux runner; no browser test runs here (tests/browser/, `bun run +# test:browser` by hand). The Rust steps run when the push changed a Rust input (STOREBOUND_CI_BASE). +# acceptance the release acceptance matrix (tools/acceptance.ts): every user-facing command from the packed npm +# tarball with real piped stdin, Linux and Windows x Node 20 / Node 22 / Bun. +# linux AppImage + deb (path-free build), self-tests, both started under Xvfb, privacy scan. +# windows NSIS installer, silent per-user install, self-test, uninstall, the Node prompt smoke, privacy scan. +# record when all are green (the installers already scanned with the private terms): the draft release rc- with the installers, the acceptance runs and matrix and +# green.json (tools/release-candidate.ts record). A v* tag on this commit only promotes it (release.yml). +# gate decides first: a commit that differs from its green parent only in version copies + CHANGELOG (a release's +# version bump, proven file by file: tools/release-candidate.ts gate) reuses the parent's ci and acceptance results +# and builds only the installers, whose embedded version changed. +# Pull requests run ci + acceptance (no installers, no record). rehearsal/* branches are release rehearsals +# (tools/release.ts --rehearsal). site/ is still published by Jenkins. name: ci on: push: - branches: [main] + branches: [main, 'rehearsal/**'] pull_request: branches: [main] workflow_dispatch: @@ -17,19 +27,402 @@ env: BUN_VERSION: '1.4.0' CARGO_TERM_COLOR: never CARGO_INCREMENTAL: '0' - CI: full jobs: + gate: + runs-on: ubuntu-22.04 + # the self-hosted runners only run code from this repo: pull requests from forks are skipped + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + timeout-minutes: 45 + outputs: + reuse: ${{ steps.gate.outputs.reuse }} + from: ${{ steps.gate.outputs.from }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + - name: Version-only commit of a green parent? + id: gate + env: + FORGEJO_TOKEN: ${{ secrets.RELEASE_TOKEN }} + FORGEJO_URL: ${{ github.server_url }} + FORGEJO_ORIGIN: ${{ secrets.FORGEJO_ORIGIN }} + EVENT: ${{ github.event_name }} + SHA: ${{ github.sha }} + run: | + # tests never gate a release (the owner's decision 2026-10-16): no waiting on the parent's run, which the + # release's own bump push cancels; every commit builds its installers and runs the tests in the background + echo "reuse=false" >> "$GITHUB_OUTPUT"; echo "from=" >> "$GITHUB_OUTPUT"; echo "tests run in the background; installers build for this commit" + + # background tests (the owner's decision 2026-10-16): they run on every push and report, but the record job (the + # release candidate) never waits for them; `bun tools/release.ts` and `bun tools/ci-status.ts` print a red result ci: + needs: gate + if: github.event_name != 'pull_request' || true # runner label: ubuntu-22.04:docker://ghcr.io/catthehacker/ubuntu:act-22.04 runs-on: ubuntu-22.04 - # the self-hosted runner only runs code from this repo: pull requests from forks are skipped - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository timeout-minutes: 60 steps: - uses: actions/checkout@v4 + with: + fetch-depth: 2 - uses: ./.forgejo/actions/linux-setup with: cache-key: ci rust-components: clippy - uses: ./.forgejo/actions/ci-run + + # The release acceptance matrix (tools/acceptance.ts, docs/acceptance.md): every user-facing command from the packed + # npm tarball, in fresh temp folders with piped stdin and the fake stores, on Linux and Windows x Node 20 / Node 22 + # (npx --package=) / Bun (bun x). One red cell and the commit is no candidate. macOS runs on the owner's Mac + # (tools/release.ts, cached per CLI key). + # after the installers: the one Windows runner builds the release installer first, the background tests wait + # (0.8.13's Windows installer queued behind six acceptance jobs) + acceptance: + needs: [gate, linux, windows] + if: github.event_name != 'pull_request' || true + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-22.04, platform: Linux, runtime: node, node: '20', name: node20 } + - { os: ubuntu-22.04, platform: Linux, runtime: node, node: '22', name: node22 } + - { os: ubuntu-22.04, platform: Linux, runtime: bun, node: '22', name: bun } + - { os: windows, platform: Windows, runtime: node, node: '20', name: node20 } + - { os: windows, platform: Windows, runtime: node, node: '22', name: node22 } + - { os: windows, platform: Windows, runtime: bun, node: '22', name: bun } + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - name: Bun (Linux) + if: ${{ matrix.platform == 'Linux' }} + shell: bash + run: | + bash tools/ci-install-bun.sh + echo "$HOME/.bun/bin" >> "${FORGEJO_PATH:-$GITHUB_PATH}" + - name: Acceptance matrix (Linux) + if: ${{ matrix.platform == 'Linux' }} + shell: bash + run: | + node --version && bun --version + # npm pack runs prepack, which builds the package's declarations with the pinned TypeScript + bun install --frozen-lockfile + bun tools/acceptance.ts run --runtime ${{ matrix.runtime }} --platform Linux --jobs 4 --out acceptance/Linux-${{ matrix.name }}.json + - name: Acceptance matrix (Windows) + if: ${{ matrix.platform == 'Windows' }} + shell: pwsh + run: | + node --version; bun --version + # npm pack runs prepack, which builds the package's declarations with the pinned TypeScript + bun install --frozen-lockfile + if ($LASTEXITCODE -ne 0) { throw "bun install failed" } + bun tools/acceptance.ts run --runtime ${{ matrix.runtime }} --platform Windows --jobs 3 --out acceptance/Windows-${{ matrix.name }}.json + if ($LASTEXITCODE -ne 0) { throw "acceptance matrix: red cells" } + - uses: actions/upload-artifact@v3 + if: ${{ always() }} + with: + name: acceptance-${{ matrix.platform }}-${{ matrix.name }} + path: acceptance/ + retention-days: 14 + + linux: + needs: gate + if: github.event_name != 'pull_request' + # runner label: ubuntu-22.04:docker://ghcr.io/catthehacker/ubuntu:act-22.04 (glibc 2.35: the AppImage and the + # deb then run on Ubuntu 22.04 and newer, Debian 12) + runs-on: ubuntu-22.04 + timeout-minutes: 90 + env: + # the path-free build keeps the target folder outside every home folder + CARGO_TARGET_DIR: /tmp/storebound-target + # linuxdeploy (AppImage) runs without FUSE inside the container + APPIMAGE_EXTRACT_AND_RUN: '1' + # linuxdeploy must not strip: the storebound sidecar is a `bun build --compile` executable whose JS payload strip + # removes (it then starts as plain bun). The Rust binary is already stripped by Cargo (profile.release strip). + NO_STRIP: '1' + # WebKitGTK's DMABUF renderer leaves the window blank on some GPU / VM setups; the app sets this itself at + # start (src-tauri/src/lib.rs linux_webkit_env) unless the user did; set here too for anything run in CI + WEBKIT_DISABLE_DMABUF_RENDERER: '1' + steps: + - uses: actions/checkout@v4 + - uses: ./.forgejo/actions/linux-setup + with: + cache-key: desktop + packages: patchelf file squashfs-tools p7zip-full xz-utils + # the release target folder (dependencies compiled once; a version bump recompiles only the app crate) + - uses: actions/cache@v4 + with: + path: /tmp/storebound-target + key: desktop-target-${{ runner.os }}-${{ hashFiles('apps/desktop/src-tauri/Cargo.lock') }} + restore-keys: desktop-target-${{ runner.os }}- + - name: Install and check + run: | + bun install --frozen-lockfile + bun tools/desktop-version.ts + bun tools/build-sidecars.ts --target bun-linux-x64 + # linuxdeploy (AppImage) sets the rpath of every executable with its own patchelf, which corrupts the bun + # sidecar (segfault, then ldd fails in the gtk plugin). With the rpath already set (Ubuntu's patchelf, which + # keeps it working), linuxdeploy leaves the file as it is. The sidecar needs only libc, so the rpath is inert. + patchelf --set-rpath '$ORIGIN/../lib' apps/desktop/src-tauri/binaries/storebound-x86_64-unknown-linux-gnu + apps/desktop/src-tauri/binaries/storebound-x86_64-unknown-linux-gnu version + - name: Build AppImage and deb (path-free) + working-directory: apps/desktop + run: | + rm -rf "$CARGO_TARGET_DIR/release/bundle" + # the AppImage tools come from GitHub releases, which answer 504 now and then (cached in ~/.cache/tauri) + bash ../../tools/ci-tauri-tools.sh + for i in 1 2 3; do bun ../../tools/desktop-release.ts exec -- bunx tauri build --bundles appimage,deb --ci && break; [ $i = 3 ] && exit 1; sleep 30; done + - name: Self-test the exact artifacts + run: | + V=$(bun tools/desktop-version.ts --print) + B="$CARGO_TARGET_DIR/release/bundle" + mkdir -p out && cp "$B/appimage/storebound_${V}_amd64.AppImage" "$B/deb/storebound_${V}_amd64.deb" out/ + chmod +x out/*.AppImage + (cd /tmp && rm -rf squashfs-root && "$GITHUB_WORKSPACE/out/storebound_${V}_amd64.AppImage" --appimage-extract >/dev/null && squashfs-root/usr/bin/storebound-desktop --self-test) + rm -rf /tmp/deb && dpkg-deb -x "out/storebound_${V}_amd64.deb" /tmp/deb && /tmp/deb/usr/bin/storebound-desktop --self-test + dpkg-deb -I "out/storebound_${V}_amd64.deb" | sed -n '1,20p' + - name: Start the app under Xvfb (AppImage and deb) + # --self-test checks only the sidecar; this opens the real window (GTK, WebKitGTK, the bundled libraries of the + # AppImage) on a virtual display. "sidecar started" in host.log means the webview ran the UI and called the host. + run: | + sudo apt-get install -y -qq --no-install-recommends xvfb xauth dbus-x11 >/dev/null + V=$(bun tools/desktop-version.ts --print) + smoke() { #