Programmatic API
Workler can be used as a library, so tools can create, list, and remove workspaces without shelling out to the CLI or parsing console text.
Install
npm install worklerThe package includes TypeScript declarations and supports both import and require. Its API is synchronous.
Quick start
import { createWorkspace, listWorkspaces, removeWorkspace } from "workler";
const root = "/path/to/git-project";
const ws = createWorkspace(root, { name: "agent-1" });
console.log(ws.path, ws.branch, ws.head);
for (const info of listWorkspaces(root)) {
console.log(info.name, info.branch ?? info.shortHead, info.clean, info.broken ?? "ok");
}
removeWorkspace(root, "agent-1", { force: true });The API and the CLI share the same core: workler add and createWorkspace() are the same code path.
Contract
- Explicit roots. Every function takes the project root as its first argument. Nothing reads
process.cwd()or mutates global state, so a long-running process can drive many projects concurrently. To get CLI-style discovery of the nearest enclosing project, callfindWorklerRoot(startDir)yourself. - Synchronous. All functions are synchronous, like the CLI.
- Structured domain errors. Validation, project/workspace state, rule, setup, and lock failures throw
WorklerErrorwith a stablecode(plusdetails); themessageis the same human-readable text the CLI prints. Unexpected host failures from Node/Git (for example an I/O permission error) may remain ordinaryErrorinstances and should still be caught. Codes includeNOT_INITIALIZED,NOT_A_GIT_REPO,WORKSPACE_EXISTS,WORKSPACE_NOT_FOUND,WORKSPACE_DIRTY,BRANCH_EXISTS,BAD_REF,INVALID_NAME,INVALID_OPTIONS,CONFIG_INVALID,RULE_CONFLICT,SETUP_FAILED, andLOCKED. - Workspaces live in
<root>/.worktrees/<name>— the same project-local layout the CLI uses, including nested workspaces (pass a workspace's path as the root to manage its own.worktrees/). The.worktreescontainment directory must not be a symlink or junction.
Functions
initProject(root): InitResult
Same as the optional workler init helper: creates .workler (if missing), .worktrees/, marks the repository with workler.* Git config, and adds .worktrees/ to .git/info/exclude (or .gitignore for non-Git roots). Ordinary Git repositories do not need initialization. Idempotent; the result reports whether the config file was created.
inspectProject(root): ProjectInfo
Non-throwing inspection: { root, exists, gitRepo, marked, configFileExists, workspacesDirExists, initialized, parent }. Every Git repository is immediately usable and reports initialized: true; a non-Git directory does so only when .workler explicitly marks it. parent is set when the root is itself a managed workspace (nested workspaces).
createWorkspace(root, options): CreateWorkspaceResult
Same semantics as workler add. options: { name, base?, branch?, checkout?, force? } with the exact add rules. checkout is exclusive, while base and branch may be combined to create an explicitly named branch from an explicit start point; name alone creates a new branch from HEAD. Optional onProgress(message) and onRuleResult(result) callbacks stream what the CLI would print. The result carries path, branch (undefined when detached), head, detached, and the structured copy/link rules outcome.
.workler copy/link rules are applied exactly as with the CLI, including conflict detection and force replacement. If setup fails after the clone was created (bad config, rule conflict), the error is SETUP_FAILED and the clone is left in place for inspection — the same partial-failure contract the CLI documents.
planWorkspaceCreation(root, options): WorkspacePlan
The read-only planning half of createWorkspace (what add --dry-run prints): the resolved target path, the checkout plan, and any warnings. Nothing is created.
listWorkspaces(root): WorkspaceInfo[]
The main project plus every entry under .worktrees/, including broken ones: { name, path, isMain, isClone, broken?, branch?, head?, shortHead?, detached, clean? }. clean uses the same guard as remove (untracked and ignored files count as dirty), so for a non-main workspace clean === true means removable without force.
resolveWorkspacePath(root, name): string
Absolute path of a workspace by name ("main" resolves to the root). Throws WORKSPACE_NOT_FOUND for unknown names.
removeWorkspace(root, name, { force? }): RemoveWorkspaceResult
Same as workler remove: refuses to remove main, and refuses a workspace with uncommitted changes (including untracked and ignored files) unless force is set — that failure is WORKSPACE_DIRTY.
Locking
The mutating API operations (initProject, createWorkspace, removeWorkspace) and mutating CLI operations (add, apply, remove, fetch, sync, and branch-sync) take a per-project lock for their duration, so mutating commands addressed to the same project root are serialized. Dry runs and read-only operations never lock.
- The lock is a JSON file at
<root>/.worktrees/.workler.lock(projectLockPath(root)returns it) recording the holder's pid, hostname, operation, start time, and a unique ownership token. - If the lock is already held, the operation fails immediately with
LockError(code: "LOCKED", withlockPathandholder). There is no built-in waiting — retry at whatever cadence suits the caller. - Stale-lock recovery: a lock whose holder can be proven dead is reclaimed automatically — same hostname and the pid no longer exists, or an unreadable/torn lock file is at least 30 seconds old. Fresh malformed files are protected because another process may still be writing them. A lock from a different host is never reclaimed (liveness cannot be checked); delete the file manually if you know the process is gone.
- Locks are scoped per project root: nested projects lock their own
.worktrees/, independent of the parent. A parentfetch,sync, orbranch-syncalso touches its child repositories, so do not run one concurrently with a mutation addressed directly to a nested project.