Repository navigation
Vite/Bun split + native macOS app (Tauri), CI/CD, and public download - #29
Merged
Merged
Conversation
Adds a root pnpm-workspace.yaml so client (Vite/Next, unchanged for now) and the new server package share one lockfile and one dependency install, matching the client+server layout this migration is building toward. Moves the onlyBuiltDependencies build-script allowlist from client's package.json to the workspace root, where pnpm now expects it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Completes the pnpm workspace scaffolding from the previous commit: the root package.json holds cross-package dev/build/test orchestration scripts (concurrently runs client + server dev servers together), and pnpm-lock.yaml is now the single workspace-wide lockfile replacing client's standalone one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Starts the server package that will replace client's Next.js API routes (see claude.md's Tauri-readiness goal). Bun is the runtime instead of Node specifically to avoid the native-binding packaging pain better-sqlite3 would cause when this later becomes a Tauri sidecar. Prisma schema/migrations are copied over unchanged -- the schema has no adapter-specific config, so moving it doesn't touch the data model, only where it lives. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Verified empirically before writing any other server code (this was the single riskiest unknown in the migration plan): copied the real dev SQLite database and confirmed @prisma/adapter-libsql reads it correctly under Bun on macOS, including the derived-fields query shape the dashboard needs. The exported class is PrismaLibSql (not PrismaLibSQL as Prisma's own docs examples suggest) -- confirmed against the installed 7.10.0 package's actual type declarations. schema.prisma has no adapter-specific config, so every existing .findMany()/.include()/etc. call site is unaffected -- only the driver construction in client.ts changes. Also copies the plain type files (types/*.ts) that both client and server need -- small enough to duplicate by hand rather than add a third shared-types workspace package for this round. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
lib/ai (OpenAI Whisper transcription, filler-word detection) and lib/captions (SRT/VTT/ASS generation, burned-in caption styling) are already server-only, framework-agnostic Node code -- zero Next.js imports -- so this is a straight copy, not a rewrite. captions/style.ts and captions/generate.ts are dual-use (also imported by client components for the live caption preview), so they're duplicated here rather than moved -- captions/format.ts is server-only and lives only here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
lib/ffmpeg (filter/property mapping, ffmpeg argv builder, video probing, the render-job orchestrator, and the execFile wrapper) moves unchanged -- it's already framework-agnostic Node code, and run.ts's execFile-with-argv-array pattern (never a shell string, per claude.md section 18) is preserved verbatim. render-job.ts's fire-and-forget error handling matters more here than it did under Next: its whole body is one try/catch that always resolves to a status update, never rethrows, so an unhandled rejection can't take down Bun's long-lived server process the way it could have taken down a per-request Next.js function differently. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
lib/video (filter presets, logo positioning, CSS/SVG property mapping)
and lib/timeline/{cuts,sentences} are dual-use -- also imported by
client components for live preview -- so they're duplicated here rather
than moved. video/logo.ts's server copy drops logoPositionToCss (the
CSS-preview-only function) and its React import, so this package has no
React dependency; the ffmpeg export path it shares with the client
(logoPositionToOverlayXY) is unaffected.
lib/validation (Zod request schemas) is server-only -- only ever
imported from API route handlers -- and moves unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
writeAsset/statAsset/ensureProjectDirs/deleteProjectDir/path-escape validation move unchanged. Drops readAssetStream and streamToWebReadable -- a ~60-line hand-rolled Node-stream-to-Web-stream bridge written to patch a NextResponse-specific abort race -- since Bun's route handlers serve files directly via Bun.file(path) as a Response body, which handles Range requests, Content-Length, and client-abort natively (verified against Bun v1.4.2). Adds writeAssetStream, used by the upload route rewrite: Bun.write(path, request.body) streams a request body straight to disk without buffering the whole file in memory first, unlike the old formData()/arrayBuffer() approach. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mechanical port from the old Next route handlers (same Zod schemas, same Prisma calls) with one real behavior addition: GET /projects now computes hasVideo/hasTranscript/cutCount itself. The old Next route returned raw Prisma rows -- that derived shape was previously computed only inside the dashboard's Server Component page, which won't exist once the client moves to Vite, so it has to live in the API response instead. Verified against the real dev database via curl -- list and detail responses match the old Next routes byte-for-byte (aside from the new derived fields). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Returning Bun.file(path) directly as the Response body natively handles the incoming Range header -- 206 + Content-Range/Accept-Ranges on a partial request, plain 200 without one -- confirmed empirically against Bun v1.4.2, including through a Hono handler specifically (not just Bun.serve directly). This replaces ~35 lines of manual range-regex parsing and manual header construction the old Next route needed. Verified against a real project's video asset: byte ranges, Content- Range headers, and full-file responses all match what the old Next route produced. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deliberate wire-contract change from the old Next route: the request body is now the raw video bytes (Content-Type: video/*, filename via a ?filename= query param) instead of multipart/form-data. This lets the server stream straight to disk via Bun.write(path, request.body) without ever buffering the whole upload in memory -- the old route's request.formData()/file.arrayBuffer() held the entire file in RAM, a real problem once videos get into the hundreds of MB. The existing 500MB cap is enforced via Content-Length before the stream starts, so an oversized upload is rejected without reading a single byte of the body. Verified end-to-end against a real 16MB video file: response asset metadata, on-disk file size, and MD5 checksum all match the source file exactly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mechanical port from the old Next route handlers -- same Whisper call, same Zod schemas, same Prisma calls. Verified the transcript PATCH (speaker label assignment/clearing) against a real project's transcript data. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…outes Mechanical port from the old Next route handlers. Verified against real project data: edit-operation create/delete round-trips correctly (including Zod rejecting an incomplete payload, same as before), and filler-word detection's OpenAI SDK call works correctly under Bun's runtime. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fire-and-forget kickoff (void runRenderJob(...) followed immediately by a 202 response) needs no waitUntil-style mechanism here: Bun.serve() is one long-lived process, not a request-scoped function, so the promise keeps running on the event loop after the response flushes. render-job.ts's own try/catch already swallows every error and writes status "failed", so an unhandled rejection can't take the process down. Verified end-to-end against a real project: kicked off a render, polled it to "completed", and downloaded a valid MP4 through the download route's Bun.file() response. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mounts all ten route modules under /api/projects and starts Bun.serve. All 13 of the old Next API routes now have a working equivalent here, verified end-to-end against real dev data (see the last several commits) -- this backend is a complete, framework-agnostic replacement for client/src/app/api/**, ready for the Vite client to point at next. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The 206 partial-content response already got this from Bun automatically; the plain 200 (no Range header in the request) didn't. Browsers consult Accept-Ranges to decide whether a media resource is seekable, so add it explicitly for consistency across both response shapes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…xt app Adds vite.config.mts (dev-server proxy for /api/* to the Bun backend on :3001, native tsconfig-paths resolution via Vite 8's resolve.tsconfigPaths rather than the vite-tsconfig-paths plugin), index.html, main.tsx, and a copy of globals.css with the --font-plex-sans/--font-plex-mono variables that next/font/google used to inject now defined directly (fonts load via a Google Fonts <link> tag in index.html instead -- no build-time font-fetch-and-self-host step in a plain Vite SPA, and this is a locally-run single-user tool, not a public site optimizing first-load metrics on a slow network). Also removes the unused create-next-app boilerplate SVGs from public/ (confirmed unreferenced anywhere in src/) and moves the favicon into Vite's public/ directory. client/src/app/** (the Next app) is untouched and still fully working -- this commit only adds new files alongside it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three routes (/, /new, /editor/:projectId) plus a catch-all, matching the old app/page.tsx, app/new/page.tsx, and app/editor/[projectId]/page.tsx. DashboardRoute and EditorRoute replace the two Next Server Components that queried Prisma directly at render time -- they now fetch from the Hono backend on mount instead (GET /api/projects and GET /api/projects/:id, both already verified to return the exact shape these pages need) and feed the result into the same, unchanged hydrate()-based Zustand stores EditorHydrator already used. EditorRoute also narrows the API's raw logoPosition string via toLogoPosition(), the same conversion the old Server Component did, and renders NotFoundRoute on a 404 in place of Next's notFound(). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mechanical prop rename (href -> to) and API swap (useRouter().push -> useNavigate()) across the 4 components that used them -- Dashboard, ProjectRow, EditorLayout, UploadScreen. UploadScreen's upload call also changes to match the server's new streaming upload contract (see the upload route commit): a raw file body with Content-Type: <file.type> and a ?filename= query param, instead of multipart/form-data -- a File/Blob is itself a valid streaming fetch body, so this is a small change, not a rewrite. Verified end-to-end in a real browser against the Vite dev server + Bun backend: dashboard renders real project data and thumbnails, editor loads and hydrates from the API (video playback, transcript sync, timeline thumbnails/waveform, captions panel), and navigation between all three routes works. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Removes client/src/app/** (all 13 API routes, the 3 page components, and layout.tsx) now that the Vite client + Bun backend fully replace them -- verified end-to-end in a real browser first (dashboard, editor hydration, video playback, transcript sync, navigation between all three routes). Also removes: next.config.ts; the client-side copies of libs that were only ever duplicated for the migration's transition period and are now server-only (lib/ai, lib/db, lib/ffmpeg, lib/storage, lib/validation, lib/captions/format.ts) -- their server/src/lib counterparts are the ones actually running now; client/prisma/ and prisma7.config.ts (the server owns the one Prisma schema); src/generated/prisma (server has its own); .env.example (nothing in the client needs an env var -- all AI/FFmpeg/DB access is server-only per claude.md's security requirements); and AGENTS.md/CLAUDE.md, which were Next-dev-server-generated and next-dev- specific respectively, both now stale. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- package.json: drop Next/Prisma/server-only deps (next, @prisma/*, better-sqlite3, openai, zod, eslint-config-next, prisma, dotenv, @types/better-sqlite3), scripts now run Vite (dev/build/preview) instead of next dev/build/start. - tsconfig.json: drop the Next TS plugin and .next/** type includes, add vite/client + node ambient types (the latter for vite.config.mts's own process.env usage, not leaked into app code). - eslint.config.mjs: replace eslint-config-next with a plain flat config (typescript-eslint, react-hooks, react-refresh). Deliberately keeps only rules-of-hooks + exhaustive-deps from eslint-plugin-react-hooks rather than v7's full React Compiler-oriented rule set, to match what eslint-config-next was actually enforcing before -- this migration ports the framework, it doesn't relint the whole codebase against a much stricter ruleset. Turns off react-refresh/only-export-components for src/components/ui/** (shadcn primitives legitimately co-export a component and its variant helper from one file). - Removes 4 now-dangling `eslint-disable-next-line @next/next/no-img-element` comments (ElementsPanel, FiltersPanel, Timeline, VideoPlayer) -- that rule no longer exists now that eslint-config-next is gone. - .gitignore: drop Next-specific entries (.next/, next-env.d.ts), correct the build-output ignore from /build to /dist (Vite's default). - vitest.config.mts: drop the OPENAI_API_KEY test env shim -- nothing client-side imports an OpenAI-touching module anymore. Verified clean: tsc --noEmit, eslint, vite build, and vitest all pass with zero errors/warnings. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces every "cd client && pnpm run dev" / port 3000 / Next-specific instruction with the new workspace-root pnpm run dev (runs the Vite client and Bun backend together) and port 5173. setup-mac.sh now also installs Bun (via its own official installer rather than the Homebrew formula, which fails to compile better-sqlite3 against a stale Xcode Command Line Tools version -- a real failure hit while building this migration) and writes server/.env instead of client/.env. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Section 13: two local processes now (Vite client, Bun/Hono server), not one Next.js process -- still no cloud, still local-first. Notes the @prisma/adapter-libsql driver choice (avoids better-sqlite3's native-binding packaging pain under Bun/Tauri). - Section 14: rendering diagram's "Next.js API route" -> "Hono API route (Bun)". - Section 21: drops the never-built `POST /api/projects/:id/ai/edit` line (pre-existing spec drift from section 3's already-documented removal of the free-text AI command bar), documents the upload endpoint's raw-body wire contract. - Section 28: the "desktop application" out-of-MVP line now notes that Tauri packaging itself still isn't built, but the client/server split exists specifically to make it addable later without a rewrite. - Section 31: replaces the single-app src/ tree with the actual client/ + server/ workspace layout, and states plainly that agents/lib/langgraph don't exist in either package (section 3's already-documented removal). - Top of file: `npm install && npm run dev` -> `pnpm install && pnpm run dev`, matching the pnpm workspace this project actually uses. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds server/src/lib/logger.ts -- `[HH:MM:SS] LEVEL message` -- and wires Hono's built-in request-logging middleware to print through it, so every request (method, path, status, timing) now shows up in the terminal running `pnpm run dev`. Previously the server logged almost nothing: a startup line and three scattered console.error calls (render job failure, transcription failure, filler-word detection failure), each in its own ad-hoc format. Those three now go through the same logger. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Produces a self-contained copy of the backend at repo-root server-bundle/ that runs via `bun run src/index.ts` from any working directory, not just this git checkout -- the foundation for spawning it as a Tauri-packaged app's backend process. Two non-obvious things had to be worked out empirically, both confirmed by running the bundle from a scratch directory with no relation to the git checkout (mirroring the risk-gate discipline used to verify @prisma/adapter-libsql under Bun in an earlier migration): - A plain rsync/cp of server/node_modules silently drops @libsql/client (and its native @libsql/darwin-arm64 binding): pnpm resolves a package's transitive deps through a private per-package node_modules slot living inside the workspace root's .pnpm store as a *sibling* of the requiring package, not anywhere reachable by walking server/node_modules's own tree. `pnpm deploy --prod --legacy` rebuilds a genuinely self-contained node_modules that gets this right. - Bundling via `bun build --target bun` (originally the plan) breaks that same resolution trick by flattening everything into one file at the top level -- @libsql/darwin-arm64 is left as an external runtime require, and once the requiring code's location changes, so does where Bun looks for it. So this bundle ships TypeScript source as-is (Bun runs .ts directly) and runs unbundled, keeping every file's location relative to node_modules exactly as pnpm deploy laid it out. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds static-file serving for the built Vite client (server-bundle/
client-dist in the packaged Tauri app; harmlessly skipped in plain
`bun run dev`, where no client-dist exists and Vite's own dev server
handles the client instead), so one Bun process can serve both the UI
and the API in the packaged app -- matching the production shape the
client/server split was originally built toward.
Registered after every /api/* mount, so static/SPA-fallback routes
never shadow an API request.
The SPA fallback (serving index.html for a client-side route like
/editor/abc123 that has no matching file on disk) needed root passed
alongside path, not path alone: Hono's serveStatic joins them via
path.join(root, path), and path.join("./", "/abs/path") silently
strips the leading slash off an absolute path when root is left at its
"./" default -- confirmed by writing a 3-line isolated repro before
fixing the real code.
Verified: plain dev mode still starts cleanly with no client-dist
present; with CLIENT_DIST_DIR set, index.html/static assets/SPA
fallback/API routes all return correct status codes and content types.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Confirms the Rust toolchain (rustup, just installed) and the whole build pipeline work before wiring anything app-specific: `tauri build --no-bundle` compiles cleanly (~1m12s cold) and produces a real arm64 Mach-O binary at src-tauri/target/release/app. Bundle identifier changed from the scaffold's placeholder "com.tauri.dev" (Tauri refuses to build with it -- "not allowed as it must be unique across applications") to "com.local.aivideoeditor". frontendDist/devUrl were already set correctly at `tauri init` time (http://localhost:3001 / :5173) via CLI flags -- confirms Tauri v2 does accept a URL for frontendDist, not just a static path, which the next phase's single-Bun-process design depends on. Everything else (window size, resource bundling, the actual spawn logic) is still the scaffold default -- next phase. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes a second, different resolution failure discovered while testing the actual packaged Tauri app end-to-end (the previous pnpm-deploy- based bundle worked standalone but broke once Tauri copied it into the .app): Tauri's `bundle.resources` copy step does not handle pnpm's nested per-package private node_modules slots (symlinks into its own local .pnpm virtual store) correctly -- confirmed by comparing file counts between the source bundle and the copy inside the built .app: same total file count (14793), but zero of the source's 395 symlinks survived, silently dropping entire directories (including @prisma/adapter-libsql itself) that were only reachable via one. Naively dereferencing those symlinks afterward (rsync -aL) doesn't work either: each symlink flattens independently, so @prisma/adapter-libsql's own copy loses the sibling relationship it needs to find @libsql/client once it's no longer nested inside its private resolution slot -- confirmed by a second, different "Cannot find module" failure after trying that. pnpm's "hoisted" node-linker sidesteps both problems at once: a classic/npm-style flat layout (every package, transitive deps included, sits directly at node_modules/<pkg>) with no private slots and no .pnpm store to symlink into in the first place. Verified against a real packaged .app this time, not just the standalone scratch-directory test: the app launches, its spawned server starts and serves both the API and the static client correctly, and a test project survives a full quit + relaunch cycle. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires client/src-tauri/tauri.conf.json (bundle identifier, resource mapping for server-bundle/, ad-hoc macOS signing, a hidden-until-ready main window) and src/lib.rs (resolve resource_dir/app_data_dir, seed the database from app.db.template on first launch only, spawn the bundled server via tauri-plugin-shell, forward its stdout/stderr into the app's own log, show the window once "server listening" appears, kill the child on RunEvent::Exit) together into a real packaged .app. Two bugs only surfaced once actually testing the built .app, not before: - `beforeBuildCommand`'s cwd assumption was wrong: Tauri runs hook commands from client/, not client/src-tauri/, so the original `cd ../..` overshot by one directory level and landed in the parent folder containing several unrelated sibling projects -- pnpm's --filter then matched a same-named "client" package in each of them and built all of them. Fixed by switching to the HookCommand object form with an explicit absolute cwd, removing the ambiguity entirely rather than guessing the right relative depth. (Confirmed harmless: each affected project only ran its own ordinary build script, writing to its own gitignored build output -- nothing was modified outside those directories.) - Spawning "bun" by bare name failed with ENOENT: GUI-launched macOS apps don't inherit the interactive shell's PATH (~/.zshrc is only sourced for shell sessions), even though `which bun` works fine in a terminal. Fixed with an absolute path built from $HOME, the same pragmatic choice already made for FFMPEG_PATH. Also excludes src-tauri from client/tsconfig.json: Tauri's own build script copies bundle.resources into target/debug/ even for `cargo check`, and the client's previously-unscoped "**/*.ts" include picked up the copied server-bundle/*.ts files, typechecking them against the client's own tsconfig (wrong path aliases, no Bun types) and failing the client build. Verified against the actual built and ad-hoc-signed .app, not just `cargo check`: launches cleanly (no Gatekeeper quarantine flag since it was built locally, not downloaded), the spawned server starts and correctly serves both the built static client and the /api/* routes on the same origin, a project created through the running app survives a full quit (via the standard Quit AppleEvent, which -- unlike a raw SIGTERM -- actually exercises RunEvent::Exit) and relaunch cycle with no orphaned server process left behind afterward, and the data lands under ~/Library/Application Support/com.local.aivideoeditor/ as expected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- claude.md section 28: removes "desktop application" from out-of-MVP now that a working (if personal-use-scoped) build exists. - claude.md section 35 (new): architecture rationale for the spawn-system-bun approach over a compiled sidecar (the @bun/libsql --compile bug), what scripts/prepare-server-bundle.sh's hoisted node-linker choice fixes and why, the frontendDist-as-URL design, the beforeBuildCommand cwd gotcha, and the current known constraints (personal-use-only scope, separate app-data location from dev, hardcoded machine-specific paths). - README.md: adds a short "macOS app" build pointer under "Running the app". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real bug, reported by the user after installing the built .app: it opened to a blank white screen. Root cause -- the webview starts loading frontendDist (http://localhost:3001) as soon as it's created, which races the just-spawned server's own startup and loses; nothing in the ready-handler told the webview to retry once the server actually came up, so the window's show() call just revealed the already-failed blank page. This was never caught earlier because verification up to this point used curl against the server directly -- which proves the server works but says nothing about what the webview itself rendered. Fixed by explicitly calling w.navigate() with the same URL once the "server listening" line appears, alongside the existing w.show(). Verified properly this time: launched the app fresh, waited without touching curl at all, and confirmed via the server's own request log that the webview organically requested /, its JS/CSS bundle, and /api/projects immediately after the server came up -- proof the page actually loaded and React mounted, not just that the server can answer a manual curl. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same class of bug as the earlier tsconfig fix: Tauri's build script
copies bundle.resources into target/{debug,release}/ (and generates
its own build-script output like __global-api-script.js there too),
and client's unscoped eslint config picked those up -- a minified
Tauri-internal JS file triggered 2300+ unrelated errors. tsconfig.json
already excludes src-tauri; eslint's own globalIgnores needed the same
treatment, since it's a separate exclusion list.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Prerequisite for distributing the packaged Mac app publicly: right now the app bakes the developer's real OPENAI_API_KEY into the bundle at build time, which is fine when the build never leaves this machine but unacceptable once strangers can download it -- anyone could extract the key and run up charges on the developer's account. This replaces that with a settings screen: each person who runs the app enters their own key, which the app stores locally and validates against OpenAI's real API before saving. - server/src/lib/settings.ts (new): reads/writes DATA_DIR/settings.json, the one piece of config supplied through the app itself rather than an env var. getOpenAiApiKey() checks settings.json first, falls back to server/.env's OPENAI_API_KEY -- local dev with `pnpm run dev` keeps working exactly as before, unchanged. - server/src/routes/settings.ts (new): GET /api/settings returns only whether a key is configured, never the key itself. POST validates the key with a real OpenAI call (models.list()) before persisting, so a typo'd key fails immediately and clearly instead of surfacing later as an opaque transcription error. - server/src/lib/ai/transcribe.ts, filler-words.ts: source the OpenAI client from getOpenAiApiKey() instead of process.env directly, and stop caching the client as a singleton -- a cached client would keep serving a stale (missing-key) instance after someone saves a key through the new screen without restarting the server. - client/src/routes/SetupRoute.tsx (new), client/src/App.tsx: a RequireApiKey wrapper checks /api/settings once on mount and redirects to /setup if no key is configured; SetupRoute doubles as a revisitable "replace your key" screen, not just first-run. Verified as a real end-to-end round-trip, not just unit-level: started both dev servers with OPENAI_API_KEY removed from server/.env and no settings.json present, confirmed visiting `/` in a real browser redirects to /setup, pasted a real key (via clipboard, never typed literally or printed to any log, to avoid the key passing through tool-call text), saved it, watched it get live-validated against OpenAI and redirect to the dashboard, and confirmed settings.json was written correctly -- then cleaned up the test key/file and restored the original .env afterward. Also fixed a bug found during that same verification pass: GET /api/settings originally checked only settings.json, so local dev (key only in .env, nobody's ever touched the setup screen) would have incorrectly reported hasApiKey: false and wrongly redirected working setups to /setup. Fixed to check the same getOpenAiApiKey() fallback chain everything else uses. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Prerequisite for CI: beforeBuildCommand had a cwd hardcoded to this exact machine's absolute path (/Users/aminuddin/Desktop/pluto/text2video), found during planning for the CI workflow -- would have failed outright on any GitHub Actions runner or any other machine. Tauri hook commands run from client/ (confirmed empirically while building this originally), so the correct portable form is `cd ..` (one level to repo root), not the absolute path -- the original object-form cwd was carrying a bug baked in from an earlier debugging session, not a deliberate choice. Also adds "dmg" to bundle.targets alongside the existing "app" -- Tauri builds it natively, and it's the standard drag-to-Applications experience for a downloaded Mac app, which the plain .app bundle isn't really meant for. Also removes prepare-server-bundle.sh's step that copied server/.env (including the real OPENAI_API_KEY) into the bundle -- superseded by the new settings.json-based setup screen, and confirmed nothing else needed it: every other env var the server needs is already set explicitly by lib.rs's spawn call, which overrides a bundled .env anyway. Applies to every build now, not just CI -- one code path, no personal-build-vs-distributed-build branching. Verified with the actual command CI will run (`pnpm exec tauri build`), not just reasoning about the diff: confirms "Done: .../text2video/server-bundle" (the correct repo root, not the parent directory containing unrelated sibling projects the original bug would have reached), produces both a .app and a .dmg, and no .env ships inside Contents/Resources/server-bundle/. Then wiped ~/Library/Application Support/com.local.aivideoeditor and launched the built .app fresh: confirmed via its own server log that the webview loaded, checked /api/settings, got hasApiKey: false, and never went on to fetch /api/projects -- proof it actually redirected to /setup and stayed there, not just that the server responds. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The workflow was stale since before the Vite/Bun migration -- scoped entirely to client/ (working-directory: client), ran `prisma generate` there even though the Prisma schema now lives in server/, and never linted, tested, or built server/ at all. Root package.json's lint/test/build scripts already correctly fan out to both packages via pnpm --filter; this just points CI at those instead of re-implementing per-package steps by hand. Also adds oven-sh/setup-bun -- server's own build script (`bun build src/index.ts ...`) needs the bun binary on PATH, which the old workflow never needed since it only ever touched client/. Verified by literally replicating the workflow's command sequence locally (pnpm install --frozen-lockfile, prisma generate in server/, pnpm run lint/test/build from root, with the same dummy OPENAI_API_KEY) -- confirms all 249 tests pass and both packages build cleanly, not just that the YAML parses. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New .github/workflows/build-macos-app.yml, runs on macos-latest (Apple Silicon by default on current GitHub-hosted runners, matching this project's arm64-only scope -- no cross-compilation flags needed). Triggers on a v*.*.* tag push (matching the existing v0.1.0 tag/ release convention already in this repo) or manual workflow_dispatch. The actual build is a single `pnpm exec tauri build` -- tauri.conf.json's beforeBuildCommand already runs the Vite client build and scripts/prepare-server-bundle.sh internally, so the workflow itself only needs to set up tools (pnpm, node, bun, rust), install deps, and run that one command. No Apple signing secrets needed -- ad-hoc signing (this project's current scope) needs no certificate or notarization credentials. Every run (including a manual workflow_dispatch) uploads the built .dmg and a zipped .app as workflow artifacts, so a build can be downloaded and verified from a run's own Actions page. Only a real tag-push run additionally attaches those same files to the matching GitHub Release -- a workflow_dispatch test run never touches the public releases page. Verified everything short of an actual GitHub Actions run (this branch hasn't been pushed to origin yet, so there's nothing to trigger remotely): replicated the same `pnpm exec tauri build` this workflow runs, and specifically tested the one new step that isn't already covered by that -- `ditto -c -k --sequesterRsrc --keepParent` zipping the built .app -- confirming the zip extracts back to a working app bundle with its ad-hoc code signature intact. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Now that the app is CI-built and released with assets attached (ad-hoc signed, arm64-only, API key entered on first launch — no key baked into the build), it needs somewhere for people to actually find and download it from. Adds docs/download.md covering the Gatekeeper right-click-Open step and the Bun/ffmpeg-full prerequisites plainly, links it from the docs nav and homepage hero, and updates README's stale "personal use only, not for distributing" language to match. Also brings claude.md section 35 in sync with the actual current architecture (CI build/release path, settings.json-based API key instead of a bundled .env, corrected beforeBuildCommand description). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…plit getting-started.md, status.md, and security.md still described the pre-migration architecture (cd client && pnpm run dev, port 3000, "server/ reserved for future use, not in use yet", API routes living in Next.js) — actively wrong now, and getting-started is the primary Guide entry point a new visitor lands on. Brings all four pages (also architecture.md, which didn't mention the process split at all) up to date with the actual Vite/Bun split and current commands, and links the new download page where relevant. Verified by serving the docs site locally and confirming each changed page returns 200. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related pieces of work, staged as 38 atomic commits:
client/) talking to aseparate Bun + Hono backend (
server/) —pnpm run devruns bothtogether from the repo root. No server-side rendering, one
deployable backend unit; this split is what makes everything below
possible.
Silicon only — see
claude.mdsection 35 for the full rationale andconstraints), spawning the system-installed
bunto run a stagedcopy of the backend rather than a compiled sidecar (works around an
upstream
bun build --compile+@libsqlbug).ci.ymlfixed to actually cover both packages (it wasstale,
client/-only, from before the split); newbuild-macos-app.ymlbuilds and releases the app onv*.*.*tags(and via manual
workflow_dispatchfor testing).developer's real
OPENAI_API_KEYinto the bundle — each person whoruns it (built locally or downloaded) enters their own key on first
launch, stored in
DATA_DIR/settings.json. Local dev still fallsback to
server/.envunchanged.fixing several docs pages (
getting-started,status,security,architecture) that still described the old Next.js setup.Test plan
pnpm run lint— cleanpnpm run test— 249 tests pass (100 client + 149 server)pnpm run build— client + server both buildpnpm exec tauri buildrun: confirms portablebeforeBuildCommand, no baked-in.env/API key, both.appand
.dmgproduced, fresh app redirects to/setupredirected to
/setup→ paste a real key → live-validatedagainst OpenAI → saved → redirected to dashboard → real
transcription works
.env, media/DB files, ornode_modules/data/inthe diff (checked by eye, not just
.gitignore)workflow_dispatchor a tag push (branch was only just pushed) — recommend running
build-macos-app.ymlmanually once, before ever tagging arelease, to confirm the macOS runner build succeeds end-to-end
🤖 Generated with Claude Code