/** * Installs a skill from a cloned repo directory into the target skills * directory using a crash-safe swap flow. */ import type { LoggerLike } from "./types.js"; /** * Copy a skill's subdirectory from `cloneDir` into `skillsDir//`, * replacing the existing copy via a crash-safe swap. * * Strategy: * 1. Validate source directory exists in the clone. * 2. Write to a `.tmp-` staging path inside `skillsDir`. * 3. Merge user-data subpaths (`preservePaths`) into staging — from any * orphaned `.bak-*` backups of this skill (crash salvage, oldest first), * then from the live target (most recent wins). Done BEFORE the swap so * the swap itself is the atomic commit point: a crash at any moment * leaves the user data in the target, the backup, or both — never only * in a directory nothing will ever read again. * 4. Move existing target to a hidden backup path (if present). * 5. Move staging → target (atomic on same fs). * 6. Merge the backup's user-data subpaths into the now-live target (newest * copy per entry wins), to catch writes that landed after step 3 read the * live dir without reverting writes that landed after step 5. * 7. Delete the backup (and any salvaged orphaned backups) only after the new * target is verified in place and step 6 completed. * * This avoids destroying the previous skill copy before the new one is ready. * There is still a brief window where `targetPath` may be absent, but the old * content remains recoverable from the backup path until install succeeds — * and a crash inside that window self-heals: the next reinstall (bootstrap on * the following start, which sees the skill dir missing; or a later update * tick) runs step 3 and salvages the preserved data from the leftover backup. * Both paths must pass `preservePaths` for that to hold — salvage only runs * when the list is non-empty. * * The `.tmp-` prefix hides the staging directory from skill discovery, which * only reads immediate subdirectories of `skillsDir`. * * @throws When the skill subdirectory is absent in the clone. * @throws When the staging copy fails. */ export declare function installSkill(cloneDir: string, skillName: string, skillsDir: string, logger?: LoggerLike, options?: { sourcePath?: string; allowedSkills?: readonly string[]; /** * The version we intend to install (the version the check read from the * remote SKILL.md). When provided, the installed SKILL.md is re-read after * the swap and the install is **rolled back** if it doesn't match — closing * the gap between the CDN version check and the git clone (TOCTOU). Omit * (e.g. bootstrap) to skip verification. */ expectedVersion?: string | null; /** * Relative subpaths of the skill directory that hold **user data** (e.g. * `strategies/` for `senpi-strategy-ops`, where deployed strategy packages * may live). They are merged from the previous install (and from any * orphaned backups of it) into the staging copy before the swap, so a * skill update can never destroy them and a crash at any point leaves * them recoverable. The user's copy wins on conflict — a deployed, * rendered package must never be clobbered by a repo template. A merge * failure aborts the install before the live skill dir is touched — * user data is never traded for a skill bump. After the swap the same * subpaths are merged back out of the backup — newest copy per entry wins * — so a write that raced the swap isn't dropped with it, and a write * landing after the swap isn't reverted by it. */ preservePaths?: readonly string[]; /** * TEST ONLY — production callers never pass this, and when absent nothing * about the install changes. Invoked between the preserve merge and the * target→backup rename: the window in which a concurrent write lands in * the live dir after the merge has read past it. It exists so the * post-swap recovery merge can be tested deterministically instead of by * racing a real writer. */ afterPreserveMergeForTest?: () => Promise | void; /** * TEST ONLY — the other side of the swap: invoked after the staging→target * rename (and verification) and before the recovery merge reads the * backup. A write landing here goes into the new live target through a * fresh path lookup, and the recovery merge must not clobber it with the * backup's older copy. */ afterSwapForTest?: () => Promise | void; }): Promise; //# sourceMappingURL=writer.d.ts.map