Generator
The generator commands power the LEAF-app assembly pipeline: they assemble no-compile stub bundles and bind them to a cloud workspace. See App Generation for the full pipeline overview.
ROOT-only commands: generator_assemble_child and generator_find_leaf_for_workspace operate on installed LEAF bundles and are only meaningful when the binary is running as ROOT. generator_assemble_child rejects with an app-mode error from a LEAF.
What a generated instance points at
A generated app instance is bound to a cloud workspace, not to a local workspace directory (epic #4204 D2/D12, #4227). Two commands and one type disappeared with the workspace concept:
generator_check_pathand itsPathClassification(Empty / ExistsNonempty / InsideGitRepo / InsideSyncedFolder) — path safety had no referent once instances stopped pointing at a folder, so the whole classification module was deleted.generator_write_configs—config.jsonis a workspace binding now, and D2's bind-only-after-unlock rule means only the onboarding flow may write the current instance's own binding. Use theapp_binding_*commands instead.
generator_assemble_child is the one exception to bind-only-after-unlock: minting a brand-new LEAF is itself the moment a workspace is attached to that not-yet-running instance, so it writes the new app's config.json directly.
Data Structures
GeneratorRunResult
Returned by generator_scaffold — a build-log-style transcript for the UI.
interface GeneratorRunResult {
stdout: string;
}AssembleResult
Returned by generator_assemble_child on success:
interface AssembleResult {
appPath: string; // Absolute path to the installed .app bundle
}There is no workspacePath — nothing on disk is created for the new app beyond the bundle and its config.
AssembleError
Rejected by generator_assemble_child. Discriminated by kind:
type AssembleError =
| { kind: 'app-mode'; message: string } // Called from a LEAF binary
| { kind: 'validation'; message: string } // Bad app name, or blank workspaceId
| { kind: 'conflict'; message: string } // App exists, replace not set
| { kind: 'io'; message: string }; // Filesystem / signing / config-write errorA clone or codesign failure leaves no partial bundle — staging is cleaned up before the rejection. A config-write failure is different: the bundle is already installed, so it stays, unbound. See the partial-failure contract below.
AssembleProgress
Emitted as generator:assemble-progress events during generator_assemble_child:
interface AssembleProgress {
step: string; // stable step id
message: string; // human-readable line
}Step ids in order: stamp-stub, locate-core, icon (only when an icon was supplied), sign, quarantine, swap, config (only when a workspaceId was supplied). The scaffold step is gone — there is no local workspace to scaffold — and config now means "wrote the new app's workspace binding", not "wrote .zudotext.settings.json".
LeafAppInfo
Returned by generator_find_leaf_for_workspace:
interface LeafAppInfo {
appName: string; // Bundle stem (e.g. "modmsg")
appPath: string; // Absolute path to the installed LEAF .app bundle
}Commands
generator_scaffold
Scaffold a standalone local workspace directory, seeded with .zudotext.settings.json.
This command is a leftover of the pre-pivot model and has no caller in the end-user flow. In particular it is not what pnpm generate runs: that CLI (scripts/) only binds a workspace — --workspace-id, --genesis, --mock, or unbound — and never creates a local workspace. Do not reach for this command when adding to the generation flow.
const result = await invoke<GeneratorRunResult>('generator_scaffold', {
appName: 'myapp',
workspacePath: '/Users/me/notes/myapp',
preset: 'standard',
settings: { /* AppSettings overrides */ },
force: false,
});| Name | Type | Description |
|---|---|---|
app_name | string | App identifier; validated as lowercase alphanumeric with hyphens |
workspace_path | string | Absolute path to scaffold into; a leading ~ is expanded |
preset | string | null | minimal, standard (default), or full |
settings | object | null | Optional AppSettings overrides, run through the write sanitizer |
force | boolean | Overwrite existing files if true |
Returns: Result<GeneratorRunResult, string>. Skips (rather than failing) when the target already has a .zudotext.settings.json.
generator_assemble_child
Assemble and install a no-compile LEAF stub bundle, optionally binding it to a workspace. ROOT-only.
const result = await invoke<AssembleResult>('generator_assemble_child', {
appName: 'myapp',
workspaceId: 'workspace-abc123', // omit to generate unbound
displayName: 'My App',
iconPath: null,
replace: false,
newWorkspaceIntent: false, // #4524, B1 — see below
});| Name | Type | Description |
|---|---|---|
app_name | string | Bundle stem for the new LEAF (e.g. "myapp") |
workspace_id | string | null | Existing workspace to bind the new app to; omit to generate unbound |
display_name | string | null | CFBundleDisplayName; falls back to app_name |
icon_path | string | null | Absolute path to a custom .icns; uses the default if null |
replace | boolean | Replace an existing LEAF bundle at the same path |
new_workspace_intent | boolean | null | Mint with explicit "create a new workspace" intent (#4524, B1). Mutually exclusive with workspace_id — supplying both is a validation error. |
workspace_id must reference an existing workspace the signed-in account owns (obtained via listAccountWorkspaces). This command never runs genesis: minting a new workspace from inside an already-booted ROOT would re-arm the global cloud-sync singleton and clobber ROOT's own live workspace session. Omitting workspace_id (with new_workspace_intent also unset) defers workspace selection with no particular intent to the fresh LEAF's own first-launch onboarding, which runs that flow safely in a brand-new process.
new_workspace_intent (#4524, B1). A bare unbound LEAF (workspace_id omitted, new_workspace_intent unset) is not safe for "I explicitly asked for a brand-new workspace": the boot flow's single-workspace shortcut (workspace-boot.ts) auto-attaches an unbound instance to the account's workspace whenever it owns exactly one — correct for "I forgot to bind this instance," wrong here, since it would silently reattach the fresh LEAF to an existing workspace instead of honoring the choice that was just made (#4517). Setting new_workspace_intent: true makes step 7 below write a {"newWorkspaceIntent": true} marker instead of a plain absent config, so the fresh LEAF's own boot flow recognizes the intent and always offers genesis/the picker, regardless of how many workspaces the account owns. Genesis (minting a brand-new workspace) still only ever runs in the LEAF's own process, on its own first-launch onboarding — never here.
Pipeline steps (in order):
stamp-stub— clone the prebuilt Mach-O stub skeleton from ROOT's resources and transform the templateInfo.plistlocate-core— verify ROOT's shared dylib exists, then writeContents/pointing at itResources/ core- path icon— copy the custom icon if providedsign— ad-hoc sign withcodesign --force --sign -quarantine— strip the quarantine extended attribute (best-effort: thexattrexit status is ignored, since removing an absent attribute already errors on some macOS versions, so a failure here never aborts the install)swap— atomically install to~/Applications/<appName>.appconfig— whenworkspace_idis set, write the new app's~/(schema v2); when. config/ zudotext/ <appName>/ config. json new_workspace_intentis set instead, write its{"newWorkspaceIntent": true}marker; otherwise this step is skipped
Ordering is load-bearing. Every bundle mutation happens before signing; signing is the last write before the quarantine strip.
Partial-failure contract: the aborting steps are 1–4 and 6 — a failure in any of them is cleaned up and no .app is left behind. Step 5 is best-effort and cannot abort. Step 7 runs after the bundle is installed; a failure there still rejects with io, but does not roll back the install. Any stale binding for that app name is cleared before the swap, so a failed config write leaves the app with no config.json at all, which resolves as Unbound(Missing) — the LEAF boots to onboarding. It never leaves a bundle pointing at the wrong workspace.
Events: emits generator:assemble-progress after each step.
generator_find_leaf_for_workspace
Reverse lookup: given a workspace id, find the installed LEAF bound to it. ROOT-only by convention.
const leaf = await invoke<LeafAppInfo | null>('generator_find_leaf_for_workspace', {
workspaceId: 'workspace-abc123',
});| Name | Type | Description |
|---|---|---|
workspace_id | string | Workspace id to look up |
Returns: LeafAppInfo if a matching LEAF is found, null otherwise.
Behavior: Scans ~/ (schema v2) and accepts a candidate only when all three hold: the binding is Bound and matches workspace_id; ~/Applications/<appName>.app is installed; and its core-path sidecar points at this ROOT's shared core (so LEAFs belonging to a different ROOT are excluded).
Returns null rather than erroring in every degenerate case — no match, a dev build with no .app ancestor, or any IO error. Used by ROOT's toolbar to choose between "Generate leaf app" and "Open leaf app".
generator_open_app
Launch an installed LEAF app bundle via the OS opener.
await invoke('generator_open_app', { appPath: '/Users/me/Applications/myapp.app' });| Name | Type | Description |
|---|---|---|
app_path | string | Absolute path to the .app bundle |
Returns: Result<(), string>.
Behavior: Before launching, the path is canonicalized and verified to resolve under ~/Applications/ and end with .app — defense against .. and symlink tricks. Paths failing that contract are rejected with a plain error string the renderer surfaces as a non-blocking toast. On success, delegates to the system open command; the LEAF launches as a separate process and this returns immediately.
Events
generator:assemble-progress
Emitted by generator_assemble_child after each assembly step.
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen<AssembleProgress>('generator:assemble-progress', (event) => {
console.log(event.payload.step, event.payload.message);
});Bridge Facade (bridge.generator)
// Scaffold a local workspace (developer CLI path)
const result = await bridge.generator.scaffold(appName, workspacePath, preset, settings, force);
// Assemble and install a LEAF (ROOT only)
const assembled = await bridge.generator.assembleChild({
appName, workspaceId, displayName, iconPath, replace,
});
// Listen to assembly progress
const unlisten = await bridge.generator.onAssembleProgress((progress) => { /* … */ });
// Find the LEAF bound to a workspace (ROOT only)
const leaf = await bridge.generator.findLeafForWorkspace(workspaceId);
// Launch a LEAF
await bridge.generator.openApp(appPath);Adapter coverage: TauriAdapter implements all of these against the real Rust commands (assembly is macOS-only). MockAdapter simulates assembly progress and success, and returns null from findLeafForWorkspace unless a test overrides it. RestAdapter rejects them as unsupported — generation is a desktop-only capability.