import { z } from "zod"; import { AihError } from "../../../errors.js"; import type { ClaudeContaminationReport, ContaminationSurface, FrameworkAttribution } from "./contamination.js"; /** * Opt-in, PREVIEWED cleanup remediation for USER-scope Claude contamination — the * machinery the physical-laptop sign-off depends on (its real polluted state is the * first target). Three phases, explicitly separated (D14 plan/apply): * * 1. {@link planClaudeCleanup} — a pure, JSON-serializable PREVIEW derived from a * contamination report. It plans the migration/disable of FRAMEWORK-ATTRIBUTED * surfaces only (unknown surfaces are opt-in via `includeUnknown`). It NEVER * deletes a whole shared JSON file — only targeted key/hook removals mirroring * D18 field-level discipline — and never touches anything outside `~/.claude/**` * or `~/.mcp.json`. * 2. {@link applyClaudeCleanup} — executes BACKUP-FIRST: every affected file (and * full dirs for removed trees) is copied into a timestamped backup root under * `/.aih/cleanup-backup//` with a schema-validated `manifest.json` * recording every step + original paths + digests. Only AFTER the manifest is * durably written do removals/edits run; a mid-apply error stops, preserves the * backup, and reports completed vs pending (fail closed). * 3. {@link rollbackClaudeCleanup} — restores from the manifest, validating each * backup file against its recorded digest and refusing a manifest that fails * schema or digest checks (a drifted entry is skipped + reported, never * overwritten). * * WRITE MECHANISM. The shared `executePlan` engine is root-contained against the * PROJECT root; cleanup targets the HOME scope, so routing through it adds no safety. * Direct, well-tested `node:fs` code with the manifest-first ordering as the safety * net is used instead — every destructive step stays behind the durable manifest. */ /** A cleanup-remediation error. Fails closed on any write-path ambiguity. */ export declare class ClaudeCleanupError extends AihError { constructor(message: string); } /** `backup-then-remove` = copy tree then delete it; `backup-then-disable` = backup JSON then targeted key/hook removal. */ export type ClaudeCleanupAction = "backup-then-remove" | "backup-then-disable"; /** How a `backup-then-disable` step edits its JSON file (targeted, never whole-file). */ export type ClaudeCleanupEdit = { kind: "json-key"; container: string; key: string; } | { kind: "hook"; event: string; command: string; }; /** One preview/apply step. JSON-serializable. */ export interface ClaudeCleanupStep { action: ClaudeCleanupAction; surface: ContaminationSurface; attribution: FrameworkAttribution; /** Home-relative POSIX path: the tree to remove, or the JSON file to edit. */ path: string; /** Present only for `backup-then-disable`. */ edit?: ClaudeCleanupEdit; } export interface ClaudeCleanupPlan { schemaVersion: 1; /** Whether unknown-attribution surfaces were folded into `steps`. */ includeUnknown: boolean; /** The steps to apply (the PREVIEW shown to the user). */ steps: ClaudeCleanupStep[]; /** Surfaces present in the report but left alone (unknown attribution, when not opted in). */ skipped: ClaudeCleanupStep[]; } export interface ClaudeCleanupPlanOptions { /** Opt in to remediating `unknown`-attribution surfaces too (default false). */ includeUnknown?: boolean; } /** * Build the cleanup PREVIEW from a contamination report. Framework-attributed * surfaces are planned; `unknown`-attribution surfaces are set aside in `skipped` * unless `includeUnknown` is set. Every step's target is validated here (and again * at apply) so a malformed report can never produce an out-of-scope write. */ export declare function planClaudeCleanup(report: ClaudeContaminationReport, opts?: ClaudeCleanupPlanOptions): ClaudeCleanupPlan; declare const CleanupManifestSchema: z.ZodObject<{ schemaVersion: z.ZodLiteral<1>; runId: z.ZodString; createdAt: z.ZodString; home: z.ZodString; backupDir: z.ZodLiteral<"files">; entries: z.ZodArray; surface: z.ZodEnum<{ agent: "agent"; hook: "hook"; mcpServer: "mcpServer"; plugin: "plugin"; rule: "rule"; skill: "skill"; }>; attribution: z.ZodEnum<{ ecc: "ecc"; gsd: "gsd"; superpowers: "superpowers"; unknown: "unknown"; }>; path: z.ZodString; edit: z.ZodOptional; container: z.ZodString; key: z.ZodString; }, z.core.$strict>, z.ZodObject<{ kind: z.ZodLiteral<"hook">; event: z.ZodString; command: z.ZodString; }, z.core.$strict>]>>; present: z.ZodBoolean; backup: z.ZodArray>; }, z.core.$strict>>; }, z.core.$strict>; export type ClaudeCleanupManifest = z.infer; export interface ClaudeCleanupApplyDeps { /** The user's home root (tests inject a mkdtemp home; NEVER the real `~`). */ home: string; /** Clock seam for a deterministic runId/timestamp. Defaults to `new Date()`. */ now?: () => Date; /** Explicit backup run id (deterministic tests). Defaults to a timestamp of `now`. */ runId?: string; /** * TEST-ONLY fault-injection seam: invoked immediately before each destructive step * (after the manifest is durably written). Throwing simulates a mid-apply failure to * prove the manifest-first ordering. No production caller sets this. */ beforeStep?: (step: ClaudeCleanupStep, index: number) => void; } export interface ClaudeCleanupApplyResult { runId: string; /** Absolute backup root — pass this to {@link rollbackClaudeCleanup}. */ backupRoot: string; status: "applied" | "failed"; completed: ClaudeCleanupStep[]; /** On failure: the failing step and everything after it (never executed). */ pending: ClaudeCleanupStep[]; /** Steps whose target was already absent (idempotent no-op). */ skippedAbsent: ClaudeCleanupStep[]; error?: string; } export declare function applyClaudeCleanup(plan: ClaudeCleanupPlan, deps: ClaudeCleanupApplyDeps): ClaudeCleanupApplyResult; export interface ClaudeCleanupRollbackDeps { /** The home root to restore into (the backup's recorded home when unmoved). */ home: string; } export interface ClaudeCleanupRollbackResult { runId: string; /** Home-relative paths restored byte-for-byte from the backup. */ restored: string[]; /** Entries skipped because the backup file drifted from its recorded digest (never overwritten). */ skippedDrifted: { path: string; reason: string; }[]; } /** * Restore a run's backup. The manifest is schema-validated (a schema break is * REFUSED outright); each backup file is re-digested and only restored when it * still matches its recorded digest — a drifted backup is skipped + reported * rather than laundered over the live file. */ export declare function rollbackClaudeCleanup(backupRoot: string, deps: ClaudeCleanupRollbackDeps): ClaudeCleanupRollbackResult; export {};