From 0c8b0d30a77a1b45d1c5c04e0605ea868398f99d Mon Sep 17 00:00:00 2001 From: PurplePrintAI <269393167+PurplePrintAI@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:12:36 +0900 Subject: [PATCH 1/2] build: add macOS subscription prerelease --- .github/workflows/announce-release.yml | 1 + .github/workflows/build-fork-macos.yml | 282 +++++++++++++++++++ .github/workflows/build.yml | 1 + FORK-CHANGELOG.md | 33 +++ FORK.md | 24 ++ MACOS-INSTALLATION.md | 57 ++++ electron-builder.fork.json | 7 +- electron/ai-edition/claude/cli.test.ts | 14 +- electron/ai-edition/claude/cli.ts | 15 +- electron/ai-edition/codex/app-server.test.ts | 17 +- electron/ai-edition/codex/app-server.ts | 44 ++- package-lock.json | 4 +- package.json | 5 +- scripts/stage-whisper-stt.sh | 29 +- 14 files changed, 508 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/build-fork-macos.yml create mode 100644 MACOS-INSTALLATION.md diff --git a/.github/workflows/announce-release.yml b/.github/workflows/announce-release.yml index 078195c88..a20163821 100644 --- a/.github/workflows/announce-release.yml +++ b/.github/workflows/announce-release.yml @@ -51,6 +51,7 @@ permissions: jobs: announce: name: Announce ${{ inputs.tag || github.event.release.tag_name }} + if: github.event_name == 'workflow_dispatch' || !contains(github.event.release.tag_name, '-subscription.') runs-on: ubuntu-latest steps: - name: Checkout diff --git a/.github/workflows/build-fork-macos.yml b/.github/workflows/build-fork-macos.yml new file mode 100644 index 000000000..b38174f91 --- /dev/null +++ b/.github/workflows/build-fork-macos.yml @@ -0,0 +1,282 @@ +name: Build OpenScreen Subscription for macOS + +on: + workflow_dispatch: + inputs: + whisper_run_id: + description: "Successful Build whisper-stt binaries run ID to package" + required: true + type: string + +permissions: + contents: read + +concurrency: + group: fork-macos-${{ github.ref_name }} + cancel-in-progress: false + +jobs: + build: + name: macOS ${{ matrix.label }} + strategy: + fail-fast: false + matrix: + include: + - arch: arm64 + label: Apple Silicon + runner: macos-latest + binary_arch: arm64 + - arch: x64 + label: Intel + runner: macos-15-intel + binary_arch: x86_64 + runs-on: ${{ matrix.runner }} + steps: + - name: Checkout source + uses: actions/checkout@v7 + + - name: Setup Node.js + uses: ./.github/actions/setup + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: "3.11" + + - name: Keep sharp on its prebuilt binary + run: npm rebuild sharp + env: + npm_config_build_from_source: "false" + + - name: Stage reviewed speech runtime + run: bash scripts/stage-whisper-stt.sh darwin-${{ matrix.arch }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OPENSCREEN_WHISPER_RUN_ID: ${{ inputs.whisper_run_id }} + + - name: Test ScreenCaptureKit package + run: npm run test:swift:mac + + - name: Build ScreenCaptureKit helper + run: npm run build:native:mac + env: + OPENSCREEN_MAC_HELPER_ARCHS: ${{ matrix.arch }} + + - name: Cache LGPL FFmpeg tree + uses: actions/cache@v6 + with: + path: crates/thirdparty + key: fork-ffmpeg-macos-${{ matrix.arch }}-${{ hashFiles('scripts/fetch-ffmpeg-macos.mjs') }} + + - name: Vendor pinned LGPL FFmpeg + run: npm run fetch:ffmpeg:mac + + - name: Cache Rust dependencies + uses: actions/cache@v6 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + crates/target + key: fork-cargo-macos-${{ matrix.arch }}-${{ hashFiles('crates/Cargo.lock') }} + restore-keys: | + fork-cargo-macos-${{ matrix.arch }}- + + - name: Build Metal compositor + run: npm run build:native:compositor:mac + + - name: Build renderer and Electron main process + run: npm run build-vite + + - name: Package fork app bundle + run: npx electron-builder --mac --${{ matrix.arch }} --dir --config electron-builder.fork.json --publish never + env: + CSC_IDENTITY_AUTO_DISCOVERY: "false" + + - name: Locate app bundle + id: app + run: | + set -euo pipefail + VERSION="$(node -p 'require("./package.json").version')" + APP="$(find "release/${VERSION}" -maxdepth 3 -name 'OpenScreen Subscription.app' -type d -print -quit)" + [ -n "$APP" ] || { echo "::error::OpenScreen Subscription.app was not packaged"; exit 1; } + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "path=${APP}" >> "$GITHUB_OUTPUT" + + - name: Apply ad-hoc hardened-runtime signature + run: | + codesign --force --deep --sign - \ + --options runtime \ + --entitlements macos.entitlements \ + "${{ steps.app.outputs.path }}" + + - name: Verify bundle identity, architecture and payload + run: | + set -euo pipefail + APP="${{ steps.app.outputs.path }}" + BIN="${APP}/Contents/MacOS/OpenScreen Subscription" + RESOURCES="${APP}/Contents/Resources" + NATIVE="${RESOURCES}/electron/native/bin/darwin-${{ matrix.arch }}" + + test -x "$BIN" + test -x "${NATIVE}/openscreen-screencapturekit-helper" + test -f "${NATIVE}/compositor_view.node" + test -x "${NATIVE}/whisper-stt-server" + test -f "${RESOURCES}/FORK.md" + test -f "${RESOURCES}/FORK-CHANGELOG.md" + test -f "${RESOURCES}/MACOS-INSTALLATION.md" + test -f "${RESOURCES}/LICENSE" + test -f "${RESOURCES}/THIRD-PARTY-NOTICES.md" + + BUNDLE_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "${APP}/Contents/Info.plist")" + [ "$BUNDLE_ID" = "io.github.purpleprintai.openscreen-subscription" ] \ + || { echo "::error::Unexpected bundle ID ${BUNDLE_ID}"; exit 1; } + + ACTUAL_ARCHS="$(lipo -archs "$BIN")" + case " ${ACTUAL_ARCHS} " in + *" ${{ matrix.binary_arch }} "*) ;; + *) echo "::error::Expected ${{ matrix.binary_arch }}, got ${ACTUAL_ARCHS}"; exit 1 ;; + esac + + codesign --verify --deep --strict "$APP" + SIGNING_ID="$(codesign -dv --verbose=2 "$APP" 2>&1 | sed -n 's/^Identifier=//p')" + [ "$SIGNING_ID" = "$BUNDLE_ID" ] \ + || { echo "::error::Signature identifier ${SIGNING_ID} does not match ${BUNDLE_ID}"; exit 1; } + + - name: Smoke-test packaged CLI boot + run: | + set -euo pipefail + BIN="${{ steps.app.outputs.path }}/Contents/MacOS/OpenScreen Subscription" + "$BIN" help > cli-help.txt + grep -q "OpenScreen CLI" cli-help.txt + + - name: Create DMG + id: dmg + run: | + set -euo pipefail + VERSION="${{ steps.app.outputs.version }}" + case "${{ matrix.arch }}" in + arm64) LABEL="Apple-Silicon" ;; + x64) LABEL="Intel" ;; + esac + OUT="release/${VERSION}/OpenScreen-Subscription-${VERSION}-macOS-${LABEL}.dmg" + STAGING="release/${VERSION}/dmg-${{ matrix.arch }}" + rm -rf "$STAGING" "$OUT" + mkdir -p "$STAGING" + cp -R "${{ steps.app.outputs.path }}" "$STAGING/" + ln -s /Applications "$STAGING/Applications" + hdiutil create \ + -srcfolder "$STAGING" \ + -volname "OpenScreen Subscription" \ + -fs HFS+ \ + -format UDBZ \ + "$OUT" + rm -rf "$STAGING" + echo "path=${OUT}" >> "$GITHUB_OUTPUT" + + - name: Mount and verify DMG + run: | + set -euo pipefail + MOUNT="$(mktemp -d)" + cleanup() { + hdiutil detach "$MOUNT" -quiet 2>/dev/null || true + rmdir "$MOUNT" 2>/dev/null || true + } + trap cleanup EXIT + hdiutil attach -readonly -nobrowse -mountpoint "$MOUNT" "${{ steps.dmg.outputs.path }}" >/dev/null + APP="${MOUNT}/OpenScreen Subscription.app" + test -d "$APP" + test -L "${MOUNT}/Applications" + codesign --verify --deep --strict "$APP" + [ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "${APP}/Contents/Info.plist")" \ + = "io.github.purpleprintai.openscreen-subscription" ] + + - name: Write validation metadata + run: | + DMG="${{ steps.dmg.outputs.path }}" \ + ARCH="${{ matrix.arch }}" \ + VERSION="${{ steps.app.outputs.version }}" \ + WHISPER_RUN_ID="${{ inputs.whisper_run_id }}" \ + SOURCE_COMMIT="${{ github.sha }}" \ + node --input-type=module <<'NODE' + import { createHash } from "node:crypto"; + import { readFileSync, writeFileSync } from "node:fs"; + const bytes = readFileSync(process.env.DMG); + const report = { + product: "OpenScreen Subscription", + version: process.env.VERSION, + architecture: process.env.ARCH, + bundleId: "io.github.purpleprintai.openscreen-subscription", + sourceCommit: process.env.SOURCE_COMMIT, + whisperBuildRunId: Number(process.env.WHISPER_RUN_ID), + signature: "ad-hoc hardened runtime", + notarized: false, + checks: { + nativePayloadPresent: true, + bundleIdentity: true, + architecture: true, + codeSignatureStructure: true, + packagedCliBoot: true, + dmgMount: true + }, + dmg: { + name: process.env.DMG.split("/").at(-1), + bytes: bytes.length, + sha256: createHash("sha256").update(bytes).digest("hex") + } + }; + writeFileSync(`macos-validation-${process.env.ARCH}.json`, `${JSON.stringify(report, null, 2)}\n`); + NODE + shasum -a 256 "${{ steps.dmg.outputs.path }}" > "SHA256-${{ matrix.arch }}.txt" + + - name: Upload architecture package + uses: actions/upload-artifact@v7 + with: + name: openscreen-subscription-macos-${{ matrix.arch }} + path: | + ${{ steps.dmg.outputs.path }} + macos-validation-${{ matrix.arch }}.json + SHA256-${{ matrix.arch }}.txt + cli-help.txt + if-no-files-found: error + retention-days: 30 + + collect: + name: Collect macOS release assets + needs: build + runs-on: ubuntu-latest + steps: + - name: Checkout release documentation + uses: actions/checkout@v7 + + - name: Download Apple Silicon package + uses: actions/download-artifact@v8 + with: + name: openscreen-subscription-macos-arm64 + path: artifacts/arm64 + + - name: Download Intel package + uses: actions/download-artifact@v8 + with: + name: openscreen-subscription-macos-x64 + path: artifacts/x64 + + - name: Flatten and checksum release files + run: | + set -euo pipefail + mkdir release-assets + find artifacts -type f -name '*.dmg' -exec cp {} release-assets/ \; + find artifacts -type f -name 'macos-validation-*.json' -exec cp {} release-assets/ \; + cp MACOS-INSTALLATION.md release-assets/ + ( + cd release-assets + sha256sum *.dmg > SHA256SUMS-macOS.txt + ) + + - name: Upload combined release assets + uses: actions/upload-artifact@v7 + with: + name: openscreen-subscription-macos-release + path: release-assets + if-no-files-found: error + retention-days: 30 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d6ecbfb6..8adbd6e62 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,7 @@ on: push: tags: - "v*" + - "!v*-subscription.*" workflow_dispatch: inputs: arch: diff --git a/FORK-CHANGELOG.md b/FORK-CHANGELOG.md index c676c9b23..5f8275729 100644 --- a/FORK-CHANGELOG.md +++ b/FORK-CHANGELOG.md @@ -26,6 +26,39 @@ see [FORK.md](FORK.md). | Context display | Separates estimated selected history, the user's reference value, and a verified context window for the active provider/model when available. | | Distribution | Uses a separate app identity, user-data directory, installer name, update origin and release configuration from upstream. | +## 1.10.0-subscription.6 — 2026-08-31 + +### macOS validation prerelease + +- Adds fork-branded DMG builds for both Apple Silicon and Intel Macs, preserving the + fork bundle identifier, product name, user-data separation and GitHub update origin. +- Verifies the ScreenCaptureKit helper, Metal compositor, speech runtime, executable + architecture, bundle identity, ad-hoc hardened-runtime signature, packaged CLI boot + and mounted DMG before collecting release assets. +- Pins speech-runtime staging to an explicit successful producer run. The previous + release workflow asked `gh run download` to inspect its own still-running job and + failed before either macOS package could be built. +- Prevents subscription tags and releases from entering the upstream-oriented build + and Discord announcement paths. + +### Local AI compatibility + +- Finds Codex CLI and Claude Code when OpenScreen is launched from Finder, whose PATH + normally omits Homebrew and user package-manager directories. +- Checks `~/.local/bin`, `~/.npm-global/bin`, `~/.volta/bin`, `~/Library/pnpm`, + `/opt/homebrew/bin` and `/usr/local/bin` without invoking a shell or copying CLI + credentials. + +### Distribution status + +- macOS packages remain prerelease-only until a real Mac completes the manual + record → edit → export checklist. +- The initial packages are ad-hoc signed and not notarized by Apple. Installation and + privacy-permission guidance is recorded in [MACOS-INSTALLATION.md](MACOS-INSTALLATION.md). +- Local validation passed 2,243 unit tests with 4 skipped across 189 files, both + application and test TypeScript checks, documentation, i18n and Biome. Biome + retained 14 pre-existing warnings. + ## 1.10.0-subscription.5 — 2026-08-30 ### Added and changed diff --git a/FORK.md b/FORK.md index e359a02ee..18b52eb9a 100644 --- a/FORK.md +++ b/FORK.md @@ -78,6 +78,30 @@ The installer is unsigned unless an independently authorized signing identity is configured. No upstream publisher identity is borrowed. Windows security prompts must be handled by the user, not disabled by this fork or its build scripts. +## macOS prerelease procedure + +macOS packages must be built natively on the matching GitHub-hosted architecture: +Apple Silicon on `macos-latest` and Intel on `macos-15-intel`. Cross-packaging an +Intel app from an Apple Silicon runner is not accepted because the capture helper, +FFmpeg and Metal compositor are host-architecture builds. + +1. Run **Build whisper-stt binaries** against the release source and retain its + successful run ID. +2. Dispatch **Build OpenScreen Subscription for macOS** with that exact run ID. + This prevents a release/tag workflow from racing a still-running dependency build. +3. Package with `electron-builder.fork.json`; never inherit the upstream bundle ID, + product name or update repository. +4. Require both architecture jobs to verify the native payload, bundle ID, executable + architecture, code-signature structure, packaged CLI boot and mounted DMG. +5. Publish the combined DMGs, validation JSON and SHA-256 file only as a prerelease + until a real Mac completes the manual record → edit → export checklist. + +Without independently authorized Apple Developer credentials, the workflow applies +an ad-hoc hardened-runtime signature so macOS privacy grants attach to the fork bundle +identifier. This is not Apple notarization. The release notes and +[MACOS-INSTALLATION.md](MACOS-INSTALLATION.md) must retain the Gatekeeper limitation; +never borrow upstream credentials or advise users to disable Gatekeeper globally. + ## Attribution and redistribution Keep `LICENSE` and `THIRD-PARTY-NOTICES.md` in source and installed resources. diff --git a/MACOS-INSTALLATION.md b/MACOS-INSTALLATION.md new file mode 100644 index 000000000..4faf332d8 --- /dev/null +++ b/MACOS-INSTALLATION.md @@ -0,0 +1,57 @@ +# OpenScreen Subscription on macOS + +OpenScreen Subscription is an unofficial fork. The current macOS packages are +validation prereleases with the fork bundle identifier and an ad-hoc hardened-runtime +signature. They are **not notarized by Apple**. + +## Choose the package + +- **Apple Silicon**: M1, M2, M3, M4 or newer Apple chips (`arm64`). +- **Intel**: Macs whose processor is shown as Intel (`x64`). + +Open **Apple menu → About This Mac** if the architecture is uncertain. Do not install +the Intel package on Apple Silicon merely because Rosetta can start it; recording, +preview and export would all run through translation. + +## Install and open + +1. Download the matching `.dmg` from this fork's GitHub prerelease. +2. Open the DMG and drag **OpenScreen Subscription** to **Applications**. +3. In Applications, Control-click **OpenScreen Subscription**, choose **Open**, and + confirm the first launch. This is the per-app Gatekeeper path for an unnotarized + validation build. Do not disable Gatekeeper globally. +4. Grant only the permissions needed for the features you use. macOS may request + Screen & System Audio Recording, Accessibility, Microphone or Camera access. +5. Fully quit and reopen the app after changing a privacy permission. + +Because the app uses the fork's own bundle identifier +`io.github.purpleprintai.openscreen-subscription`, its privacy grants are separate +from the official OpenScreen app. + +## Local AI runtimes + +Codex CLI and Claude Code are optional external dependencies. They are never bundled +in the DMG. + +- ChatGPT subscription: install the official Codex CLI, then connect the account in + OpenScreen's AI settings. +- Claude subscription: install the official Claude Code CLI and run + `claude auth login` in Terminal before checking the connection in OpenScreen. + +The packaged app checks the inherited PATH and common Finder-safe locations: +`~/.local/bin`, `~/.npm-global/bin`, `~/.volta/bin`, `~/Library/pnpm`, +`/opt/homebrew/bin` and `/usr/local/bin`. The corresponding command should work in +Terminal (`codex --version` or `claude --version`) before troubleshooting OpenScreen. + +## Validation boundary + +GitHub Actions verifies both architectures, the ScreenCaptureKit helper, Metal +compositor, speech runtime, bundle identity, code-signature structure, packaged CLI +boot and a mountable DMG. A real Mac user must still verify the operating-system +permission prompts and a record → edit → export pass before this build is promoted +beyond prerelease status. + +Report failures at +[PurplePrintAI/openscreen-subscription issues](https://github.com/PurplePrintAI/openscreen-subscription/issues) +with the Mac model, macOS version, package architecture and a diagnostic export. Do +not include account tokens, CLI credential files or private recording contents. diff --git a/electron-builder.fork.json b/electron-builder.fork.json index 34ff16226..64513ca1a 100644 --- a/electron-builder.fork.json +++ b/electron-builder.fork.json @@ -17,6 +17,10 @@ "target": ["nsis"], "artifactName": "OpenScreen-Subscription-${version}-Windows-x64-Setup.${ext}" }, + "mac": { + "target": ["dmg"], + "artifactName": "OpenScreen-Subscription-${version}-macOS-${arch}.${ext}" + }, "nsis": { "oneClick": false, "allowToChangeInstallationDirectory": true, @@ -26,6 +30,7 @@ }, "extraResources": [ { "from": "FORK.md", "to": "FORK.md" }, - { "from": "FORK-CHANGELOG.md", "to": "FORK-CHANGELOG.md" } + { "from": "FORK-CHANGELOG.md", "to": "FORK-CHANGELOG.md" }, + { "from": "MACOS-INSTALLATION.md", "to": "MACOS-INSTALLATION.md" } ] } diff --git a/electron/ai-edition/claude/cli.test.ts b/electron/ai-edition/claude/cli.test.ts index 62d516a7d..e98b1c3ce 100644 --- a/electron/ai-edition/claude/cli.test.ts +++ b/electron/ai-edition/claude/cli.test.ts @@ -1,6 +1,6 @@ import { type ChildProcessWithoutNullStreams, type spawn } from "node:child_process"; import { EventEmitter } from "node:events"; -import { mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { PassThrough } from "node:stream"; @@ -216,6 +216,18 @@ describe("Claude local runtime", () => { ); }); + it("finds a user-installed CLI when a Finder launch has an empty PATH", async () => { + const base = await directory(); + const executable = path.join(base, ".local", "bin", "claude"); + await mkdir(path.dirname(executable), { recursive: true }); + await writeFile(executable, "fixture"); + const posixHome = base.replace(/\\/g, "/"); + + expect(findClaudeExecutable({ HOME: posixHome, PATH: "" }, "darwin", "arm64")).toBe( + `${posixHome}/.local/bin/claude`, + ); + }); + it("streams UTF-8 JSONL without duplicate final text and preserves CLI-owned authentication", async () => { vi.stubEnv("ANTHROPIC_API_KEY", "user-owned-key"); vi.stubEnv("CLAUDE_CONFIG_DIR", "user-owned-profile"); diff --git a/electron/ai-edition/claude/cli.ts b/electron/ai-edition/claude/cli.ts index edf6b91c8..a8524f3eb 100644 --- a/electron/ai-edition/claude/cli.ts +++ b/electron/ai-edition/claude/cli.ts @@ -126,7 +126,20 @@ export function findClaudeExecutable( .filter(Boolean) .map((directory) => paths.join(directory.replace(/^"|"$/g, ""), filename)); const userDirectory = env.USERPROFILE ?? env.HOME; - if (userDirectory) candidates.push(paths.join(userDirectory, ".local", "bin", filename)); + if (platform !== "win32") { + if (userDirectory) { + for (const directory of [ + [".local", "bin"], + [".npm-global", "bin"], + [".volta", "bin"], + ["Library", "pnpm"], + ]) + candidates.push(paths.join(userDirectory, ...directory, filename)); + } + // Finder-launched apps normally miss both Homebrew prefixes in PATH. + candidates.push(paths.join("/opt/homebrew/bin", filename)); + candidates.push(paths.join("/usr/local/bin", filename)); + } if (platform === "win32" && env.APPDATA) { const root = paths.join(env.APPDATA, "npm", "node_modules", "@anthropic-ai", "claude-code"); candidates.push(paths.join(root, "bin", filename)); diff --git a/electron/ai-edition/codex/app-server.test.ts b/electron/ai-edition/codex/app-server.test.ts index 0bee9f068..a6e539cb7 100644 --- a/electron/ai-edition/codex/app-server.test.ts +++ b/electron/ai-edition/codex/app-server.test.ts @@ -1,11 +1,11 @@ import type { spawn } from "node:child_process"; import { EventEmitter } from "node:events"; -import { mkdtempSync, rmSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { PassThrough, Writable } from "node:stream"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { CodexAppServer } from "./app-server"; +import { CodexAppServer, findCodexExecutable } from "./app-server"; type Packet = { id?: number; @@ -74,6 +74,19 @@ function fixture( } describe("Codex app-server subscription transport", () => { + it("finds a user-installed CLI when a Finder launch has an empty PATH", () => { + const home = mkdtempSync(path.join(os.tmpdir(), "openscreen-codex-home-")); + cleanups.push(() => rmSync(home, { recursive: true, force: true })); + const executable = path.join(home, ".local", "bin", "codex"); + mkdirSync(path.dirname(executable), { recursive: true }); + writeFileSync(executable, "fixture"); + const posixHome = home.replace(/\\/g, "/"); + + expect(findCodexExecutable({ HOME: posixHome, PATH: "" }, "darwin", "arm64")).toBe( + `${posixHome}/.local/bin/codex`, + ); + }); + it("isolates the profile, strips API billing credentials and initializes once", async () => { vi.stubEnv("OPENAI_API_KEY", "should-not-reach-subscription"); vi.stubEnv("CODEX_API_KEY", "also-not-subscription"); diff --git a/electron/ai-edition/codex/app-server.ts b/electron/ai-edition/codex/app-server.ts index 259e3227f..e787d1f44 100644 --- a/electron/ai-edition/codex/app-server.ts +++ b/electron/ai-edition/codex/app-server.ts @@ -1,5 +1,5 @@ import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process"; -import { existsSync } from "node:fs"; +import { statSync } from "node:fs"; import { mkdir } from "node:fs/promises"; import path from "node:path"; import { createInterface } from "node:readline"; @@ -56,33 +56,57 @@ function message(error: unknown): string { /** Resolve an actual executable; never pass prompts, JSON or model names through a shell. */ export function findCodexExecutable( - env: NodeJS.ProcessEnv = process.env, + env: Record = process.env, platform = process.platform, arch = process.arch, ): string { + const paths = platform === "win32" ? path.win32 : path.posix; + const isFile = (candidate: string) => { + try { + return statSync(candidate).isFile(); + } catch { + return false; + } + }; const override = env.OPENSCREEN_CODEX_EXECUTABLE?.trim(); if (override) { - if (!path.isAbsolute(override) || /\.(cmd|bat|ps1)$/i.test(override)) { + if (!paths.isAbsolute(override) || /\.(cmd|bat|ps1)$/i.test(override)) { throw new Error("OPENSCREEN_CODEX_EXECUTABLE must point to the native Codex executable."); } - if (!existsSync(override)) throw new Error("The configured Codex executable does not exist."); + if (!isFile(override)) throw new Error("The configured Codex executable does not exist."); return override; } const filename = platform === "win32" ? "codex.exe" : "codex"; const candidates = (env.PATH ?? env.Path ?? "") - .split(path.delimiter) + .split(platform === "win32" ? ";" : ":") .filter(Boolean) - .map((dir) => path.join(dir.replace(/^"|"$/g, ""), filename)); + .map((directory) => paths.join(directory.replace(/^"|"$/g, ""), filename)); + const userDirectory = env.USERPROFILE ?? env.HOME; + if (platform !== "win32") { + if (userDirectory) { + for (const directory of [ + [".local", "bin"], + [".npm-global", "bin"], + [".volta", "bin"], + ["Library", "pnpm"], + ]) + candidates.push(paths.join(userDirectory, ...directory, filename)); + } + // Apps opened from Finder do not inherit the user's interactive shell PATH. + // These are the standard Apple Silicon and Intel Homebrew/global npm prefixes. + candidates.push(paths.join("/opt/homebrew/bin", filename)); + candidates.push(paths.join("/usr/local/bin", filename)); + } if (platform === "win32" && env.APPDATA) { const triple = arch === "arm64" ? "aarch64-pc-windows-msvc" : "x86_64-pc-windows-msvc"; - const root = path.join(env.APPDATA, "npm", "node_modules", "@openai", "codex"); - for (const base of [root, path.join(root, "node_modules", "@openai", `codex-win32-${arch}`)]) { + const root = paths.join(env.APPDATA, "npm", "node_modules", "@openai", "codex"); + for (const base of [root, paths.join(root, "node_modules", "@openai", `codex-win32-${arch}`)]) { for (const binDir of ["bin", "codex"]) { - candidates.push(path.join(base, "vendor", triple, binDir, filename)); + candidates.push(paths.join(base, "vendor", triple, binDir, filename)); } } } - const found = candidates.find((candidate) => existsSync(candidate)); + const found = candidates.find(isFile); if (!found) throw new Error("Codex CLI was not found. Install Codex CLI, then retry the connection."); return found; diff --git a/package-lock.json b/package-lock.json index 5d3458619..b5b682869 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openscreen", - "version": "1.10.0-subscription.5", + "version": "1.10.0-subscription.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openscreen", - "version": "1.10.0-subscription.5", + "version": "1.10.0-subscription.6", "license": "MIT", "dependencies": { "@fix-webm-duration/fix": "^1.0.1", diff --git a/package.json b/package.json index 51d0ef2e8..835f67575 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openscreen", "private": true, - "version": "1.10.0-subscription.5", + "version": "1.10.0-subscription.6", "description": "Record your screen and polish the demo", "homepage": "https://getopenscreen.com/", "license": "MIT", @@ -93,7 +93,8 @@ "fetch:ffmpeg": "node scripts/fetch-ffmpeg.mjs", "fetch:ffmpeg:mac": "node scripts/fetch-ffmpeg-macos.mjs", "fetch:ffmpeg:sdk": "node scripts/fetch-ffmpeg.mjs --sdk-only", - "build:win:fork": "npm run build:native:win && npm run fetch:ffmpeg && node scripts/stage-fork-speech.mjs && npm run stage:vcomp && npm run build:native:compositor && tsc && vite build && electron-builder --win nsis --x64 --config electron-builder.fork.json --config.npmRebuild=false --publish never" + "build:win:fork": "npm run build:native:win && npm run fetch:ffmpeg && node scripts/stage-fork-speech.mjs && npm run stage:vcomp && npm run build:native:compositor && tsc && vite build && electron-builder --win nsis --x64 --config electron-builder.fork.json --config.npmRebuild=false --publish never", + "build:mac:fork": "npm run build:native:mac && npm run fetch:ffmpeg:mac && npm run build:native:compositor:mac && tsc && vite build && electron-builder --mac --config electron-builder.fork.json --publish never" }, "dependencies": { "@fix-webm-duration/fix": "^1.0.1", diff --git a/scripts/stage-whisper-stt.sh b/scripts/stage-whisper-stt.sh index cb09ca555..27961f80c 100755 --- a/scripts/stage-whisper-stt.sh +++ b/scripts/stage-whisper-stt.sh @@ -79,18 +79,35 @@ fi TMP="$(mktemp -d)" trap 'rm -rf "${TMP}"' EXIT -echo "Fetching ${ARTIFACT} from the latest successful build-whisper-stt run..." -# No run id: gh resolves the most recent run that published this artifact. -# Artifacts expire (retention-days in build-whisper-stt.yml), so a stale branch -# can legitimately find nothing — say so in terms someone can act on. -if ! gh run download --repo "${REPO}" --name "${ARTIFACT}" --dir "${TMP}" 2>"${TMP}/err"; then +echo "Resolving ${ARTIFACT} from a successful build-whisper-stt run..." +# `gh run download` without an ID resolves the current workflow run under Actions, +# not the latest run that produced the named artifact. The release workflow and +# the speech build start together on a tag, so the old command deterministically +# inspected the release run before it had artifacts and failed every platform. +# Resolve the completed producer explicitly. A release may pin a reviewed run ID +# through OPENSCREEN_WHISPER_RUN_ID; otherwise use the newest successful producer. +RUN_ID="${OPENSCREEN_WHISPER_RUN_ID:-}" +if [ -z "${RUN_ID}" ]; then + RUN_ID="$(gh run list --repo "${REPO}" --workflow build-whisper-stt.yml --status success \ + --limit 1 --json databaseId --jq '.[0].databaseId' 2>"${TMP}/list-err" || true)" +fi +if [ -z "${RUN_ID}" ]; then + cat "${TMP}/list-err" >&2 2>/dev/null || true + echo "FATAL: no successful build-whisper-stt run is available in ${REPO}." >&2 + exit 1 +fi +echo "Using build-whisper-stt run ${RUN_ID}." +# Artifacts expire (retention-days in build-whisper-stt.yml), so a stale run can +# legitimately have no downloadable files — say so in terms someone can act on. +if ! gh run download "${RUN_ID}" --repo "${REPO}" --name "${ARTIFACT}" --dir "${TMP}" 2>"${TMP}/err"; then cat "${TMP}/err" >&2 cat >&2 < Date: Mon, 31 Aug 2026 16:16:11 +0900 Subject: [PATCH 2/2] fix(nix): refresh subscription dependency hash --- nix/package.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/package.nix b/nix/package.nix index d92712cb8..74838ff6e 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -51,7 +51,7 @@ buildNpmPackage { ); }; - npmDepsHash = "sha256-QHLIgb4jFy9Vtt74kHjUnQeUvTZUf6/UTl/cSdtvuMM="; + npmDepsHash = "sha256-SDnZHgTCq/oBxFKfAF0AS1McQ43iOVsZdMFpmDpP5Tc="; env.ELECTRON_SKIP_BINARY_DOWNLOAD = "1";