zudo-text

検索したい単語を入力

いつでも検索バーを開ける

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_path and its PathClassification (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_configsconfig.json is 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 the app_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 error

A 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/generate-app.ts) 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,
});
NameTypeDescription
app_namestringApp identifier; validated as lowercase alphanumeric with hyphens
workspace_pathstringAbsolute path to scaffold into; a leading ~ is expanded
presetstring | nullminimal, standard (default), or full
settingsobject | nullOptional AppSettings overrides, run through the write sanitizer
forcebooleanOverwrite 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
});
NameTypeDescription
app_namestringBundle stem for the new LEAF (e.g. "myapp")
workspace_idstring | nullExisting workspace to bind the new app to; omit to generate unbound
display_namestring | nullCFBundleDisplayName; falls back to app_name
icon_pathstring | nullAbsolute path to a custom .icns; uses the default if null
replacebooleanReplace an existing LEAF bundle at the same path
new_workspace_intentboolean | nullMint 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):

  1. stamp-stub — clone the prebuilt Mach-O stub skeleton from ROOT's resources and transform the template Info.plist

  2. locate-core — verify ROOT's shared dylib exists, then write Contents/Resources/core-path pointing at it

  3. icon — copy the custom icon if provided

  4. sign — ad-hoc sign with codesign --force --sign -

  5. quarantine — strip the quarantine extended attribute (best-effort: the xattr exit status is ignored, since removing an absent attribute already errors on some macOS versions, so a failure here never aborts the install)

  6. swap — atomically install to ~/Applications/<appName>.app

  7. config — when workspace_id is set, write the new app's ~/.config/zudotext/<appName>/config.json (schema v2); when new_workspace_intent is 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',
});
NameTypeDescription
workspace_idstringWorkspace id to look up

Returns: LeafAppInfo if a matching LEAF is found, null otherwise.

Behavior: Scans ~/.config/zudotext/*/config.json (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' });
NameTypeDescription
app_pathstringAbsolute 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.