Files
skills/docs/devtools/tooling/installer/src/host/apply/apply.md
2026-05-29 08:20:56 +01:00

5.8 KiB

apply

(no submodules)

- conflict — Resolves the per-op `conflict` policy on…
  • conflict — Resolves the per-op conflict policy on a target that already exists. Five policies are supported: error (raise `APP…

  • json-merge — Implements the deep-merge semantics used by patch-json ops with conflict: "merge". Match the conventions of `@suave…

  • text-patch — Applies an ordered sequence of TextEdits to a string. Used by patch-text ops to make targeted insertions/deletions/…

  • conflict — Resolves the per-op conflict policy on…

  • conflict — Resolves the per-op conflict policy on a target that already exists. Five policies are supported: error (raise `APP…

  • json-merge — Implements the deep-merge semantics used by patch-json ops with conflict: "merge". Match the conventions of `@suave…

  • text-patch — Applies an ordered sequence of TextEdits to a string. Used by patch-text ops to make targeted insertions/deletions/…

  • conflict — Resolves the per-op conflict policy on…

  • conflict — Resolves the per-op conflict policy on a target that already exists. Five policies are supported: error (raise `APP…

  • json-merge — Implements the deep-merge semantics used by patch-json ops with conflict: "merge". Match the conventions of `@suave…

  • text-patch — Applies an ordered sequence of TextEdits to a string. Used by patch-text ops to make targeted insertions/deletions/…

  • conflict — Resolves the per-op conflict policy on…

  • conflict — Resolves the per-op conflict policy on a target that already exists. Five policies are supported: error (raise `APP…

  • json-merge — Implements the deep-merge semantics used by patch-json ops with conflict: "merge". Match the conventions of `@suave…

  • text-patch — Applies an ordered sequence of TextEdits to a string. Used by patch-text ops to make targeted insertions/deletions/…

  • conflict — Resolves the per-op conflict policy on a target that already exists. Five policies are supported: error (raise `APP…

  • json-merge — Implements the deep-merge semantics used by patch-json ops with conflict: "merge". Match the conventions of `@suave…

  • text-patch — Applies an ordered sequence of TextEdits 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

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

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.