/** * Doctor — `cleo skills doctor bridge` business logic. * * @remarks * Implements the canonical discovery topology described in * `docs/architecture/SG-CLEO-SKILLS-architecture-v3.md` §1: * * - `~/.cleo/skills//` is the per-user install root (Sphere A + B). * - `~/.claude/skills/agents-shared/` is a symlink INTO `~/.cleo/skills/` * for each installed skill — Claude Code's hardcoded discovery mount. * - `~/.agents/skills` is the SINGLE bridge symlink → `~/.claude/skills/agents-shared` * used by every non-Claude harness (Cursor, Aider, Codeium, etc.). * * The bridge command takes a host machine from any pre-v3 state and: * * 1. Ensures `~/.claude/skills/agents-shared/` exists (mkdir -p). * 2. Creates a symlink under `agents-shared/` for every skill currently in * `~/.cleo/skills/` whose target is missing or wrong. * 3. Atomically replaces `~/.agents/skills` with a symlink to * `~/.claude/skills/agents-shared`. If the existing `~/.agents/skills` is a * REAL directory with contents, the function refuses without `--force` and * backs up to `~/.cleo/backups/agents-skills-pre-bridge-YYYYMMDD-HHmmss/` * when `--force` is supplied. * 4. Rips per-skill symlinks under `~/.claude/skills/*` that point OUTSIDE * `agents-shared/` (orphans from the old per-skill fan-out model). * * The function is pure-functional with a dependency-injected `homeDir` so it * can be exercised against tmpfs fixtures in unit tests without touching the * real user environment. * * ## Locality (T9740 Wave B — T9744) * * Moved from `packages/caamp/src/commands/skills/doctor-bridge.ts` to CORE so * the cleo CLI (and any other CORE consumer) can import it without crossing * the `core → caamp` dep boundary. The legacy Commander registrar in caamp * was deleted at the same time — cleo dispatches via citty and never wired * the registrar in. * * @see {@link docs/architecture/SG-CLEO-SKILLS-architecture-v3.md} §1 * @task T9744 * @epic T9740 */ /** * One symlink that was created (or would be created in `--dry-run`). * * @public */ export interface BridgeSymlinkRecord { /** Skill basename, e.g. `ct-orchestrator`. */ name: string; /** Absolute path of the symlink under `~/.claude/skills/agents-shared/`. */ linkPath: string; /** Absolute path of the symlink target inside `~/.cleo/skills/`. */ target: string; } /** * One per-skill symlink that was removed (or would be removed in `--dry-run`). * * @public */ export interface PerSkillSymlinkRemoval { /** Absolute path of the symlink that was removed. */ linkPath: string; /** Resolved target the symlink pointed at, or `null` when unreadable. */ previousTarget: string | null; } /** * LAFS-shaped result payload emitted by {@link runDoctorBridge}. * * @public */ export interface DoctorBridgeResult { /** Whether this run materially changed disk state. `false` on idempotent re-runs. */ bridgeCreated: boolean; /** * Whether `~/.agents/skills` is now a symlink to `~/.claude/skills/agents-shared`. * * @remarks * Always `true` after a successful run. `false` only when `--dry-run` was * requested and the bridge had to be created. */ bridgeSymlinkActive: boolean; /** Symlinks created under `~/.claude/skills/agents-shared/` (or planned in dry-run). */ perSkillSymlinksCreated: BridgeSymlinkRecord[]; /** Per-skill symlinks under `~/.claude/skills/*` that were removed (or planned). */ perSkillSymlinksRemoved: PerSkillSymlinkRemoval[]; /** * Absolute backup path when the existing `~/.agents/skills` real dir was * relocated to make room for the bridge symlink. `null` when no backup was * needed. */ backupPath: string | null; /** `true` when `--dry-run` was passed and no disk state was mutated. */ dryRun: boolean; /** Resolved skills root, e.g. `~/.cleo/skills`. */ skillsRoot: string; /** Resolved bridge target, e.g. `~/.claude/skills/agents-shared`. */ bridgeTarget: string; /** Resolved bridge symlink path, e.g. `~/.agents/skills`. */ bridgePath: string; } /** * Dependency-injected options accepted by {@link runDoctorBridge}. * * @public */ export interface DoctorBridgeOptions { /** * Override the home directory. Defaults to {@link homedir}. * * @remarks * Tests pass a tmpfs root so the bridge logic can be exercised end-to-end * without touching the real user environment. */ homeDir?: string; /** * Allow the function to clobber an existing real `~/.agents/skills` directory. * * @remarks * When `false` (the default) and `~/.agents/skills` is a non-empty real * directory, the function refuses with `E_AGENTS_SKILLS_REAL_DIR` to preserve * user data. When `true`, the directory is moved to * `~/.cleo/backups/agents-skills-pre-bridge-/` before the bridge symlink * is created. * * @defaultValue `false` */ force?: boolean; /** * Plan-only mode. When `true`, no disk state is mutated; the result still * lists what WOULD happen. * * @defaultValue `false` */ dryRun?: boolean; } /** * Error thrown when {@link runDoctorBridge} refuses to clobber a real * `~/.agents/skills` directory and `--force` was not passed. * * @public */ export declare class AgentsSkillsRealDirError extends Error { /** LAFS error code surfaced by the CLI. */ readonly code: "E_AGENTS_SKILLS_REAL_DIR"; /** Resolved path of the offending real directory. */ readonly agentsSkillsPath: string; /** Number of immediate entries in the offending directory. */ readonly entryCount: number; /** * Construct an `AgentsSkillsRealDirError`. * * @param agentsSkillsPath - Path to the real `~/.agents/skills` directory. * @param entryCount - Number of entries inside the directory. */ constructor(agentsSkillsPath: string, entryCount: number); } /** * Generate a deterministic backup-suffix timestamp `YYYYMMDD-HHmmss` (UTC). * * @remarks * Pulled out as a helper so callers (and tests) can deterministically compute * the expected backup path without re-implementing the format. The string is * UTC so backups taken on different machines round-trip identically. * * @returns Timestamp suffix string for backup directory naming. * * @public */ export declare function buildBackupTimestamp(): string; /** * Execute the bridge flow described in the module docblock. * * @remarks * Order of operations: * * 1. Ensure `~/.claude/skills/agents-shared/` exists. * 2. For each skill in `~/.cleo/skills/`, ensure * `~/.claude/skills/agents-shared/` → `~/.cleo/skills/` exists. * 3. Rip every per-skill entry under `~/.claude/skills/` that is a symlink * pointing OUTSIDE `agents-shared/` — those are orphans from the old * per-skill fan-out model and must be deleted. * 4. Replace `~/.agents/skills` with a symlink to `~/.claude/skills/agents-shared`. * If it is currently a real directory, refuse unless `options.force` is * `true`, in which case back up to * `~/.cleo/backups/agents-skills-pre-bridge-/` first. * * Idempotency invariant: re-running on a fully-bridged tree returns * `{ bridgeCreated: false, bridgeSymlinkActive: true, perSkillSymlinksCreated: [], perSkillSymlinksRemoved: [], backupPath: null }`. * * @param options - Dependency-injected options (homeDir / force / dry-run). * @returns Materialized {@link DoctorBridgeResult} reflecting the run. * @throws AgentsSkillsRealDirError when `~/.agents/skills` is a non-empty real * directory and `options.force` is not set. * * @example * ```typescript * import { runDoctorBridge } from '@cleocode/core'; * * const result = await runDoctorBridge({ homeDir: '/tmp/test-home' }); * console.log(result.perSkillSymlinksCreated.length); // # of new bridge symlinks * ``` * * @public */ export declare function runDoctorBridge(options?: DoctorBridgeOptions): Promise; //# sourceMappingURL=doctor-bridge.d.ts.map