first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
# `apply`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - 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<PlanSummary>`
- `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.