Skip to main content

Embed Workstation in TypeScript

Import the public API from @dovocode/workstation; internal source modules are not a supported integration surface. The JavaScript package requires its declared Node.js version even when the standalone CLI needs no external Node installation.

Use a configuration file

import { createWorkstation } from "@dovocode/workstation";
import { fileURLToPath } from "node:url";

const client = createWorkstation({
configPath: fileURLToPath(new URL("./workstation.config.ts", import.meta.url)),
onAction: action => console.log(action.type, action.id, action.reason),
onProgress: message => console.log(message),
});

const actions = await client.plan();
console.log(actions);
// Apply when your application's workflow is ready:
await client.build({ frozen: true, noRemove: true });

Planning requires existing package managers and does not persist newly resolved pins. A frozen build needs an existing matching lock; create it with updateLock() or an initial non-frozen build before using this combination. Methods evaluate fresh definitions and throw errors instead of exiting your host process.

Supply an inline definition

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

const client = createWorkstation({
configPath: "/absolute/path/setup/workstation.config.ts",
config: {
resources: [files.json("~/.config/example/settings.json", { theme: "dark" })],
},
});

const actions = await client.plan();
console.log(actions);

With config, the entry file need not exist. configPath still determines the lock location and default state identity. Targets retain normal home-relative semantics. Inline definitions can use the same factories, machines, tasks, aliases, and resource builders as file-based configs.

Client methods and effects

MethodResult/useWorkstation persistence or mutation
configuration()Resolved config, paths, tasks and resource IDsNone; evaluates configuration/source inputs
plan({ frozen? })Proposed Action[]None; queries and checks may run
status()Resource status entriesNone; uses recorded pins
doctor()Diagnostic messagesNone; checks command availability
history()Snapshot identifiersNone
rollback(id)Restoration previewNone
rollback(id, { apply: true })Apply supported restorationResource/state changes and a new snapshot
updateLock(ids?)Lock result including config, path, changedWrites lock; no prerequisite bootstrap or resource apply
build({ frozen?, noRemove? })Applied actionsBootstrap, locks, manifest, state, resources, hooks
upgrade(ids?)Refreshed build actionsRefresh pins and reconcile full configuration
task(name, args?){ exitCode, stdout, stderr }Runs selected command; no reconciliation

Omit ids to refresh all packages. upgrade currently accepts IDs only; it does not expose build's noRemove option or forward the client's action/progress callbacks. For a guarded embedded update, use updateLock(ids) followed by build({ frozen: true, noRemove: true }), with required managers/repositories already present. This is a two-command workflow, not one atomic transaction.

The CLI bootstraps selected Node-based tasks; client.task() does not. Provide its executable/runtime before dispatch. Always inspect task exit codes:

const result = await client.task("test", ["--run"]);
if (result.exitCode !== 0) {
throw new Error(`Task failed (${result.exitCode}): ${result.stderr}`);
}

This fragment assumes a client with a declared test task. Spawn failures reject rather than returning an ordinary nonzero command result.

Inspect the resolved configuration

import { createWorkstation, resourceId } from "@dovocode/workstation";

const client = createWorkstation({ configPath: "/absolute/path/workstation.config.ts" });
const config = await client.configuration();
console.log(config.stateFile);
console.log(config.context);
console.log(config.resources.map(resourceId));

This is useful before moving a setup, diagnosing machine selection, or choosing a dependency ID. Loading executable TypeScript is not a sandbox; configuration code can have its own side effects. Avoid printing entire resolved declarations when they contain private values.

Logging and custom runners

Embedded execution is silent by default. Opt into command output with:

import { createWorkstation, ProcessRunner } from "@dovocode/workstation";

const client = createWorkstation({
configPath: "/absolute/path/workstation.config.ts",
runner: new ProcessRunner({ progress: true, verbose: true }),
});

onAction reports build actions; onProgress reports build/plan progress where supported. ProcessRunner captures output, inherits stdin, and directly spawns literal argument vectors without an implicit shell. It does not allocate a PTY.

Implement Runner.run(command, args, options) for tests or another process boundary. Respect cwd, environment, and exit-code semantics; return actual observations instead of unconditional success. Reconciliation verifies results. A custom runner controls subprocesses, not Workstation's direct filesystem I/O.

Isolate file-only integration tests

import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createWorkstation, files } from "@dovocode/workstation";

const root = await mkdtemp(join(tmpdir(), "workstation-example-"));
try {
const client = createWorkstation({
configPath: join(root, "workstation.config.ts"),
context: {
home: root, configDir: root, hostname: "fixture", machine: "fixture",
platform: "linux",
},
config: {
stateFile: join(root, "state.json"),
resources: [files.json("settings.json", { enabled: true })],
},
});
await client.build();
const actions = await client.plan();
if (actions.length !== 0) throw new Error("Expected a converged second run");
// Release claims through reconciliation before deleting the temporary directory.
await createWorkstation({
configPath: join(root, "workstation.config.ts"),
context: {
home: root, configDir: root, hostname: "fixture", machine: "fixture",
platform: "linux",
},
config: { stateFile: join(root, "state.json"), resources: [] },
}).build();
} finally {
await rm(root, { recursive: true, force: true });
}

An explicit context is accepted only with inline definitions. This example isolates file targets and state; it is not OS emulation. Native service resources also consult the process environment, and package/provision operations can affect the real machine. Use disposable environments for those integration tests.

For lower-level loading, locking, manifests, diagnostics, fingerprints, and runner contracts, see the generated API reference and development guide.