Skip to main content

Development

What you need installed, what language each part is in, and how to run it.

Read this after KICKOFF.md, which covers installing and using the pipeline. This is about changing it.


The short version​

git clone https://github.com/RoleModel/rolemodel-openscreen.git
cd rolemodel-openscreen
npm install # playwright and playwright-recast; nothing else
npm run dev # the Studio on :4600, reloading on change
npm run check # 390 assertions

That is the whole loop for most work. No code is compiled — the toolkit is plain ESM JavaScript, the Studio is plain DOM, and lib/studio.js is served to the browser as-is. Save a file, the page reloads.

npm run build exists but builds assets, not code: it syncs the brand tokens, regenerates the Optics CSS and re-renders the wallpapers. You need it after changing brand/, and never for a change to lib/ or bin/.

The other two repositories are only needed to change the app or the review instance: npm run forks fetches them.


Four languages, and which parts are in which​

partlanguagewhy it is that
the toolkit (rolemodel-openscreen)plain ESM JavaScript, Node ≥20No compile step means no build to debug, and the Studio's client is the same language as its server. lib/wallpaper.mjs runs in the browser preview, the Studio export and the batch renderer — one implementation, three callers.
the app's UI (openscreen)TypeScript + React 18, Vite 7, Tailwind 3Upstream's choice. 335 files.
the app's main processTypeScriptUpstream's choice. Where our Studio window and open verb live.
the compositorRustUpstream's choice. Frame compositing and the ffmpeg bindings.
the capture helperSwift (ScreenCaptureKit)The only API that captures a window on modern macOS. The one thing that needs full Xcode, which is why the app is built in CI.
review (openframe)TypeScript, Next.js 16, Prisma 7, PostgresUpstream's choice. Built with bun, not npm.

The split matters when you are deciding where a change goes. Anything about brand, narration, demo scripting or the Studio is JavaScript in the toolkit, with no compile step. Anything about recording, the timeline or export is TypeScript or Rust in the fork, with one.


Tools​

Node≥20 in the toolkit; the fork pins 22.22.1 in engines. Node 26 works but warns.
BiomeFormatting and linting in both the toolkit and the fork. npx biome check .
PlaywrightNot just for tests — it renders the wallpapers, drives demo scripts, and records them. It is a runtime dependency here, not a dev one.
DockerOnly for the review instance.
Rust + full XcodeOnly to build the app from source. CI has both.

Testing​

The toolkit has no test framework. lib/verify.mjs is a single file of 390 assertions that runs in about two seconds:

npm run check # brand sync, Optics pin, the assertions, tap drift
npm run verify # just the assertions

It is deliberately not a unit-test suite. Most of what breaks here is a contract between two things — a formula that promises ten commands, a client that builds a path the server resolves differently, a cask whose checksums no longer match its release — and those are the assertions worth having. Several exist because something shipped broken:

  • three lists of CLI names that had drifted apart
  • a client building <library>/<id>/<rel> for a file under media/
  • a file:// URL handed to a compositor that opens paths
  • a poster frame seeking into an auto-zoom

Write the assertion against the broken code first. An assertion that has never failed has not been tested, and several of mine passed vacuously until I checked. git stash the fix, run it, watch it fail, restore.

The forks use vitest (npm test) and have their own suites — 172 pass across the areas we touched in the openscreen fork. Upstream has ~40 pre-existing failures unrelated to us; run the suite against upstream/main before assuming you caused one, which is two minutes and has already saved me from reporting someone else's bug as mine.


Running each piece​

The Studio, on its own​

npm run dev # :4600, reloads on change

--watch never opens a browser: the tab you have reloads itself, and opening a new window on every save is how you end up with forty of them.

The Studio inside the app​

cd ../openscreen && npm run app

This is the real configuration — the Studio is a window in the app, so opening a document in the editor is an IPC call rather than a shell-out. npm run app handles the environment for you; see scripts/launch.mjs for what each piece of it is for, because every one is load-bearing.

The app's renderer, hot-reloading​

cd ../openscreen && npm run dev

Vite with HMR, for working on the editor itself.

The review instance​

cd ../openframe && docker compose up -d --build

App, Postgres and MinIO. Configuration is .env.docker, which is not committed.


Things that will bite you​

These are all environment, and all of them cost someone an hour already.

symptomcause
npm install installs no dev dependenciesNODE_ENV=production in your shell. npm omits them silently.
Electron dies on Cannot find module …/recordELECTRON_RUN_AS_NODE leaked from an editor terminal. The toolkit strips it for children; a shell you ran it in yourself does not.
Electron is missing after a clean installnpm 11 blocks install scripts by default. npm install-scripts approve electron
cargo not found with Rust installedThe build script looked only in rustup's ~/.cargo/bin. Fixed in our fork; it checks $CARGO, rustup, then PATH.
A spawn works in your terminal and fails from the appA GUI app launched from Finder does not inherit your shell's PATH, so /opt/homebrew/bin is not on it.
The first release build failswhisper-stt artifacts expire, and the macOS job refuses to package without them. Run build-whisper-stt.yml first.

Working on a fork without making rebasing expensive​

Our diff on openscreen is 7 commits, 29 files, 895 lines — on top of 2260 upstream commits. On openframe it is 9 files and 185 lines. Both are small on purpose, and keeping them small is the whole reason git pull upstream main stays a non-event. Check yours before you push:

npm run forks # also reports how far each is from upstream

The pattern, everywhere we have touched their code:

  • One new file carrying the logic (electron/studio/server.ts, lib/api-token.ts), so upstream never conflicts with it.
  • One line changed per call site. await auth() became await authFromRequest(request) in eleven places and nothing else moved.
  • Never reformat. A formatter run across a file you changed three lines of turns a clean rebase into a manual one.

When you need something from their code, check whether they already solved it. The Studio window's drag region is injected the same way they inject --titlebar-inset-left; the release tag validator was widened rather than worked around. Both are cheaper to rebase than an invention would have been.


Where the seams are​

Each of these runs on its own, with no Studio and no app. If you want to lift one piece and leave the rest, start here.

moduledoesneeds
lib/wallpaper.mjsrecipe → canvasa canvas (browser or node)
lib/demo-script.mjsparse a demo scriptnothing
lib/demo-record.mjscapture a demo by doing itplaywright
lib/openframe.mjssend a video for reviewfetch, a token
lib/narration.mjslines → audio + an exact SRTa TTS provider
lib/theme.mjsapply a brand preset to a documentnothing
lib/script-parse.mjsmarkdown → speakable linesnothing

lib/verify.mjs asserts a lot of the behaviour above, so it doubles as the specification for anything you reimplement.