Desktop and CLI

The desktop app holds no business logic. Every operation is a Tauri command that shells out to the bundled mochi binary and parses its JSON output. Same binary, two surfaces.

A few details that turned out to matter more than expected:

It goes through a shell. A GUI app on macOS doesn’t inherit a login shell’s environment, so invocations source the user’s profile first. Arguments are POSIX-quoted, which matters because executable references contain a space. validate flow/ns:name has to survive the round trip as one token instead of being split into two.

Runs are tagged with their origin. Every spawned process gets FLOW_RUN_SOURCE=desktop. Without it, a run started by clicking Execute is indistinguishable from one typed into a terminal. Both would record cli, because that’s what Flow assumes when nothing says otherwise. The desktop is its own origin, and history should say so.

Errors have a fallback path. The JSON envelope may appear on any line of stderr, so parsing scans lines in reverse. If there’s no envelope, the message is recovered from stderr with ANSI codes and log prefixes stripped. Failing that, it comes from stdout, because Flow’s logger writes there and a command that fails by logging leaves stderr empty. Without that last fallback the user gets shown the arguments of the failed command and nothing about why it failed.

Secrets reach providers through the environment, never argv. Arguments are visible to every process on the machine; a child process’s environment is not.

There’s also a small trick for version compatibility: mochi desktop api check-version exists only in Mochi, so the desktop app uses its absence to detect that it’s running against open-source Flow instead.

Type Safety Across Three Languages

Mochi is Go, TypeScript, and Rust in one repo. JSON Schemas are the contract, vendored from Flow, which owns them. TypeScript types and the raw schema modules are generated for the frontend; Rust types are generated for the Tauri backend; Go gets the same types by importing Flow as a library rather than by generating them.

That’s a better arrangement than it sounds like: the schema is what keeps the Rust and TypeScript mirrors honest against the Go types they’re shadowing. Generation is orchestrated by Flow executables (of course), and CI fails if generated code is out of date.

Docs and code generation