/**
 * Directory & file scaffolding utilities.
 *
 * Shared ensure/check functions extracted from init.ts for reuse
 * by init.ts, upgrade.ts, and doctor health checks.
 *
 * Rules:
 *   - All ensure functions are idempotent (safe to call multiple times)
 *   - All check functions are read-only (no side effects)
 *   - Uses imports from ./paths.js for path resolution
 */
import { execFile } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { existsSync, constants as fsConstants, readFileSync, statSync } from 'node:fs';
import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { homedir as getHomedir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import { generateProjectHash } from './nexus/hash.js';
import { getCleoDirAbsolute, getCleoHome, getCleoTemplatesDir, getConfigPath } from './paths.js';
import { saveJson } from './store/json.js';
const execFileAsync = promisify(execFile);
// ── Constants ────────────────────────────────────────────────────────
/** Required subdirectories under .cleo/. */
export const REQUIRED_CLEO_SUBDIRS = [
    'backups/operational',
    'backups/safety',
    'agent-outputs',
    'logs',
    'rcasd',
    'adrs',
];
/** Embedded fallback for .cleo/.gitignore content (deny-by-default). */
export const CLEO_GITIGNORE_FALLBACK = `# .cleo/.gitignore — Deny-by-default for CLEO project data
# Ignore everything, then explicitly allow only tracked files.

# Step 1: Ignore everything
*

# Allow list
!.gitignore
!config.json
!project-context.json
!project-info.json
!setup-otel.sh
!DATA-SAFETY-IMPLEMENTATION-SUMMARY.md
!adrs/
!adrs/**
!rcasd/
!rcasd/**
!agent-outputs/
!agent-outputs/**

# Explicit deny safety net
*.db
*.db-shm
*.db-wal
*.db-journal
log.json
bypass-log.json
qa-log.json
.deps-cache/
.context-alert-state.json
.context-state*.json
context-states/
.git-checkpoint-state
.migration-state.json
migrations.json
sync/
metrics/
.backups/
backups/
`;
// ── Pure helpers ─────────────────────────────────────────────────────
/**
 * Check if a file exists and is readable.
 */
export async function fileExists(path) {
    try {
        await access(path, fsConstants.R_OK);
        return true;
    }
    catch {
        return false;
    }
}
/**
 * Strip legacy <!-- CLEO:START -->...<!-- CLEO:END --> blocks from a file.
 * Called before CAAMP injection to prevent competing blocks.
 *
 * Handles both bare markers (<!-- CLEO:START -->) and versioned markers
 * (<!-- CLEO:START v0.53.4 -->, <!-- CLEO:START v1.2.3 -->).
 */
export async function stripCLEOBlocks(filePath) {
    if (!existsSync(filePath))
        return;
    const content = await readFile(filePath, 'utf8');
    const stripped = content.replace(/\n?<!-- CLEO:START[^>]*-->[\s\S]*?<!-- CLEO:END -->\n?/g, '');
    if (stripped !== content)
        await writeFile(filePath, stripped, 'utf8');
}
/**
 * Remove .cleo/ or .cleo entries from the project root .gitignore.
 */
export async function removeCleoFromRootGitignore(projectRoot) {
    const rootGitignorePath = join(projectRoot, '.gitignore');
    if (!(await fileExists(rootGitignorePath))) {
        return { removed: false };
    }
    const content = await readFile(rootGitignorePath, 'utf-8');
    const lines = content.split('\n');
    const filtered = lines.filter((line) => {
        const trimmed = line.trim();
        return !/^\/?\.cleo\/?(\*)?$/.test(trimmed);
    });
    if (filtered.length === lines.length) {
        return { removed: false };
    }
    await writeFile(rootGitignorePath, filtered.join('\n'));
    return { removed: true };
}
// generateProjectHash moved to src/core/nexus/hash.ts (canonical location)
export { generateProjectHash } from './nexus/hash.js';
/**
 * Resolve the package root directory (where schemas/ and templates/ live).
 * scaffold.ts lives in packages/core/src/, so 1 level up reaches the package root.
 */
export function getPackageRoot() {
    const thisFile = fileURLToPath(import.meta.url);
    return resolve(dirname(thisFile), '..');
}
/**
 * Load the gitignore template from the package's templates/ directory.
 * Falls back to embedded content if file not found.
 */
export function getGitignoreContent() {
    try {
        const packageRoot = getPackageRoot();
        const templatePath = join(packageRoot, 'templates', 'cleo-gitignore');
        if (existsSync(templatePath)) {
            return readFileSync(templatePath, 'utf-8');
        }
    }
    catch {
        // fallback
    }
    return CLEO_GITIGNORE_FALLBACK;
}
/**
 * Read CLEO version from package.json.
 */
export function getCleoVersion() {
    try {
        const pkgPath = join(getPackageRoot(), 'package.json');
        const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
        return pkg.version ?? '0.0.0';
    }
    catch {
        return '0.0.0';
    }
}
/**
 * Create default config.json content.
 */
/**
 * Detect whether projectRoot is the CLEO source repository itself.
 * Verified by fingerprinting the expected source layout and package identity.
 * Only the canonical CLEO repository matches all criteria (ADR-029).
 */
function isCleoContributorProject(projectRoot) {
    const exists = (p) => existsSync(join(projectRoot, p));
    // Must have all three canonical source directories
    if (!exists('src/mcp') || !exists('src/dispatch') || !exists('src/core'))
        return false;
    // Must have package.json identifying as @cleocode/cleo
    try {
        const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf-8'));
        return pkg.name === '@cleocode/cleo';
    }
    catch {
        return false;
    }
}
export function createDefaultConfig() {
    return {
        version: '2.10.0',
        output: {
            defaultFormat: 'json',
            showColor: true,
            showUnicode: true,
            dateFormat: 'relative',
        },
        backup: {
            maxOperationalBackups: 10,
            maxSafetyBackups: 5,
        },
        hierarchy: {
            maxDepth: 3,
            maxSiblings: 0,
        },
        session: {
            autoStart: false,
            multiSession: false,
        },
        lifecycle: {
            mode: 'strict',
        },
    };
}
// ── ensure* functions (idempotent) ───────────────────────────────────
/**
 * Create .cleo/ directory and all required subdirectories.
 * Idempotent: skips directories that already exist.
 */
export async function ensureCleoStructure(projectRoot) {
    // Guard: reject global home as project root to prevent project-level
    // subdirectories (adrs/, rcasd/, backups/, etc.) from polluting ~/.cleo/
    const resolvedRoot = resolve(projectRoot);
    if (resolvedRoot === resolve(getCleoHome())) {
        return {
            action: 'skipped',
            path: join(resolvedRoot, '.cleo'),
            details: 'Refused to scaffold project structure inside global CLEO home',
        };
    }
    const cleoDir = getCleoDirAbsolute(projectRoot);
    const alreadyExists = existsSync(cleoDir);
    await mkdir(cleoDir, { recursive: true });
    for (const subdir of REQUIRED_CLEO_SUBDIRS) {
        await mkdir(join(cleoDir, subdir), { recursive: true });
    }
    return {
        action: alreadyExists ? 'skipped' : 'created',
        path: cleoDir,
        details: alreadyExists
            ? 'Directory already existed, ensured subdirs'
            : `Created .cleo/ with ${REQUIRED_CLEO_SUBDIRS.length} subdirectories`,
    };
}
/**
 * Create or repair .cleo/.gitignore from template.
 * Idempotent: skips if file already exists with correct content.
 */
export async function ensureGitignore(projectRoot) {
    const cleoDir = getCleoDirAbsolute(projectRoot);
    const gitignorePath = join(cleoDir, '.gitignore');
    const templateContent = getGitignoreContent();
    if (existsSync(gitignorePath)) {
        const existing = readFileSync(gitignorePath, 'utf-8');
        const normalize = (s) => s.trim().replace(/\r\n/g, '\n');
        if (normalize(existing) === normalize(templateContent)) {
            return { action: 'skipped', path: gitignorePath, details: 'Already matches template' };
        }
        await writeFile(gitignorePath, templateContent);
        return { action: 'repaired', path: gitignorePath, details: 'Updated to match template' };
    }
    await writeFile(gitignorePath, templateContent);
    return { action: 'created', path: gitignorePath };
}
/**
 * Create default config.json if missing.
 * Idempotent: skips if file already exists.
 */
export async function ensureConfig(projectRoot, opts) {
    const configPath = getConfigPath(projectRoot);
    if (existsSync(configPath) && !opts?.force) {
        // Backfill contributor block for existing configs that predate ADR-029
        try {
            const existing = JSON.parse(readFileSync(configPath, 'utf-8'));
            if (!existing['contributor'] && isCleoContributorProject(projectRoot)) {
                existing['contributor'] = {
                    isContributorProject: true,
                    devCli: 'cleo-dev',
                    verifiedAt: new Date().toISOString(),
                };
                await writeFile(configPath, JSON.stringify(existing, null, 2));
                return {
                    action: 'repaired',
                    path: configPath,
                    details: 'Added contributor block (ADR-029)',
                };
            }
        }
        catch {
            /* non-fatal */
        }
        return { action: 'skipped', path: configPath, details: 'Config already exists' };
    }
    const config = createDefaultConfig();
    if (isCleoContributorProject(projectRoot)) {
        config['contributor'] = {
            isContributorProject: true,
            devCli: 'cleo-dev',
            verifiedAt: new Date().toISOString(),
        };
    }
    await saveJson(configPath, config);
    return {
        action: existsSync(configPath) ? 'repaired' : 'created',
        path: configPath,
    };
}
/**
 * Create or refresh project-info.json.
 * Idempotent: skips if file already exists (unless force).
 */
export async function ensureProjectInfo(projectRoot, opts) {
    const cleoDir = getCleoDirAbsolute(projectRoot);
    const projectInfoPath = join(cleoDir, 'project-info.json');
    // Backfill projectId on existing files that lack it (T5333)
    if (existsSync(projectInfoPath) && !opts?.force) {
        try {
            const existing = JSON.parse(readFileSync(projectInfoPath, 'utf-8'));
            if (typeof existing.projectId !== 'string' || existing.projectId.length === 0) {
                existing.projectId = randomUUID();
                existing.lastUpdated = new Date().toISOString();
                await writeFile(projectInfoPath, JSON.stringify(existing, null, 2));
                return { action: 'repaired', path: projectInfoPath, details: 'Added projectId' };
            }
        }
        catch {
            // If parse fails, fall through to regenerate
        }
        return { action: 'skipped', path: projectInfoPath, details: 'Already exists' };
    }
    const projectHash = generateProjectHash(projectRoot);
    const cleoVersion = getCleoVersion();
    const now = new Date().toISOString();
    const { readSchemaVersionFromFile } = await import('./validation/schema-integrity.js');
    const { SQLITE_SCHEMA_VERSION } = await import('./store/sqlite.js');
    const configSchemaVersion = readSchemaVersionFromFile('config.schema.json') ?? cleoVersion;
    const projectContextSchemaVersion = readSchemaVersionFromFile('project-context.schema.json') ?? '1.0.0';
    const projectInfo = {
        $schema: './schemas/project-info.schema.json',
        schemaVersion: '1.0.0',
        projectId: randomUUID(),
        projectHash,
        cleoVersion,
        lastUpdated: now,
        schemas: {
            config: configSchemaVersion,
            sqlite: SQLITE_SCHEMA_VERSION,
            projectContext: projectContextSchemaVersion,
        },
        injection: {
            'CLAUDE.md': null,
            'AGENTS.md': null,
            'GEMINI.md': null,
        },
        health: {
            status: 'unknown',
            lastCheck: null,
            issues: [],
        },
        features: {
            multiSession: false,
            verification: false,
            contextAlerts: false,
        },
    };
    await writeFile(projectInfoPath, JSON.stringify(projectInfo, null, 2));
    return { action: 'created', path: projectInfoPath };
}
/**
 * Ensure .mcp.json contains a cleo-dev server entry pointing to the local build.
 * Only runs when isCleoContributorProject() is true (ADR-029).
 *
 * Writes the server entry:
 *   cleo-dev → node <projectRoot>/dist/mcp/index.js
 *
 * This ensures Claude Code loads the LOCAL dev build MCP server for this project,
 * not the published @cleocode/cleo@latest. Idempotent: preserves other entries.
 */
export async function ensureContributorMcp(projectRoot) {
    if (!isCleoContributorProject(projectRoot)) {
        return {
            action: 'skipped',
            path: join(projectRoot, '.mcp.json'),
            details: 'Not a contributor project',
        };
    }
    const mcpJsonPath = join(projectRoot, '.mcp.json');
    const mcpEntry = {
        command: 'node',
        args: ['--disable-warning=ExperimentalWarning', join(projectRoot, 'dist', 'mcp', 'index.js')],
        env: {},
    };
    let config = { mcpServers: {} };
    if (existsSync(mcpJsonPath)) {
        try {
            config = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
        }
        catch {
            /* start fresh */
        }
    }
    const servers = (config['mcpServers'] ?? {});
    const existing = servers['cleo-dev'];
    // Skip if already pointing to the same dist path
    const existingArgs = existing?.['args'];
    const targetArg = join(projectRoot, 'dist', 'mcp', 'index.js');
    if (existing && existingArgs?.includes(targetArg)) {
        return { action: 'skipped', path: mcpJsonPath, details: 'cleo-dev MCP entry already current' };
    }
    servers['cleo-dev'] = mcpEntry;
    // Remove production 'cleo' entry from project-level config — production cleo
    // is global-only. Project-level .mcp.json for contributor projects should
    // only contain cleo-dev (ADR-016 §2.3, ADR-029).
    const removedProduction = 'cleo' in servers;
    delete servers['cleo'];
    config['mcpServers'] = servers;
    await writeFile(mcpJsonPath, JSON.stringify(config, null, 2));
    const details = [
        `cleo-dev → node ${targetArg}`,
        ...(removedProduction ? ['removed production cleo entry (global-only per ADR-029)'] : []),
    ].join('; ');
    return {
        action: existing ? 'repaired' : 'created',
        path: mcpJsonPath,
        details,
    };
}
/**
 * Detect and write project-context.json.
 * Idempotent: skips if file exists and is less than staleDays old (default: 30).
 */
export async function ensureProjectContext(projectRoot, opts) {
    const cleoDir = getCleoDirAbsolute(projectRoot);
    const contextPath = join(cleoDir, 'project-context.json');
    const staleDays = opts?.staleDays ?? 30;
    if (existsSync(contextPath) && !opts?.force) {
        try {
            const content = JSON.parse(readFileSync(contextPath, 'utf-8'));
            if (content.detectedAt) {
                const detectedAt = new Date(content.detectedAt);
                const ageMs = Date.now() - detectedAt.getTime();
                const ageDays = ageMs / (1000 * 60 * 60 * 24);
                if (ageDays < staleDays) {
                    return {
                        action: 'skipped',
                        path: contextPath,
                        details: `Fresh (${Math.floor(ageDays)}d old)`,
                    };
                }
            }
        }
        catch {
            // If we can't parse it, regenerate
        }
    }
    const { detectProjectType } = await import('./store/project-detect.js');
    const context = detectProjectType(projectRoot);
    // Validate against schema before writing (best-effort, never blocks write)
    try {
        const schemaPath = join(dirname(fileURLToPath(import.meta.url)), '../schemas/project-context.schema.json');
        if (existsSync(schemaPath)) {
            const AjvModule = await import('ajv');
            const ajvMod = AjvModule;
            const AjvClass = (typeof ajvMod.default === 'function' ? ajvMod.default : AjvModule.default);
            const schema = JSON.parse(readFileSync(schemaPath, 'utf-8'));
            const ajv = new AjvClass({ strict: false });
            const valid = ajv.validate(schema, context);
            if (!valid) {
                // eslint-disable-next-line no-console
                console.warn('[CLEO] project-context.json schema validation warnings:', ajv.errors);
            }
        }
    }
    catch {
        // Schema validation is best-effort — never block the write
    }
    await writeFile(contextPath, JSON.stringify(context, null, 2));
    return {
        action: existsSync(contextPath) ? 'repaired' : 'created',
        path: contextPath,
    };
}
/**
 * Initialize isolated .cleo/.git checkpoint repository.
 * Idempotent: skips if .cleo/.git already exists.
 */
export async function ensureCleoGitRepo(projectRoot) {
    const cleoDir = getCleoDirAbsolute(projectRoot);
    const cleoGitDir = join(cleoDir, '.git');
    if (existsSync(cleoGitDir)) {
        return { action: 'skipped', path: cleoGitDir, details: 'Already initialized' };
    }
    const gitEnv = {
        ...process.env,
        GIT_DIR: cleoGitDir,
        GIT_WORK_TREE: cleoDir,
    };
    await execFileAsync('git', ['init', '--quiet'], { cwd: cleoDir, env: gitEnv });
    await execFileAsync('git', ['config', 'user.email', 'cleo@local'], { cwd: cleoDir, env: gitEnv });
    await execFileAsync('git', ['config', 'user.name', 'CLEO'], { cwd: cleoDir, env: gitEnv });
    return { action: 'created', path: cleoGitDir, details: 'Isolated checkpoint repository' };
}
/**
 * Create SQLite database if missing.
 * Idempotent: skips if tasks.db already exists.
 */
export async function ensureSqliteDb(projectRoot) {
    const cleoDir = getCleoDirAbsolute(projectRoot);
    const dbPath = join(cleoDir, 'tasks.db');
    if (existsSync(dbPath)) {
        return { action: 'skipped', path: dbPath, details: 'tasks.db already exists' };
    }
    try {
        const { getDb } = await import('./store/sqlite.js');
        await getDb(projectRoot);
        return { action: 'created', path: dbPath, details: 'SQLite database initialized' };
    }
    catch (err) {
        return {
            action: 'skipped',
            path: dbPath,
            details: `Failed to initialize SQLite: ${err instanceof Error ? err.message : String(err)}`,
        };
    }
}
// ── check* functions (read-only) ─────────────────────────────────────
/**
 * Verify all required .cleo/ subdirectories exist.
 */
export function checkCleoStructure(projectRoot) {
    const cleoDir = getCleoDirAbsolute(projectRoot);
    const missing = [];
    if (!existsSync(cleoDir)) {
        return {
            id: 'cleo_structure',
            category: 'scaffold',
            status: 'failed',
            message: '.cleo/ directory does not exist',
            details: { path: cleoDir, exists: false },
            fix: 'cleo init',
        };
    }
    for (const subdir of REQUIRED_CLEO_SUBDIRS) {
        if (!existsSync(join(cleoDir, subdir))) {
            missing.push(subdir);
        }
    }
    if (missing.length > 0) {
        return {
            id: 'cleo_structure',
            category: 'scaffold',
            status: 'warning',
            message: `Missing subdirectories: ${missing.join(', ')}`,
            details: { path: cleoDir, missing },
            fix: 'cleo init',
        };
    }
    return {
        id: 'cleo_structure',
        category: 'scaffold',
        status: 'passed',
        message: 'All required .cleo/ subdirectories exist',
        details: { path: cleoDir, subdirs: [...REQUIRED_CLEO_SUBDIRS] },
        fix: null,
    };
}
/**
 * Verify .cleo/.gitignore exists and matches template.
 */
export function checkGitignore(projectRoot) {
    const cleoDir = getCleoDirAbsolute(projectRoot);
    const gitignorePath = join(cleoDir, '.gitignore');
    if (!existsSync(gitignorePath)) {
        return {
            id: 'cleo_gitignore',
            category: 'scaffold',
            status: 'warning',
            message: '.cleo/.gitignore not found',
            details: { path: gitignorePath, exists: false },
            fix: 'cleo init --force',
        };
    }
    const installed = readFileSync(gitignorePath, 'utf-8');
    const template = getGitignoreContent();
    const normalize = (s) => s.trim().replace(/\r\n/g, '\n');
    const matches = normalize(installed) === normalize(template);
    return {
        id: 'cleo_gitignore',
        category: 'scaffold',
        status: matches ? 'passed' : 'warning',
        message: matches
            ? '.cleo/.gitignore matches template'
            : '.cleo/.gitignore has drifted from template',
        details: { path: gitignorePath, matchesTemplate: matches },
        fix: matches ? null : 'cleo upgrade',
    };
}
/**
 * Verify config.json exists and is valid JSON.
 */
export function checkConfig(projectRoot) {
    const configPath = getConfigPath(projectRoot);
    if (!existsSync(configPath)) {
        return {
            id: 'cleo_config',
            category: 'scaffold',
            status: 'failed',
            message: 'config.json not found',
            details: { path: configPath, exists: false },
            fix: 'cleo init',
        };
    }
    try {
        JSON.parse(readFileSync(configPath, 'utf-8'));
    }
    catch (err) {
        return {
            id: 'cleo_config',
            category: 'scaffold',
            status: 'failed',
            message: `config.json is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
            details: { path: configPath, valid: false },
            fix: 'cleo init --force',
        };
    }
    return {
        id: 'cleo_config',
        category: 'scaffold',
        status: 'passed',
        message: 'config.json exists and is valid JSON',
        details: { path: configPath, valid: true },
        fix: null,
    };
}
/**
 * Verify project-info.json exists with required fields.
 */
export function checkProjectInfo(projectRoot) {
    const cleoDir = getCleoDirAbsolute(projectRoot);
    const infoPath = join(cleoDir, 'project-info.json');
    if (!existsSync(infoPath)) {
        return {
            id: 'cleo_project_info',
            category: 'scaffold',
            status: 'warning',
            message: 'project-info.json not found',
            details: { path: infoPath, exists: false },
            fix: 'cleo init',
        };
    }
    try {
        const content = JSON.parse(readFileSync(infoPath, 'utf-8'));
        const requiredFields = ['projectHash', 'cleoVersion', 'lastUpdated'];
        const missing = requiredFields.filter((f) => !(f in content));
        if (missing.length > 0) {
            return {
                id: 'cleo_project_info',
                category: 'scaffold',
                status: 'warning',
                message: `project-info.json missing fields: ${missing.join(', ')}`,
                details: { path: infoPath, missingFields: missing },
                fix: 'cleo init --force',
            };
        }
        return {
            id: 'cleo_project_info',
            category: 'scaffold',
            status: 'passed',
            message: 'project-info.json exists with all required fields',
            details: { path: infoPath, valid: true },
            fix: null,
        };
    }
    catch (err) {
        return {
            id: 'cleo_project_info',
            category: 'scaffold',
            status: 'failed',
            message: `project-info.json is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
            details: { path: infoPath, valid: false },
            fix: 'cleo init --force',
        };
    }
}
/**
 * Verify project-context.json exists and is not stale (default: 30 days).
 */
export function checkProjectContext(projectRoot, staleDays = 30) {
    const cleoDir = getCleoDirAbsolute(projectRoot);
    const contextPath = join(cleoDir, 'project-context.json');
    if (!existsSync(contextPath)) {
        return {
            id: 'cleo_project_context',
            category: 'scaffold',
            status: 'warning',
            message: 'project-context.json not found',
            details: { path: contextPath, exists: false },
            fix: 'cleo init --detect',
        };
    }
    try {
        const content = JSON.parse(readFileSync(contextPath, 'utf-8'));
        if (!content.detectedAt) {
            return {
                id: 'cleo_project_context',
                category: 'scaffold',
                status: 'warning',
                message: 'project-context.json missing detectedAt timestamp',
                details: { path: contextPath, hasTimestamp: false },
                fix: 'cleo init --detect',
            };
        }
        const detectedAt = new Date(content.detectedAt);
        const ageMs = Date.now() - detectedAt.getTime();
        const ageDays = ageMs / (1000 * 60 * 60 * 24);
        if (ageDays > staleDays) {
            return {
                id: 'cleo_project_context',
                category: 'scaffold',
                status: 'warning',
                message: `project-context.json is stale (${Math.floor(ageDays)} days old, threshold: ${staleDays})`,
                details: { path: contextPath, ageDays: Math.floor(ageDays), staleDays },
                fix: 'cleo init --detect',
            };
        }
        return {
            id: 'cleo_project_context',
            category: 'scaffold',
            status: 'passed',
            message: `project-context.json is fresh (${Math.floor(ageDays)} days old)`,
            details: { path: contextPath, ageDays: Math.floor(ageDays), staleDays },
            fix: null,
        };
    }
    catch (err) {
        return {
            id: 'cleo_project_context',
            category: 'scaffold',
            status: 'failed',
            message: `project-context.json is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
            details: { path: contextPath, valid: false },
            fix: 'cleo init --detect',
        };
    }
}
/**
 * Verify .cleo/.git checkpoint repository exists.
 */
export function checkCleoGitRepo(projectRoot) {
    const cleoDir = getCleoDirAbsolute(projectRoot);
    const cleoGitDir = join(cleoDir, '.git');
    if (!existsSync(cleoGitDir)) {
        return {
            id: 'cleo_git_repo',
            category: 'scaffold',
            status: 'warning',
            message: '.cleo/.git checkpoint repository not found',
            details: { path: cleoGitDir, exists: false },
            fix: 'cleo init',
        };
    }
    return {
        id: 'cleo_git_repo',
        category: 'scaffold',
        status: 'passed',
        message: '.cleo/.git checkpoint repository exists',
        details: { path: cleoGitDir, exists: true },
        fix: null,
    };
}
/**
 * Verify .cleo/tasks.db exists and is non-empty.
 */
export function checkSqliteDb(projectRoot) {
    const cleoDir = getCleoDirAbsolute(projectRoot);
    const dbPath = join(cleoDir, 'tasks.db');
    if (!existsSync(dbPath)) {
        return {
            id: 'sqlite_db',
            category: 'scaffold',
            status: 'failed',
            message: 'tasks.db not found',
            details: { path: dbPath, exists: false },
            fix: 'cleo init',
        };
    }
    const stat = statSync(dbPath);
    if (stat.size === 0) {
        return {
            id: 'sqlite_db',
            category: 'scaffold',
            status: 'warning',
            message: 'tasks.db exists but is empty (0 bytes)',
            details: { path: dbPath, exists: true, size: 0 },
            fix: 'cleo upgrade',
        };
    }
    return {
        id: 'sqlite_db',
        category: 'scaffold',
        status: 'passed',
        message: `tasks.db exists (${stat.size} bytes)`,
        details: { path: dbPath, exists: true, size: stat.size },
        fix: null,
    };
}
/**
 * Create brain.db if missing.
 * Idempotent: skips if brain.db already exists.
 */
export async function ensureBrainDb(projectRoot) {
    const cleoDir = getCleoDirAbsolute(projectRoot);
    const dbPath = join(cleoDir, 'brain.db');
    if (existsSync(dbPath)) {
        // Ensure FTS5 tables exist even for pre-existing brain.db (T5698)
        try {
            const { getBrainNativeDb } = await import('./store/brain-sqlite.js');
            const nativeDb = getBrainNativeDb();
            if (nativeDb) {
                const { ensureFts5Tables } = await import('./memory/brain-search.js');
                ensureFts5Tables(nativeDb);
            }
        }
        catch {
            // Non-fatal — FTS5 may not be available
        }
        return { action: 'skipped', path: dbPath, details: 'brain.db already exists' };
    }
    try {
        const { getBrainDb, getBrainNativeDb } = await import('./store/brain-sqlite.js');
        await getBrainDb(projectRoot);
        // Create FTS5 virtual tables eagerly so search works immediately (T5698)
        try {
            const nativeDb = getBrainNativeDb();
            if (nativeDb) {
                const { ensureFts5Tables } = await import('./memory/brain-search.js');
                ensureFts5Tables(nativeDb);
            }
        }
        catch {
            // FTS5 may not be available in all SQLite builds — non-fatal
        }
        return { action: 'created', path: dbPath, details: 'Brain database initialized with FTS5' };
    }
    catch (err) {
        return {
            action: 'skipped',
            path: dbPath,
            details: `Failed to initialize brain.db: ${err instanceof Error ? err.message : String(err)}`,
        };
    }
}
/**
 * Verify .cleo/brain.db exists and is non-empty.
 */
export function checkBrainDb(projectRoot) {
    const cleoDir = getCleoDirAbsolute(projectRoot);
    const dbPath = join(cleoDir, 'brain.db');
    if (!existsSync(dbPath)) {
        return {
            id: 'brain_db',
            category: 'scaffold',
            status: 'failed',
            message: 'brain.db not found',
            details: { path: dbPath, exists: false },
            fix: 'cleo init',
        };
    }
    const stat = statSync(dbPath);
    if (stat.size === 0) {
        return {
            id: 'brain_db',
            category: 'scaffold',
            status: 'warning',
            message: 'brain.db exists but is empty (0 bytes)',
            details: { path: dbPath, exists: true, size: 0 },
            fix: 'cleo upgrade',
        };
    }
    return {
        id: 'brain_db',
        category: 'scaffold',
        status: 'passed',
        message: `brain.db exists (${stat.size} bytes)`,
        details: { path: dbPath, exists: true, size: stat.size },
        fix: null,
    };
}
/**
 * Verify .cleo/memory-bridge.md exists.
 * Warning level if missing (not failure) — it is auto-generated.
 */
export function checkMemoryBridge(projectRoot) {
    const cleoDir = getCleoDirAbsolute(projectRoot);
    const bridgePath = join(cleoDir, 'memory-bridge.md');
    if (!existsSync(bridgePath)) {
        return {
            id: 'memory_bridge',
            category: 'scaffold',
            status: 'warning',
            message: 'memory-bridge.md not found',
            details: { path: bridgePath, exists: false },
            fix: 'cleo init or cleo refresh-memory',
        };
    }
    return {
        id: 'memory_bridge',
        category: 'scaffold',
        status: 'passed',
        message: 'memory-bridge.md exists',
        details: { path: bridgePath, exists: true },
        fix: null,
    };
}
// ── Global (~/.cleo) scaffold functions ──────────────────────────────
/**
 * Required subdirectories under the global ~/.cleo/ home.
 * These are infrastructure directories managed by CLEO itself,
 * not project-specific data.
 *
 * Truly global:
 *   - logs      — global log output
 *   - templates — CLEO-INJECTION.md symlink target
 *
 * Note: nexus.db lives directly in ~/.cleo/, not a subdir.
 * Schemas are read at runtime from getPackageRoot()/schemas/ — no copy needed.
 * Project-level dirs (adrs/, rcasd/, agent-outputs/, backups/) live in .cleo/ only.
 */
export const REQUIRED_GLOBAL_SUBDIRS = ['logs', 'templates'];
/**
 * Stale entries that must NOT exist at the global ~/.cleo/ level.
 * These were project-level artefacts accidentally placed in the global home
 * by old versions of CLEO. They are safe to remove because:
 *   - adrs/, rcasd/, agent-outputs/, backups/ — project-level, live in .cleo/
 *   - sandbox/                                — unused
 *   - tasks.db / tasks.db-shm / tasks.db-wal  — project-level database files
 *   - brain-worker.pid                         — stale PID file
 *   - VERSION                                  — redundant (read from package.json)
 *   - schemas/                                 — now read from npm binary at runtime
 *   - bin/                                     — stale dev symlink directory
 */
export const STALE_GLOBAL_ENTRIES = [
    'adrs',
    'rcasd',
    'agent-outputs',
    'backups',
    'sandbox',
    'tasks.db',
    'tasks.db-shm',
    'tasks.db-wal',
    'brain-worker.pid',
    'VERSION',
    'schemas',
    'bin',
    '.install-state',
    'templates/templates',
];
/**
 * Ensure the global ~/.cleo/ home directory and its required
 * subdirectories exist. Idempotent: skips directories that already exist.
 *
 * Also removes known stale project-level entries that old CLEO versions
 * incorrectly placed at the global level (see STALE_GLOBAL_ENTRIES).
 *
 * This is the SSoT for global home scaffolding, replacing raw mkdirSync
 * calls that were previously scattered across global-bootstrap.ts.
 */
export async function ensureGlobalHome() {
    const cleoHome = getCleoHome();
    const alreadyExists = existsSync(cleoHome);
    await mkdir(cleoHome, { recursive: true });
    for (const subdir of REQUIRED_GLOBAL_SUBDIRS) {
        await mkdir(join(cleoHome, subdir), { recursive: true });
    }
    // Create global config.json from template if it doesn't exist
    const globalConfigPath = join(cleoHome, 'config.json');
    if (!existsSync(globalConfigPath)) {
        const templatePath = join(getPackageRoot(), 'templates', 'global-config.template.json');
        if (existsSync(templatePath)) {
            const template = readFileSync(templatePath, 'utf-8');
            const resolved = template.replace('{{SCHEMA_VERSION_GLOBAL_CONFIG}}', '1.0.0');
            await writeFile(globalConfigPath, resolved);
        }
    }
    // Remove stale project-level entries from both the current global home
    // AND the legacy ~/.cleo/ path (old CLEO versions used ~/.cleo/ before
    // env-paths moved it to the XDG-compliant location).
    const homedir = (await import('node:os')).homedir();
    const legacyCleoHome = join(homedir, '.cleo');
    const cleanupPaths = [cleoHome];
    if (legacyCleoHome !== cleoHome && existsSync(legacyCleoHome)) {
        cleanupPaths.push(legacyCleoHome);
    }
    for (const homeDir of cleanupPaths) {
        for (const stale of STALE_GLOBAL_ENTRIES) {
            const stalePath = join(homeDir, stale);
            if (existsSync(stalePath)) {
                try {
                    await rm(stalePath, { recursive: true, force: true });
                    // eslint-disable-next-line no-console
                    console.warn(`[CLEO] Removed stale global entry: ${stalePath}`);
                }
                catch (err) {
                    // eslint-disable-next-line no-console
                    console.warn(`[CLEO] Could not remove stale global entry ${stalePath}: ${err instanceof Error ? err.message : String(err)}`);
                }
            }
        }
    }
    return {
        action: alreadyExists ? 'skipped' : 'created',
        path: cleoHome,
        details: alreadyExists
            ? 'Global home already existed, ensured subdirs'
            : `Created ~/.cleo/ with ${REQUIRED_GLOBAL_SUBDIRS.length} subdirectories`,
    };
}
/**
 * Ensure the global CLEO injection template is installed.
 * Delegates to injection.ts for the template content, but owns the
 * filesystem write to maintain SSoT for scaffolding.
 *
 * Idempotent: skips if the template already exists with correct content.
 */
export async function ensureGlobalTemplates() {
    // Lazy import to avoid circular dependency (injection imports scaffold)
    const { getInjectionTemplateContent } = await import('./injection.js');
    const templatesDir = getCleoTemplatesDir();
    const injectionPath = join(templatesDir, 'CLEO-INJECTION.md');
    // Ensure directory exists (idempotent via ensureGlobalHome, but defensive)
    await mkdir(templatesDir, { recursive: true });
    const templateContent = getInjectionTemplateContent();
    if (!templateContent) {
        return {
            action: 'skipped',
            path: injectionPath,
            details: 'Bundled injection template not found; skipped',
        };
    }
    if (existsSync(injectionPath)) {
        const existing = readFileSync(injectionPath, 'utf-8');
        if (existing === templateContent) {
            return { action: 'skipped', path: injectionPath, details: 'Template already current' };
        }
        // Content differs — repair
        await writeFile(injectionPath, templateContent, 'utf-8');
        return {
            action: 'repaired',
            path: injectionPath,
            details: 'Updated injection template to match bundled version',
        };
    }
    await writeFile(injectionPath, templateContent, 'utf-8');
    return { action: 'created', path: injectionPath };
}
/**
 * Perform a complete global scaffold operation: ensure home and templates
 * are all present and current. This is the single entry point for global
 * infrastructure scaffolding.
 *
 * Schemas are NOT copied here — they are read at runtime from the npm
 * package path (getPackageRoot() + '/schemas/'). Use ensureGlobalSchemas()
 * explicitly from init or upgrade if a copy is needed for a specific workflow.
 *
 * Used by:
 *   - MCP startup (via startupHealthCheck in health.ts)
 *   - init (for first-time global setup)
 *   - upgrade (for global repair)
 */
export async function ensureGlobalScaffold() {
    const home = await ensureGlobalHome();
    const templates = await ensureGlobalTemplates();
    return { home, templates };
}
// ── Global check* functions (read-only diagnostics) ──────────────────
/**
 * Check that the global ~/.cleo/ home and its required subdirectories exist.
 * Read-only: no side effects.
 */
export function checkGlobalHome() {
    const cleoHome = getCleoHome();
    if (!existsSync(cleoHome)) {
        return {
            id: 'global_home',
            category: 'global',
            status: 'failed',
            message: 'Global ~/.cleo/ directory not found',
            details: { path: cleoHome, exists: false },
            fix: 'cleo init (or restart MCP server)',
        };
    }
    const missingDirs = REQUIRED_GLOBAL_SUBDIRS.filter((dir) => !existsSync(join(cleoHome, dir)));
    if (missingDirs.length > 0) {
        return {
            id: 'global_home',
            category: 'global',
            status: 'warning',
            message: `Global home exists but missing subdirs: ${missingDirs.join(', ')}`,
            details: { path: cleoHome, exists: true, missingDirs },
            fix: 'cleo upgrade --include-global',
        };
    }
    return {
        id: 'global_home',
        category: 'global',
        status: 'passed',
        message: 'Global ~/.cleo/ home and subdirectories present',
        details: { path: cleoHome, exists: true, subdirs: REQUIRED_GLOBAL_SUBDIRS.length },
        fix: null,
    };
}
/**
 * Check that the global injection template is present and current.
 * Read-only: no side effects.
 */
export function checkGlobalTemplates() {
    const templatesDir = getCleoTemplatesDir();
    const injectionPath = join(templatesDir, 'CLEO-INJECTION.md');
    if (!existsSync(injectionPath)) {
        return {
            id: 'global_templates',
            category: 'global',
            status: 'failed',
            message: 'CLEO-INJECTION.md template not found in global templates',
            details: { path: injectionPath, exists: false },
            fix: 'cleo init (or restart MCP server)',
        };
    }
    // Check version sync between XDG and legacy paths
    const xdgContent = readFileSync(injectionPath, 'utf-8');
    const xdgVersion = xdgContent.match(/^Version:\s*(.+)$/m)?.[1]?.trim();
    const home = getHomedir();
    const legacyPath = join(home, '.cleo', 'templates', 'CLEO-INJECTION.md');
    if (existsSync(legacyPath)) {
        const legacyContent = readFileSync(legacyPath, 'utf-8');
        const legacyVersion = legacyContent.match(/^Version:\s*(.+)$/m)?.[1]?.trim();
        if (legacyVersion && xdgVersion && legacyVersion !== xdgVersion) {
            return {
                id: 'global_templates',
                category: 'global',
                status: 'warning',
                message: `Legacy template version (${legacyVersion}) out of sync with XDG (${xdgVersion})`,
                details: { path: injectionPath, exists: true, xdgVersion, legacyVersion, legacyPath },
                fix: 'npm install -g @cleocode/cleo (reinstall syncs both paths)',
            };
        }
    }
    return {
        id: 'global_templates',
        category: 'global',
        status: 'passed',
        message: `Global injection template present (v${xdgVersion ?? 'unknown'})`,
        details: { path: injectionPath, exists: true, version: xdgVersion },
        fix: null,
    };
}
/**
 * Check that the project log directory exists.
 * Read-only: no side effects.
 */
export function checkLogDir(projectRoot) {
    const logDir = join(getCleoDirAbsolute(projectRoot), 'logs');
    if (!existsSync(logDir)) {
        return {
            id: 'log_dir',
            category: 'scaffold',
            status: 'warning',
            message: 'Log directory .cleo/logs/ not found',
            details: { path: logDir, exists: false },
            fix: 'cleo upgrade',
        };
    }
    return {
        id: 'log_dir',
        category: 'scaffold',
        status: 'passed',
        message: 'Log directory .cleo/logs/ present',
        details: { path: logDir, exists: true },
        fix: null,
    };
}
//# sourceMappingURL=scaffold.js.map