zudo-tauri-wisdom

Type to search...

to open search from anywhere

App Generation System

CreatedApr 3, 2026Takeshi Takatsudo

A config-driven generator that produces different Tauri app instances from the same codebase

App Generation System

The zudo-text repository is not a single app — it is an app generator. The same Rust backend and React frontend produce different app instances depending on configuration. Each instance has its own name, bundle identifier, workspace directory, and settings.

The Concept

An “app instance” is defined by three things:

  1. Tauri config overridetauri.conf.<name>.json overrides productName and identifier
  2. Runtime config~/.config/zudotext/<name>/config.json points to the workspace directory
  3. Workspace — A directory containing drafts, archives, pins, and .zudotext.settings.json

The generator creates all three with a single command:

pnpm generate <app-name> <workspace-path> [options]

Usage

# Generate configs only (no build)
pnpm generate ztoffice ~/Documents/office-notes

# Generate + build the .app bundle
pnpm generate ztoffice ~/Documents/office-notes --build

# Generate + build + install to /Applications
pnpm generate ztoffice ~/Documents/office-notes --install

# Skip workspace scaffolding (directory already exists)
pnpm generate ztoffice ~/Documents/office-notes --skip-scaffold --install

# Use a specific scaffold preset
pnpm generate ztoffice ~/Documents/office-notes --preset full

What the Generator Does

Step 1: Scaffold the Workspace

Unless --skip-scaffold is used, the generator creates a workspace directory with the required structure:

~/Documents/office-notes/
  .zudotext.settings.json    # App settings (validated defaults + overrides)
  inbox/                     # Draft files (draft1.md, draft2.md, ...)
  assets/                    # Image and file assets
  pins/                      # Default pin directory
  archives/                  # Archived messages (optional, based on preset)

The scaffold uses presets (minimal, standard, full) that determine which directories and template files are created:

const standard: PresetConfig = {
  templateDir: path.join(presetsDir, "standard", "template"),
  pins: [
    { path: ".claude/skills", title: "Skills" },
  ],
};

const full: PresetConfig = {
  templateDir: path.join(presetsDir, "full", "template"),
  pins: [
    { path: ".claude/skills", title: "Skills" },
    { path: "pins/notes", title: "Notes" },
  ],
};

The settings file is generated by merging the preset defaults with validateSettings() to ensure all required fields are present:

const raw = {
  ...defaultSettings,
  ...userSettings,
  general: {
    ...defaultSettings.general,
    ...(userSettings.general ?? {}),
    projectRoot: targetDir,
  },
  pins: pinConfigs.length > 0 ? pinConfigs : defaultSettings.pins,
};
const mergedSettings = validateSettings(raw) ?? defaultSettings;

Step 2: Create Tauri Config Override

The generator creates a minimal JSON file that overrides only what differs:

const tauriConf = {
  productName: appName,
  identifier: `com.takazudo.${appName}`,
};
fs.writeFileSync(tauriConfPath, JSON.stringify(tauriConf, null, 2) + "\n");

This produces tauri-app/tauri.conf.ztoffice.json:

{
  "productName": "ztoffice",
  "identifier": "com.takazudo.ztoffice"
}

Tauri’s --config flag merges this on top of the base tauri.conf.json at build time.

Step 3: Create Runtime Config

The runtime config tells the app where to find its workspace:

const configDir = path.join(os.homedir(), ".config", "zudotext", appName);
const appConfig = { workspace: workspacePath };
fs.writeFileSync(configPath, JSON.stringify(appConfig, null, 2) + "\n");

This creates ~/.config/zudotext/ztoffice/config.json:

{
  "workspace": "/Users/you/Documents/office-notes"
}

At runtime, the Rust backend reads this config to determine which workspace to serve.

Step 4: Build (Optional)

If --build or --install is specified:

execFileSync(
  "cargo",
  ["tauri", "build", "--config", `tauri.conf.${appName}.json`],
  { cwd: tauriDir, stdio: "inherit" },
);

Step 5: Install (Optional)

If --install is specified, the built .app is copied to /Applications:

const builtApp = path.join(tauriDir, "target/release/bundle/macos", `${appName}.app`);
const dest = path.join("/Applications", `${appName}.app`);

if (fs.existsSync(dest)) {
  const ok = await confirm(`${dest} already exists. Replace it?`);
  if (ok) fs.rmSync(dest, { recursive: true });
}
fs.cpSync(builtApp, dest, { recursive: true });

⚠️ Warning

The generator prompts for confirmation before replacing an existing .app in /Applications. This prevents accidentally overwriting a running app.

App Name Validation

App names must be lowercase alphanumeric with optional hyphens:

function validateAppName(name: string): boolean {
  return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(name);
}

Valid: ztoffice, zt-notes, myapp123 Invalid: ZTOffice, zt_notes, my app

Real-World Example

The repository defines build scripts for actual app instances:

{
  "scripts": {
    "build:ztoffice": "pnpm generate ztoffice ~/Library/CloudStorage/Dropbox/ainotes/office --skip-scaffold --install",
    "build:ztprompts": "pnpm generate ztprompts ~/Library/CloudStorage/Dropbox/ainotes/prompts --skip-scaffold --install"
  }
}

Each runs in about 2-3 minutes (Rust compilation + signing). The result is a standalone macOS .app (~5MB) that opens to its own workspace.

The @takazudo/app-scaffold Package

The scaffold logic lives in a separate package (packages/app-scaffold/) that can be used programmatically or via CLI:

import { scaffoldApp, getPreset } from "@takazudo/app-scaffold";

const presetConfig = getPreset("standard");
const result = scaffoldApp({
  targetDir: "/path/to/workspace",
  ...presetConfig,
});

// result.projectRoot -> absolute path to created workspace
// result.cleanup() -> remove the workspace (for test teardown)

The cleanup() method is used in tests to tear down scaffolded workspaces.

Key Takeaway

The app generation system demonstrates a powerful pattern: one codebase, many apps. By separating the configuration (names, identifiers, workspace paths) from the code (Rust backend, React frontend, shared packages), new app instances can be created in seconds and built in minutes. The scaffold, config overlay, and runtime config work together to produce fully independent .app bundles from a single repository.