diff --git a/.claude/skills/omnivoice/SKILL.md b/.claude/skills/omnivoice/LEGACY.md similarity index 75% rename from .claude/skills/omnivoice/SKILL.md rename to .claude/skills/omnivoice/LEGACY.md index c1a0dd64..6b0b43fa 100644 --- a/.claude/skills/omnivoice/SKILL.md +++ b/.claude/skills/omnivoice/LEGACY.md @@ -1,11 +1,6 @@ ---- -name: omnivoice -description: Legacy VoiceStudio skill alias for existing Claude installations. Generate local speech, discover saved voices, and transcribe audio through the running VoiceStudio backend. ---- - # VoiceStudio compatibility entry -The current cross-agent package is [voicestudio](../../../skills/voicestudio/SKILL.md). +The current cross-agent package is [voicestudio](../../../../skills/voicestudio/SKILL.md). For new installations use `npx skills add debpalash/VoiceStudio --skill voicestudio`. Use the running backend at the user's configured address (default @@ -27,3 +22,8 @@ never disable authentication to make an example work. Source and current setup documentation: https://github.com/debpalash/VoiceStudio + +This archived entry is not an installable skill. Existing installations should +remove the old `omnivoice` / `oss-maintainer` entries and install `voicestudio` / +`voicestudio-maintainer` from the canonical repository. Legacy helpers remain +for existing users; the Electron supervisor is the preferred launcher. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ca85f6ca..eabcb0c2 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -56,7 +56,17 @@ bun install bun run dev ``` -This starts both services: +This launches Electron with hot reload. Its runtime supervisor manages backend setup +and startup; do not launch a second backend. See [Electron setup](../electron/README.md). + +```bash +bun run build # build Electron +bun run start # launch the built Electron app +bun run dist # package locally without publishing +bun run dev:web # legacy browser UI + backend +``` + +The legacy browser command starts both services: | Service | URL | What it does | |---------|-----|---| @@ -71,29 +81,29 @@ cause doesn't scroll away with the terminal. The same death is also reported as a crash notice in the UI the next time the backend starts (see [docs/install/troubleshooting.md §14c](docs/install/troubleshooting.md)). -### Desktop App (Tauri) +### Legacy Desktop App (Tauri) ```bash -bun run desktop # dev: hot-reload Tauri shell + backend -bun run desktop-prod # production: builds, bundles the backend, then launches +bun run tauri # legacy dev: hot-reload Tauri shell + backend +bun run tauri:desktop-prod # legacy production: builds, bundles the backend, then launches ``` Both run `uv sync` first (so the Python backend env is set up) and start the backend automatically — you do **not** start it separately. Use the exact script -names: there is no `desktop=prod` (note the **hyphen** in `desktop-prod`). -`desktop-prod` is Windows-aware (auto-detects bash/git; see `scripts/desktop-prod.mjs`). +names: there is no `desktop=prod` (note the **hyphen** in `tauri:desktop-prod`). +`tauri:desktop-prod` is Windows-aware (auto-detects bash/git; see `scripts/desktop-prod.mjs`). Requires [Rust](https://rustup.rs/) and platform-specific Tauri dependencies — see the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/). After installing Rust with rustup (or `uv` with its installer), a terminal that -was already open still has the old `PATH`. The desktop launchers (`bun desktop`, -`bun desktop-prod`, `bun desktop-fresh`) detect this and add `~/.cargo/bin` / +was already open still has the old `PATH`. The desktop launchers (`bun tauri`, +`bun tauri:desktop-prod`, `bun tauri:desktop-fresh`) detect this and add `~/.cargo/bin` / `~/.local/bin` for that run, printing a one-line note; to make it permanent, open a new terminal, or on macOS/Linux load Cargo into the current one: ```bash source "$HOME/.cargo/env" -bun desktop +bun run tauri ``` If Rust is genuinely not installed, the launchers stop up front with the @@ -310,7 +320,7 @@ that — the agent recalls the architecture, conventions, and your past findings instead of re-reading the tree each time. [**memxt**](https://github.com/debpalash/memxt) (100% local, MCP-based, built by this project's maintainer) exists for exactly this; any MCP memory server works. Pair it with the repo's agent skill — -`npx skills add debpalash/omnivoice-studio` — so your agent knows the project's +`npx skills add debpalash/VoiceStudio` — so your agent knows the project's hard rules from the first prompt. ## Quality gates your PR must pass diff --git a/.github/workflows/electron-release.yml b/.github/workflows/electron-release.yml new file mode 100644 index 00000000..76ef199c --- /dev/null +++ b/.github/workflows/electron-release.yml @@ -0,0 +1,242 @@ +name: Electron desktop release + +# Builds are safe by default. Only an explicit publish dispatch exposes a release. +on: + push: + tags: ['v*'] + workflow_dispatch: + inputs: + release_tag: + description: "Existing version tag to package using this workflow from main (optional)" + type: string + default: '' + publish: + description: "Publish the tagged Electron release after all platforms pass" + type: boolean + default: false + allow_unsigned: + description: "Explicitly accept unsigned/unnotarized Electron installers and documented updater limitations" + type: boolean + default: false + +permissions: + contents: read + +concurrency: + group: electron-release-${{ inputs.release_tag || github.ref_name }} + cancel-in-progress: false + +env: + RELEASE_REF: ${{ inputs.release_tag && format('refs/tags/{0}', inputs.release_tag) || github.ref }} + +jobs: + validate: + # The transition tag is assembled after the manual Tauri draft succeeds. + if: github.event_name == 'workflow_dispatch' || github.ref_name != vars.TAURI_SUNSET_TAG + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ env.RELEASE_REF }} + - name: Require an exact version tag + env: + REF: ${{ env.RELEASE_REF }} + WORKFLOW_REF: ${{ github.ref }} + RELEASE_TAG_OVERRIDE: ${{ inputs.release_tag }} + ALLOW_UNSIGNED: ${{ inputs.allow_unsigned }} + DISPATCH_ACTOR: ${{ github.actor }} + RERUN_ACTOR: ${{ github.triggering_actor }} + OWNER: ${{ github.repository_owner }} + run: | + if [ "$ALLOW_UNSIGNED" = true ]; then + test "$DISPATCH_ACTOR" = "$OWNER" && test "$RERUN_ACTOR" = "$OWNER" || { + echo "Only the repository owner may accept unsigned installers"; exit 1; + } + fi + if [ -n "$RELEASE_TAG_OVERRIDE" ]; then + test "$WORKFLOW_REF" = refs/heads/main || { echo "Tag overrides require the workflow from main"; exit 1; } + fi + VERSION=$(node -p "require('./frontend/package.json').version") + test "$REF" = "refs/tags/v$VERSION" || { echo "Select the exact version tag"; exit 1; } + test "$(git rev-parse HEAD)" = "$(git rev-parse "$REF^{commit}")" || { echo "Checkout does not match the release tag"; exit 1; } + package: + needs: validate + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-24.04 + platform: linux + arch: x64 + target: x86_64-unknown-linux-gnu + flags: --linux --x64 + - runner: windows-2022 + platform: win32 + arch: x64 + target: x86_64-pc-windows-msvc + flags: --win --x64 + - runner: macos-15 + platform: darwin + arch: arm64 + target: aarch64-apple-darwin + flags: --mac --arm64 + - runner: macos-15-intel + platform: darwin + arch: x64 + target: x86_64-apple-darwin + flags: --mac --x64 + defaults: + run: + shell: bash + env: + VOICESTUDIO_RUST_TARGET: ${{ matrix.target }} + VOICESTUDIO_UPDATE_CHANNEL: electron-stable-${{ matrix.platform }}-${{ matrix.arch }} + CSC_IDENTITY_AUTO_DISCOVERY: 'false' + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ env.RELEASE_REF }} + - uses: actions/setup-node@v4 + with: + node-version: '22' + - uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.4.2' + - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable + with: + targets: ${{ matrix.target }} + - uses: Swatinem/rust-cache@v2 + with: + workspaces: native/desktop-bridge -> target + key: electron-${{ matrix.target }} + - uses: astral-sh/setup-uv@v6 + with: + version: '0.12.13' + enable-cache: false + - name: Linux native dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libasound2-dev libxdo-dev libxtst-dev libx11-dev libxkbcommon-dev libwayland-dev libssl-dev pkg-config xvfb + - name: Bundle pinned uv for the host architecture + run: | + node --input-type=module <<'NODE' + import { execFileSync } from 'node:child_process'; + import { mkdirSync, copyFileSync, chmodSync } from 'node:fs'; + import { join } from 'node:path'; + const expected = process.env.VOICESTUDIO_RUST_TARGET; + const targets = { 'linux-x64': 'x86_64-unknown-linux-gnu', 'win32-x64': 'x86_64-pc-windows-msvc', 'darwin-arm64': 'aarch64-apple-darwin', 'darwin-x64': 'x86_64-apple-darwin' }; + if (targets[`${process.platform}-${process.arch}`] !== expected) throw new Error('Runner architecture does not match package target'); + const source = execFileSync(process.platform === 'win32' ? 'where.exe' : 'which', ['uv'], { encoding: 'utf8' }).trim().split(/\r?\n/)[0]; + const dir = 'frontend/src-tauri/binaries'; + mkdirSync(dir, { recursive: true }); + const destination = join(dir, `uv-${expected}${process.platform === 'win32' ? '.exe' : ''}`); + copyFileSync(source, destination); + if (process.platform !== 'win32') chmodSync(destination, 0o755); + NODE + - name: Install locked dependencies + run: bun install --frozen-lockfile + - name: Validate and build Electron + run: bun run check:electron + - name: Package without publishing + env: + CSC_LINK: ${{ secrets.ELECTRON_CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.ELECTRON_CSC_KEY_PASSWORD }} + working-directory: electron + run: | + # An empty CSC_LINK is interpreted as the working directory by the + # signer. Omit absent credentials rather than passing empty strings. + if [ -z "${CSC_LINK:-}" ]; then + unset CSC_LINK CSC_KEY_PASSWORD + fi + bun x electron-builder --config electron-builder.config.mjs ${{ matrix.flags }} --publish never + node tests/packaging-contract.mjs --artifact + node tests/update-package-contract.mjs --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} + - name: Verify macOS signing and notarization before publication + if: inputs.publish == true && inputs.allow_unsigned != true && matrix.platform == 'darwin' + run: | + APP=$(find electron/release -maxdepth 2 -name VoiceStudio.app -type d -print -quit) + test -n "$APP" + codesign --verify --deep --strict "$APP" + spctl --assess --type execute --verbose=2 "$APP" + - name: Verify Windows installer signature before publication + if: inputs.publish == true && inputs.allow_unsigned != true && matrix.platform == 'win32' + shell: pwsh + run: | + $installers = @(Get-ChildItem electron/release/VoiceStudio-Electron-*.exe) + if ($installers.Count -eq 0) { throw "No installer to verify" } + foreach ($installer in $installers) { + $signature = Get-AuthenticodeSignature $installer.FullName + if ($signature.Status -ne 'Valid') { throw "Installer signature is not trusted: $($installer.Name)" } + } + - name: Packaged startup smoke test + working-directory: electron + run: | + if [ "$RUNNER_OS" = Linux ]; then + xvfb-run -a node tests/packaged-smoke.mjs --setup + else + node tests/packaged-smoke.mjs --setup + fi + - name: Save installers and updater metadata for review + uses: actions/upload-artifact@v4 + with: + name: electron-release-${{ matrix.platform }}-${{ matrix.arch }} + retention-days: 14 + if-no-files-found: error + path: | + electron/release/VoiceStudio-Electron-* + electron/release/electron-*.yml + + release: + needs: package + runs-on: ubuntu-latest + permissions: + contents: write + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ inputs.release_tag || github.ref_name }} + SUNSET_TAG: ${{ vars.TAURI_SUNSET_TAG }} + PUBLISH: ${{ inputs.publish }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ env.RELEASE_REF }} + - uses: actions/download-artifact@v4 + with: + pattern: electron-release-* + merge-multiple: true + path: release-assets + - name: Validate all platforms before creating a release + run: | + python3 scripts/prepare_electron_release.py --assets release-assets --tag "$TAG" + - name: Preserve the final Tauri updater feeds + run: | + test -n "$SUNSET_TAG" || { echo "Set TAURI_SUNSET_TAG before releasing"; exit 1; } + # The transition tag already holds its own final Tauri feeds. + # Later releases carry copies pointing to the immutable sunset payloads. + gh release download "$SUNSET_TAG" --pattern latest.json --dir release-assets + gh release download "$SUNSET_TAG" --pattern latest-user.json --dir release-assets + python3 scripts/prepare_electron_release.py --assets release-assets --tag "$TAG" --sunset-tag "$SUNSET_TAG" + - name: Disclose explicitly accepted unsigned artifacts + if: inputs.allow_unsigned == true + run: | + cat >> release-assets/RELEASE_NOTES.md <<'EOF' + + ### Electron installer trust + These Electron installers are unsigned or ad-hoc signed and are not Apple-notarized. + Windows/macOS may show trust warnings. macOS automatic updates are unverified; + use manual installer updates. Tauri updater signatures remain independently verified. + EOF + - name: Create or update draft + run: | + if ! gh release view "$TAG" >/dev/null 2>&1; then + gh release create "$TAG" --verify-tag --draft --title "$TAG — VoiceStudio" --notes-file release-assets/RELEASE_NOTES.md + fi + test "$(gh release view "$TAG" --json isDraft --jq .isDraft)" = true || { echo "Refusing to replace a published release"; exit 1; } + gh release edit "$TAG" --notes-file release-assets/RELEASE_NOTES.md + find release-assets -maxdepth 1 -type f ! -name RELEASE_NOTES.md -print0 | xargs -0 gh release upload "$TAG" --clobber + - name: Publish only when explicitly requested + if: github.event_name == 'workflow_dispatch' && inputs.publish == true + run: gh release edit "$TAG" --draft=false --latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4a0c50e2..56492909 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,27 +27,22 @@ # each to surface PyInstaller/Tauri issues that never showed up locally on # macOS — iterate on CI. -name: Desktop Release +name: Tauri sunset (manual only) +# Legacy workflow: run once on the final Tauri version tag. +# Electron releases are owned by electron-release.yml. on: - push: - tags: ['v*'] - schedule: - # 07:00 UTC daily — rolling `preview` prerelease from `main`. The - # preview-gate job no-ops the matrix when main hasn't moved in a day. - - cron: '0 7 * * *' workflow_dispatch: inputs: draft: - description: "Create as draft release (tag push only)" - required: false + description: "Keep the final Tauri release draft until Electron artifacts are ready" default: "true" publish_preview: - description: "Publish a rolling 'preview' prerelease (updater Preview channel). Previews ALWAYS build from main — dispatching from any other branch fails the preview-gate." - required: false + description: "Legacy compatibility input; previews are retired" type: boolean default: false + permissions: contents: write # needed to attach artifacts + updater manifest to GH Release @@ -76,6 +71,16 @@ jobs: name: Tests (backend + frontend) runs-on: ubuntu-22.04 steps: + - name: Require the designated final Tauri tag + env: + SUNSET_TAG: ${{ vars.TAURI_SUNSET_TAG }} + REF: ${{ github.ref }} + PREVIEW: ${{ inputs.publish_preview }} + run: | + test -n "$SUNSET_TAG" || { echo "Set TAURI_SUNSET_TAG to the final v* tag first"; exit 1; } + test "$REF" = "refs/tags/$SUNSET_TAG" + test "$PREVIEW" != "true" + - uses: actions/checkout@v4 - name: Setup Python 3.11 @@ -476,6 +481,9 @@ jobs: fi { echo 'body<> "$GITHUB_OUTPUT" @@ -667,6 +675,7 @@ jobs: includeUpdaterJson: true - name: Build + publish Electron desktop + if: false # Electron is released independently by electron-release.yml. shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -973,7 +982,7 @@ jobs: # Writes SHA256SUMS-