/** * @fileoverview Pre-write install-target safety guard. * @module @skillsmith/core/services/skill-installation.target-guard * @see SMI-6529 Wave A0 * * On a real user machine, `skillsmith update --all` overwrote uncommitted * local work in git-cloned skill directories and wrote into the wrong * directory (a target named after the upstream repo, not the directory * being updated). `checkInstallTarget()` is the single pre-write gate every * `install()`/`installFromContent()` call runs BEFORE any content fetch or * disk write — it never mutates anything itself, only inspects the * filesystem and the manifest and reports whether the write may proceed. * * Rules are evaluated in order; the first that fails wins: * (pre) A matching manifest entry's `installPath` must be a usable, real * absolute path — a missing or non-absolute value refuses rather than * reaching `path.resolve()`/`fs.realpath()` (SMI-6529 L17). * (a) `expectedInstallPath` (set by `update`) must match `installPath` * exactly, AND must exist on disk — `force` does NOT override this. * (b) `installPath` missing (ENOENT) -> fresh install, nothing else to * check, UNLESS the manifest key it will occupy already has an entry * whose recorded path is a live conflict (SMI-6529 M8) — see below. * Any other `lstat` error is rethrown (fail closed). * (c) `installPath` must be a directory, or a symlink that resolves to a * directory inside `skillsDir`. * (d) Filesystem-only git-working-tree check (never spawns git): a `.git` * entry (file or directory) at `installPath` or any ancestor up to and * including `skillsDir` refuses the write — `force` does NOT override * this. Walks BOTH the lexical ancestor chain and (SMI-6529 H1) the * REALPATH ancestor chain, since a symlinked `installPath` can resolve * into a git clone whose ancestry `path.dirname()` on the symlink's own * lexical string never crosses into. * (e) The manifest must already track this exact path (by realpath) — * an untracked pre-existing directory is refused — `force` does NOT * override this either. Even a path-matching entry refuses if it is * itself untracked by Skillsmith's trust model (`provenance:'local'` * or `source:'unknown'` — ADR-139 adoption or a user's local * assertion) — `force` does NOT override this either. * (f) Tracked + matching path: existing `ALREADY_INSTALLED` behavior * (refuse without `force`, proceed with it). */ import type { InstallErrorCode, SkillManifestEntry } from './skill-installation.types.js'; /** The refusal codes {@link checkInstallTarget} can return. */ export type InstallTargetFailureCode = Extract; /** Input to {@link checkInstallTarget}: the write target, its skills root, and the manifest entry at its key. */ export interface CheckInstallTargetParams { installPath: string; skillsDir: string; /** The manifest entry at the computed manifest key, or undefined when untracked. */ manifestEntry: SkillManifestEntry | undefined; force: boolean; /** Set by `update` — see {@link InstallOptions.expectedInstallPath}'s own doc comment. */ expectedInstallPath?: string; } /** Whether the write may proceed; `preExisted` says whether the target directory was already there. */ export type CheckInstallTargetResult = { ok: true; preExisted: boolean; } | { ok: false; code: InstallTargetFailureCode; error: string; tips?: string[]; }; /** The outcome of walking for a `.git` entry: a real hit, a fail-closed * "couldn't verify" at some ancestor, or nothing found (return `null`). */ export type GitWalkResult = { kind: 'found'; path: string; } | { kind: 'error'; path: string; errorCode: string; }; /** * The pre-write gate: decide, without changing anything, whether an install * or update may write into `installPath`. The rules (pre) and (a)–(f) in the * module comment are checked in order, and the first to fail wins. Callers * run it before any content fetch or disk write. */ export declare function checkInstallTarget(params: CheckInstallTargetParams): Promise; //# sourceMappingURL=skill-installation.target-guard.d.ts.map