# `apply` _(no submodules)_ - conflict — Resolves the per-op `conflict` policy on… - [conflict](./conflict.md) — Resolves the per-op `conflict` policy on a target that already exists. Five policies are supported: `error` (raise `APP… - [json-merge](./json-merge.md) — Implements the deep-merge semantics used by `patch-json` ops with `conflict: "merge"`. Match the conventions of `@suave… - [text-patch](./text-patch.md) — Applies an ordered sequence of `TextEdit`s to a string. Used by `patch-text` ops to make targeted insertions/deletions/… - conflict — Resolves the per-op `conflict` policy on… - [conflict](./conflict.md) — Resolves the per-op `conflict` policy on a target that already exists. Five policies are supported: `error` (raise `APP… - [json-merge](./json-merge.md) — Implements the deep-merge semantics used by `patch-json` ops with `conflict: "merge"`. Match the conventions of `@suave… - [text-patch](./text-patch.md) — Applies an ordered sequence of `TextEdit`s to a string. Used by `patch-text` ops to make targeted insertions/deletions/… - conflict — Resolves the per-op `conflict` policy on… - [conflict](./conflict.md) — Resolves the per-op `conflict` policy on a target that already exists. Five policies are supported: `error` (raise `APP… - [json-merge](./json-merge.md) — Implements the deep-merge semantics used by `patch-json` ops with `conflict: "merge"`. Match the conventions of `@suave… - [text-patch](./text-patch.md) — Applies an ordered sequence of `TextEdit`s to a string. Used by `patch-text` ops to make targeted insertions/deletions/… - conflict — Resolves the per-op `conflict` policy on… - [conflict](./conflict.md) — Resolves the per-op `conflict` policy on a target that already exists. Five policies are supported: `error` (raise `APP… - [json-merge](./json-merge.md) — Implements the deep-merge semantics used by `patch-json` ops with `conflict: "merge"`. Match the conventions of `@suave… - [text-patch](./text-patch.md) — Applies an ordered sequence of `TextEdit`s to a string. Used by `patch-text` ops to make targeted insertions/deletions/… - [conflict](./conflict.md) — Resolves the per-op `conflict` policy on a target that already exists. Five policies are supported: `error` (raise `APP… - [json-merge](./json-merge.md) — Implements the deep-merge semantics used by `patch-json` ops with `conflict: "merge"`. Match the conventions of `@suave… - [text-patch](./text-patch.md) — Applies an ordered sequence of `TextEdit`s to a string. Used by `patch-text` ops to make targeted insertions/deletions/… ## Purpose Materialises a `FileOp[]` against a `MutableInstallerFs` with rollback semantics. This is the only mutating stage in the installer pipeline. Each successful op records an `undo` entry on a journal; if any op fails, the journal replays in LIFO order to reverse already-applied work. Apply is wrapped in an OTel span (`installer.apply`) and is the only place in the host where `INSTALLER.APPLY_*` and `INSTALLER.ROLLBACK_FAILED` codes originate. ## Features - Per-op conflict resolution via `resolveConflict` — the host consults each op's `conflict` policy before mutating the fs. - Transactional rollback in reverse order — an apply failure undoes everything that succeeded earlier in the run. - Distinct codes: `APPLY_CONFLICT` for policy violations, `APPLY_FAILED` for plugin/fs exceptions, `ROLLBACK_FAILED` when undo itself throws, `ABORTED` when the run was cancelled mid-apply. - `dry-run` is honoured by the host _before_ apply runs, so apply itself always writes — no internal `if (dryRun) skip` branches to test. - Per-op summary (`written`/`patched`/`skipped`/`conflicts`) for the host to return. ## Basic Example ```ts import { applyOps, MemoryFs } from "@suaveplan/installer"; const fs = new MemoryFs(); const summary = await applyOps({ ops: [ { kind: "write", path: "/hello.txt", contents: new TextEncoder().encode("hi"), }, ], ctx, fs, pluginId: "demo", pluginVersion: "0.1.0", }); console.log(summary); // { written: 1, patched: 0, skipped: 0, conflicts: 0 } ``` ## Advanced Example ```ts import { applyOps, InstallerError } from "@suaveplan/installer"; try { await applyOps({ ops, ctx, fs, pluginId, pluginVersion }); } catch (err) { if (err instanceof InstallerError) { switch (err.code) { case "INSTALLER.APPLY_CONFLICT": console.error("Pre-existing files conflict; re-run with --force"); break; case "INSTALLER.ROLLBACK_FAILED": console.error( "Apply failed AND rollback failed — fs may be in a partial state", err.context?.["original"] ); break; default: throw err; } } } ``` ## API Reference - `applyOps(options: ApplyOptions): Promise` - `interface ApplyOptions` — `{ ops, ctx, fs, pluginId, pluginVersion }`. - Errors: `INSTALLER.APPLY_CONFLICT`, `INSTALLER.APPLY_FAILED`, `INSTALLER.ROLLBACK_FAILED`, `INSTALLER.ABORTED`. ## Implementation Notes Each op's undo is captured into the journal _before_ the next op runs, so even if abort fires mid-op the partial state is recoverable. `mkdir` ops are short-circuited by `resolveConflict` for already-existing directories (idempotent default), so we don't need a redundant `if (exists) return` inside apply. The patch ops (`patch-json`, `patch-text`) read the prior content (when the file exists) so undo can restore byte-for-byte; when the file didn't exist, undo is a `deleteFile`. The `ROLLBACK_FAILED` path attaches the original apply error as `context.original` so the caller can still report root cause even when the recovery path failed.