diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 5f15b9d3..00000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(npm run *)" - ] - } -} diff --git a/.github/scripts/generate-latest-json.mjs b/.github/scripts/generate-latest-json.mjs new file mode 100644 index 00000000..e4ac9ce2 --- /dev/null +++ b/.github/scripts/generate-latest-json.mjs @@ -0,0 +1,87 @@ +// Generates the Tauri updater manifest (latest.json) from the signed bundles +// produced by `createUpdaterArtifacts`, then uploads it to the GitHub release. +// +// The updater endpoint in tauri.conf.json points at this file. For each platform +// it needs the signed artifact URL and the contents of the matching `.sig` file. +// +// Env: TAG (e.g. v1.5.2), VERSION (e.g. 1.5.2), GH_TOKEN (for `gh`). + +import { execFileSync } from 'node:child_process'; +import { readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'; +import { basename, join } from 'node:path'; + +const { TAG, VERSION } = process.env; +if (!TAG || !VERSION) { + console.error('TAG and VERSION env vars are required'); + process.exit(1); +} + +// 1. Map uploaded asset name -> browser download URL. +// GitHub stores asset names with spaces replaced by dots, so we match both forms. +const assets = + JSON.parse(execFileSync('gh', ['release', 'view', TAG, '--json', 'assets'], { encoding: 'utf8' })) + .assets ?? []; +const urlByName = new Map(assets.map((a) => [a.name, a.url])); + +function resolveUrl(localBasename) { + return urlByName.get(localBasename) ?? urlByName.get(localBasename.replace(/ /g, '.')) ?? null; +} + +// 2. Find every `.sig` file across the downloaded build artifacts. +function walk(dir) { + const out = []; + for (const entry of readdirSync(dir)) { + const p = join(dir, entry); + if (statSync(p).isDirectory()) out.push(...walk(p)); + else out.push(p); + } + return out; +} +const sigFiles = walk('release-artifacts').filter((f) => f.endsWith('.sig')); + +// 3. Map each updater artifact to its Tauri platform key(s). +const platforms = {}; +for (const sig of sigFiles) { + const artifact = basename(sig.slice(0, -4)); // strip ".sig" + let keys = null; + if (artifact.endsWith('-setup.exe')) { + keys = ['windows-x86_64']; // NSIS installer + } else if (artifact.endsWith('.app.tar.gz')) { + // macOS updater bundle. A universal binary has no arch in its name and reports + // its *running* arch at runtime, so it must serve both darwin keys. An explicit + // arch in the name (a dedicated single-arch build) maps to just that key. + if (/x86_64|x64/.test(artifact)) keys = ['darwin-x86_64']; + else if (/aarch64|arm64/.test(artifact)) keys = ['darwin-aarch64']; + else keys = ['darwin-aarch64', 'darwin-x86_64']; // universal + } else { + continue; // ignore non-updater signatures (.msi, .dmg, etc.) + } + + const url = resolveUrl(artifact); + if (!url) { + console.warn(`No uploaded asset URL for "${artifact}" - skipping ${keys.join(', ')}`); + continue; + } + const entry = { signature: readFileSync(sig, 'utf8').trim(), url }; + for (const key of keys) platforms[key] = entry; +} + +if (Object.keys(platforms).length === 0) { + console.error( + 'No updater platforms resolved - is createUpdaterArtifacts enabled and signing configured?' + ); + process.exit(1); +} + +// 4. Write and upload the manifest. +const manifest = { + version: VERSION, + pub_date: new Date().toISOString(), + notes: `Release ${TAG}`, + platforms, +}; +writeFileSync('latest.json', JSON.stringify(manifest, null, 2)); +console.log(JSON.stringify(manifest, null, 2)); + +execFileSync('gh', ['release', 'upload', TAG, 'latest.json', '--clobber'], { stdio: 'inherit' }); +console.log('Uploaded latest.json'); diff --git a/.github/workflows/manual-cross-platform-release.yml b/.github/workflows/manual-cross-platform-release.yml index a4325bfb..38541456 100644 --- a/.github/workflows/manual-cross-platform-release.yml +++ b/.github/workflows/manual-cross-platform-release.yml @@ -18,10 +18,13 @@ jobs: include: - os: windows-latest platform: win - build_command: npx electron-builder --win --x64 --publish never + args: '' + rust-targets: '' - os: macos-latest platform: mac - build_command: npx electron-builder --mac --publish never + # Universal binary: runs natively on both Apple Silicon and Intel (macOS 14.4+). + args: '--target universal-apple-darwin' + rust-targets: 'aarch64-apple-darwin x86_64-apple-darwin' runs-on: ${{ matrix.os }} permissions: contents: write @@ -29,42 +32,54 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 11 + - name: Setup Node uses: actions/setup-node@v4 with: node-version: 22 - cache: npm + cache: pnpm - - name: Install dependencies - run: npm ci + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Cargo + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri - - name: Build Electron main process - run: npm run electron:build-main + - name: Install dependencies + run: pnpm install --frozen-lockfile - - name: Build renderer assets - run: npm run build + - name: Add Rust targets + if: matrix.rust-targets != '' + run: rustup target add ${{ matrix.rust-targets }} - - name: Build release packages + - name: Build Tauri bundle + run: pnpm tauri build ${{ matrix.args }} env: - CSC_IDENTITY_AUTO_DISCOVERY: "false" - run: ${{ matrix.build_command }} - shell: bash + # Required for the updater to sign the bundles. Without these the + # `createUpdaterArtifacts` step cannot produce the `.sig` files and + # updates will fail signature verification on the client. + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: release-${{ matrix.platform }} + # Windows lands in target/release/bundle; the macOS universal build + # lands in target/universal-apple-darwin/release/bundle. path: | - release/*.exe - release/*.dmg - release/*.zip - release/*.yml - release/*.yaml - release/*.blockmap + src-tauri/target/release/bundle/**/* + src-tauri/target/universal-apple-darwin/release/bundle/**/* if-no-files-found: error publish: - if: ${{ inputs.publish }} + if: inputs.publish name: Publish GitHub Release needs: build runs-on: ubuntu-latest @@ -74,35 +89,52 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 11 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download artifacts uses: actions/download-artifact@v4 with: path: release-artifacts - - name: Prepare release metadata + - name: Read version id: meta run: | VERSION=$(node -p "require('./package.json').version") - TAG="v${VERSION}" echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT" - - name: Create or update release + - name: Create or update GitHub release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG="${{ steps.meta.outputs.tag }}" - TITLE="Release ${TAG}" if gh release view "$TAG" >/dev/null 2>&1; then echo "Release $TAG already exists; uploading artifacts." else - gh release create "$TAG" --title "$TITLE" --notes "Manual cross-platform build artifacts." + gh release create "$TAG" \ + --title "Release $TAG" \ + --notes "Manual cross-platform Tauri build." fi while IFS= read -r -d '' file; do echo "Uploading $(basename "$file")" gh release upload "$TAG" "$file" --clobber - done < <( - find release-artifacts -type f \ - \( -name '*.exe' -o -name '*.dmg' -o -name '*.zip' -o -name '*.yml' -o -name '*.yaml' -o -name '*.blockmap' \) \ - -print0 - ) + done < <(find release-artifacts -type f \( \ + -name '*.exe' -o -name '*.msi' -o \ + -name '*.dmg' -o -name '*.tar.gz' -o -name '*.zip' -o \ + -name '*.sig' \ + \) -print0) + + - name: Generate and upload latest.json + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.meta.outputs.tag }} + VERSION: ${{ steps.meta.outputs.version }} + run: node .github/scripts/generate-latest-json.mjs diff --git a/.gitignore b/.gitignore index 81a3c688..5c119e16 100644 --- a/.gitignore +++ b/.gitignore @@ -1,35 +1,55 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -# Environment variables -.env -.env.local -.env.*.local -.env.production - -node_modules -dist -dist-ssr -electron-dist -release -*.local - -# Claude Code -.claude - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? +# ── Logs ────────────────────────────────────────────────────────────────────── +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# ── Environment variables ────────────────────────────────────────────────────── +.env +.env.local +.env.*.local +.env.production + +# ── Node.js ─────────────────────────────────────────────────────────────────── +node_modules/ + +# ── Frontend build output ────────────────────────────────────────────────────── +dist/ +dist-ssr/ + +# ── Tauri ───────────────────────────────────────────────────────────────────── +# Rust build artifacts - can be several GiB, always regenerated by cargo +src-tauri/target/ + +# Tauri-generated mobile/desktop scaffolding (tauri android/ios init) +src-tauri/gen/ + +# Wix toolchain downloaded by tauri-plugin-bundler for Windows NSIS/MSI +src-tauri/WixTools/ + +# Tauri CLI local cache +.tauri/ + + +# ── Editor ──────────────────────────────────────────────────────────────────── +.vscode/* +!.vscode/extensions.json +.idea/ +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# ── OS ──────────────────────────────────────────────────────────────────────── +.DS_Store +Thumbs.db + +# ── Misc ────────────────────────────────────────────────────────────────────── +*.local + +# ── Claude Code ─────────────────────────────────────────────────────────────── +.claude/ diff --git a/CLAUDE.md b/CLAUDE.md index 5f9efbb0..4561be51 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,75 +1,64 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This repository is a **Tauri desktop application** for Windows and macOS. ## Commands ```bash -# Development -npm run dev # Vite renderer dev server only -npm run electron:dev-hide # Electron + renderer dev (hidden window) -npm run electron:dev-show # Electron + renderer dev (visible window) -npm start # Alias for electron:dev-hide - -# Build -npm run build # tsc + vite build (renderer) -npm run electron:build-main # Build Electron main process -npm run electron:build # Full Electron distribution build - -# Code quality -npm run lint # ESLint check -npm run format # Prettier + ESLint auto-fix +pnpm dev +pnpm tauri:dev +pnpm build +pnpm tauri:build +pnpm lint +pnpm format ``` ## Architecture -This is an **Electron desktop application** - an AI-powered live interview assistant that provides real-time transcription and AI suggestions during job interviews. +The app is built as a Tauri desktop client with a React frontend. -**Stack:** React 19 + TypeScript + Tailwind CSS + shadcn/ui (renderer), Electron 40 (main), Vite (build). +### Frontend -### Process Split +- `src/` - React, Tailwind, hooks, components, pages. +- `src/lib/tauri-bridge.ts` exposes the IPC compatibility API used by renderer hooks. -``` -src/main/ ← Electron main process (Node.js) -src/renderer/ ← React/Vite frontend -``` - -The path alias `@/*` resolves to `./renderer/*`. - -**Main process** handles: audio capture, WebSocket connections to backend, IPC with renderer, Electron Store persistence, global hotkeys, auto-updates. +### Native Backend -**Renderer** handles: UI, routing, state display. It never calls backend APIs directly - all backend communication goes through IPC to main. +- `src-tauri/src/` - Tauri command handlers, services, state, and native utilities. +- `src-tauri/tauri.conf.json` - macOS and Windows bundle settings. +- `src-tauri/Cargo.toml` - Rust dependency manifest. ### IPC Bridge -[src/main/preload.cts](src/main/preload.cts) exposes `window.electronAPI` to the renderer with namespaced APIs: `config`, `auth`, `payment`, `llm`, `appState`, `transcription`, `liveSuggestion`, `actionSuggestion`, `tools`, `window`, `autoUpdater`, `external`. - -IPC handlers live in [src/main/ipc/](src/main/ipc/) (one file per domain). Services in [src/main/services/](src/main/services/) contain the business logic called by handlers. - -### State Management (Renderer) - -Two distinct stores: - -1. **AppState** ([src/renderer/hooks/use-app-state.tsx](src/renderer/hooks/use-app-state.tsx)) - React Context, synced from main via IPC. Holds real-time interview state: running status, transcripts, AI suggestions, credits, backend health. Read-only in renderer; mutated by main process pushing updates. +- Tauri `invoke()` is exposed through `tauriApi` and assigned to `window.electronAPI` for compatibility. +- Transcription, permissions, payment, config, and window control are handled through Tauri commands. -2. **ConfigStore** ([src/renderer/hooks/use-config-store.ts](src/renderer/hooks/use-config-store.ts)) - Zustand store backed by Electron Store. Holds user settings, auth tokens, audio/video device selection, interview configuration (CV, job description). Persisted to disk. +## Key Implementation Notes -### API Layer (Main Process) +- Electron has been removed from the repository. +- The build flows are now Tauri-first. +- Native audio loopback is implemented in `src-tauri/src/commands/transcription.rs`. +- macOS screen recording permission is validated natively. +- The GitHub Actions workflow builds Tauri bundles for Windows and macOS. -[src/main/api/client.ts](src/main/api/client.ts) - `ApiClient` class: fetch-based, Bearer token auth, streaming support. Wrapped by domain-specific clients: `AuthApi`, `LLMApi`, `PaymentApi`, `HealthCheckApi` in [src/main/api/](src/main/api/). +## Build and Release Workflow -Backend URL is defined in [src/main/consts.ts](src/main/consts.ts). +The workflow at `.github/workflows/manual-cross-platform-release.yml`: -### Routing (Renderer) +- builds on Windows and macOS in parallel +- installs pnpm dependencies +- runs `pnpm tauri:build` (which builds the frontend via `beforeBuildCommand` automatically) +- uploads bundle artifacts +- publishes a GitHub release when the `publish` input is enabled -Hash-based router (required for Electron): `/` → auth flow → `/main` (interview UI) → `/payment`. +## Platform Support -Router defined in [src/renderer/router.tsx](src/renderer/router.tsx). +- Windows 11+ +- macOS 14.4+ -### Key Features +## Notes for Developers -- **Transcription:** Dual-channel (speaker + interviewer mic) via WebSocket streaming - [src/main/services/transcript-service.ts](src/main/services/transcript-service.ts) -- **Live Suggestions:** Real-time AI responses based on CV + job description - [src/main/services/live-suggestion-service.ts](src/main/services/live-suggestion-service.ts) -- **Action Suggestions:** Screenshot-based problem solving (up to 3 images) - [src/main/services/action-suggestion-service.ts](src/main/services/action-suggestion-service.ts) -- **Credits:** Purchase and usage tracking via payment API -- **Auto-Updates:** electron-updater publishing to GitHub releases +- There is no `src/main/` Electron host code in this repo anymore. +- Use the Tauri app as the single desktop implementation. +- Update native dependencies in `src-tauri/Cargo.toml` and frontend dependencies in `package.json`. +- Package manager is pnpm - do not use npm or yarn. diff --git a/README.md b/README.md index 898c4114..ac3ca798 100644 --- a/README.md +++ b/README.md @@ -1,193 +1,109 @@ -# Power Interview AI - Privacy-First AI Interview Assistant - -