Skip to main content

Configuration and machine splits

Composition

Every fragment may be an object, a factory receiving Context, or a nested array. false, null, and undefined fragments and resource entries are ignored. Factories run synchronously. Imports are ordinary TypeScript modules.

import { defineConfig, darwin, linux, machine, tools } from "@dovocode/workstation";

const common = defineConfig({ resources: [tools.mise({ node: "lts" })] });
const macos = defineConfig({ resources: [tools.brewCask(["ghostty"])] });

export default defineConfig([
common,
darwin(macos),
linux({ resources: [tools.apt(["git", "jq"])] }),
machine(["studio", "macbook"], { resources: [tools.mise(["shfmt"])] }),
]);

Move those fragments into modules and import them from your entry point as the setup grows. configure(factory) provides a typed factory helper. when(boolean, resources) conditionally includes resources, whereas darwin, linux, and machine select configuration fragments.

Context and paths

FieldMeaning
machineOS hostname before the first dot, or exact --machine override
hostnameFull OS hostname
platformdarwin or linux from the executing machine
homeAbsolute home directory of the executing user
configDirAbsolute directory of the entry-point file

Machine selection does not use the network SSID, serial number, or macOS display name. --machine changes the selector, not the OS or home directory.

Relative source paths (symlinks, custom-tool sources, service programs) resolve against configDir. Relative file targets resolve against home. ~ expands to home and absolute paths remain absolute. Imported fragments share the entry point's base directory, not the importing file's directory.

Overrides

Declarations are processed in order. The last declaration with an identical resource ID wins; objects are not deep-merged. File-like resources use the resolved target path as their ID. Packages use manager and name; services use their label or scope and unit name.

import { defineConfig, files } from "@dovocode/workstation";

export default defineConfig([
{ resources: [files.json("~/.config/app.json", { theme: "light" })] },
{ resources: [files.json("~/.config/app.json", { theme: "dark" })] },
]);

You can also use machines: { studio: [...] } inside a fragment. Shared resources come before that fragment's machine resources. Later manager and stateFile settings override earlier settings.

import { defineConfig, tools } from "@dovocode/workstation";

export default defineConfig({
managers: { darwin: "brew", linux: "apt" },
resources: [tools.system(["git", "jq"])],
stateFile: "~/.local/state/workstation/state.json",
});

Keep the state path stable: it contains ownership and original-file backups. Omit managers.linux to detect APT, DNF, YUM, or pacman from PATH (in that order). Use linux: "dnf", linux: "yum", or linux: "pacman" to select explicitly. Flatpak is an additional app backend, not a system-manager detection candidate. Use linux(...) for Flatpak/pacman declarations and darwin(...) for MAS. The state belongs to one machine selector and rejects a different selector.

Commands after reconciliation

afterApply accepts an array of commands declared with task(...). They run in order after successful build and upgrade commands, including no-change runs. Imported fragments append hooks rather than replacing them. See post-apply scripts for execution and failure behavior.

Dependencies and resource IDs

Resources accept dependsOn to name prerequisites. Dependencies affect apply ordering and keep dependent packages out of the same native batch. Generated services and checked provision operations react when dependencies change. Unknown IDs and cycles fail before build mutation.

import { configure, defineConfig, files, systemdService, linux } from "@dovocode/workstation";

export default defineConfig(linux(configure(({ home }) => ({ resources: [
files.json("~/.config/example/settings.json", { port: 3000 }),
systemdService("example", {
program: "/usr/local/bin/example",
args: ["--config", `${home}/.config/example/settings.json`],
dependsOn: [`file:${home}/.config/example/settings.json`],
}),
] }))));

This template assumes the example executable is installed. Package and file IDs can be read from a plan, status, or resourceId on resolved resources.

ResourceResolved ID example
mise toolpackage:mise:node
Scoped npm tool via misepackage:mise:npm:@example/cli
Homebrew formula/caskpackage:brew:jq / package:brew-cask:ghostty
Flatpakpackage:flatpak:user:org.mozilla.firefox:stable
Mac App Store apppackage:mas:497799835
Generated file, symlink, custom tool, provision copyfile:/absolute/target/path
LaunchAgentlaunch-agent:dev.example.worker
systemd unitsystemd-service:user:example.service
Other provision operationprovision:vendor-repository

tools.system resolves to the actual backend before identity is calculated. Flatpak remote is declaration metadata, while scope and branch are part of the ID. Copy provisions use the destination ID, not provision:<name>.

Stable identities for new setups

import { defineConfig } from "@dovocode/workstation";

export default defineConfig({
id: "personal-workstation",
resources: [],
});

An id accepts letters, numbers, underscores, dots, and hyphens and replaces the entry-point path as the input to default state namespacing. Machine selection still separates state. An explicit stateFile takes precedence. Adding or changing an ID on an existing setup changes its default state location; preserve the old state explicitly before moving a managed configuration. See migration.