/** * `cleo skills doctor diagnose` — read-only health report for the skill store. * * @remarks * Pure read-only inspection of the four skill-related filesystem locations and * the per-user `skills.db` registry. Reports drift, orphans, broken symlinks, * and bridge status. Performs ZERO writes — never creates, removes, or renames * anything on disk. * * Locations inspected (per `docs/architecture/SG-CLEO-SKILLS-architecture-v3.md` §1): * * 1. **Canonical SSoT** — `~/.cleo/skills/` (the preferred user-machine root). * 2. **Legacy XDG** — `~/.local/share/agents/skills/` (read-only fallback). * 3. **Bridge mount** — `~/.agents/skills` (expected to be a symlink to * `~/.claude/skills/agents-shared`; if it is a real directory it must be * migrated and replaced with the symlink). * 4. **Claude Code discovery mount** — `~/.claude/skills/agents-shared/` * (per-skill symlinks into `~/.cleo/skills/`). * 5. **Claude direct entries** — `~/.claude/skills//` that are NOT under * `agents-shared/` (these should be reconciled). * * The diagnose runs in two passes: * * - **Disk pass** — walks each location, counts entries, classifies each * entry as `dir` / `symlink` / `brokenSymlink`, and resolves symlink * targets via `realpathSync`. * - **Db pass** — loads {@link openSkillsDb} and compares the registry rows * against the disk entries under the resolved canonical root to detect drift * (rows whose `installPath` is missing, dirs on disk not present in the db). * * The output schema is locked by `T9652` acceptance criteria: * * ```typescript * interface DoctorDiagnoseReport { * canonicalRoot: { path: string; exists: boolean; entryCount: number; isPreferredSsot: boolean }; * legacyRoot: { path: string; exists: boolean; entryCount: number }; * bridgeStatus: { agentsSkillsPath: string; kind: 'missing' | 'symlink' | 'real-dir'; symlinkTarget?: string; bridgeOk: boolean; }; * claudeSkillsAgentsShared: { path: string; exists: boolean; entryCount: number }; * claudeSkillsDirect: { path: string; exists: boolean; entryCount: number; sample: string[] }; * db: { path: string; rowCount: number; missingOnDisk: string[] }; * perSkillSymlinks: SkillSymlinkRecord[]; * orphans: OrphanRecord[]; * driftEntries: DriftRecord[]; * brokenSymlinks: BrokenSymlinkRecord[]; * healthy: boolean; * } * ``` * * @task T9652 * @epic T9571 * @saga T9560 * @architecture docs/architecture/SG-CLEO-SKILLS-architecture-v3.md §1, §4 */ import type { SkillSourceType } from '../store/schema/skills-schema.js'; /** Classification of a single on-disk entry under any inspected root. */ export type EntryKind = 'dir' | 'symlink' | 'broken-symlink'; /** * Per-skill symlink record under `~/.claude/skills/agents-shared/`. * * @public */ export interface SkillSymlinkRecord { /** The basename of the entry (e.g. `ct-orchestrator`). */ name: string; /** Absolute path to the symlink itself. */ path: string; /** Target after `realpathSync` (or null when the link is broken). */ target: string | null; /** True when the resolved target still exists. */ resolved: boolean; /** True when the resolved target is under the canonical SSoT root. */ pointsToCanonical: boolean; } /** * On-disk directory that is NOT registered in `skills.db`. * * @public */ export interface OrphanRecord { /** Skill basename (e.g. `my-custom-skill`). */ name: string; /** Absolute path to the orphan directory. */ path: string; /** Which inspected root the orphan was discovered under. */ rootLabel: 'canonical' | 'legacy' | 'agents' | 'claude-direct'; } /** * A row in `skills.db` whose `installPath` no longer resolves on disk. * * @public */ export interface DriftRecord { /** Skill name from the db row. */ name: string; /** `installPath` recorded in the db. */ recordedPath: string; /** Source-type from the db row (preserves provenance for the report). */ sourceType: SkillSourceType; /** Reason the row is considered drifted. */ reason: 'missing-on-disk' | 'lifecycle-archived-but-present'; } /** * A symlink whose target cannot be `realpathSync`-resolved. * * @public */ export interface BrokenSymlinkRecord { /** Absolute path to the broken link. */ path: string; /** Root label the link was discovered under. */ rootLabel: 'agents' | 'claude-direct' | 'agents-shared' | 'canonical' | 'legacy'; } /** * Bridge status — describes the `~/.agents/skills` entry. * * @public */ export interface BridgeStatus { /** Absolute path to `~/.agents/skills`. */ agentsSkillsPath: string; /** Whether the path exists at all, and if so, what kind. */ kind: 'missing' | 'symlink' | 'real-dir'; /** Resolved symlink target (only when `kind === 'symlink'`). */ symlinkTarget?: string; /** True when the symlink resolves to `~/.claude/skills/agents-shared`. */ bridgeOk: boolean; /** Count of entries when `kind === 'real-dir'` (else 0). */ realDirEntryCount: number; } /** * The full diagnose report. * * @public */ export interface DoctorDiagnoseReport { /** Resolved canonical SSoT root (`~/.cleo/skills/`). */ canonicalRoot: { path: string; exists: boolean; entryCount: number; /** True when the path is `~/.cleo/skills/` (i.e. the preferred SSoT). */ isPreferredSsot: boolean; }; /** Legacy XDG canonical store (`~/.local/share/agents/skills/`). */ legacyRoot: { path: string; exists: boolean; entryCount: number; }; /** Bridge mount status — see {@link BridgeStatus}. */ bridgeStatus: BridgeStatus; /** Claude Code discovery mount (`~/.claude/skills/agents-shared/`). */ claudeSkillsAgentsShared: { path: string; exists: boolean; entryCount: number; }; /** Direct entries under `~/.claude/skills/` (NOT under `agents-shared/`). */ claudeSkillsDirect: { path: string; exists: boolean; entryCount: number; sample: string[]; }; /** `skills.db` summary. */ db: { path: string; rowCount: number; missingOnDisk: string[]; }; /** Per-skill symlinks under `agents-shared/`. */ perSkillSymlinks: SkillSymlinkRecord[]; /** On-disk dirs not present in `skills.db`. */ orphans: OrphanRecord[]; /** Db rows whose paths no longer resolve. */ driftEntries: DriftRecord[]; /** Symlinks that cannot be `realpathSync`-resolved. */ brokenSymlinks: BrokenSymlinkRecord[]; /** True when NO issues were found (used to gate CLI exit code). */ healthy: boolean; } /** * Optional dependency-injection hooks for {@link diagnoseSkillStore}. * * @remarks * Tests pass `homeOverride` + `dbPathOverride` to point the diagnose at a * `mkdtemp`-prepared fixture. Production callers leave both undefined. * * @public */ export interface DoctorDiagnoseOptions { /** Override `homedir()` resolution (test-only). */ homeOverride?: string; /** Override the path used to open `skills.db` (test-only). */ dbPathOverride?: string; } /** * Run the read-only health diagnose pass on the local user's skill store. * * @remarks * Returns a fully-populated {@link DoctorDiagnoseReport}. Performs ZERO writes * to the filesystem and ZERO writes to `skills.db` — opens the db read-only * via {@link openSkillsDb}. * * Healthy criteria (all must hold for `healthy: true`): * * - Canonical root is the preferred `~/.cleo/skills/` (NOT the legacy fallback). * - Bridge symlink is present and resolves to `~/.claude/skills/agents-shared`. * - `~/.agents/skills` is a symlink (NOT a real directory). * - No broken symlinks anywhere in the inspected paths. * - No drift rows (db `installPath`s all resolve). * - No orphan directories. * * @param options - Optional DI overrides for tests; production callers omit. * @returns The complete diagnose report. * * @example * ```typescript * import { diagnoseSkillStore } from '@cleocode/caamp'; * * const report = await diagnoseSkillStore(); * if (!report.healthy) { * console.error('Skill store needs attention — run `cleo skills doctor migrate`'); * process.exit(1); * } * ``` * * @public */ export declare function diagnoseSkillStore(options?: DoctorDiagnoseOptions): Promise; /** * Render a {@link DoctorDiagnoseReport} as a plain-text health summary. * * @remarks * Used by the `cleo skills doctor diagnose` CLI surface when neither `--json` * nor `--verbose` is requested. Returns a multi-line string; the caller is * responsible for writing it to stdout. * * @param report - The report produced by {@link diagnoseSkillStore}. * @param verbose - When true, includes per-skill detail lines. * @returns A formatted multi-line string ending in a trailing newline. * * @public */ export declare function renderDoctorDiagnoseReport(report: DoctorDiagnoseReport, verbose?: boolean): string; //# sourceMappingURL=doctor.d.ts.map