/**
 * 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, copyFile, mkdir, readdir, 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 { getCleoCantWorkflowsDir, getCleoDirAbsolute, getCleoGlobalAgentsDir, getCleoGlobalRecipesDir, getCleoHome, getCleoPiExtensionsDir, 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).
 *
 * Must stay in sync with `packages/core/templates/cleo-gitignore`. The
 * template file is the source of truth; this constant is only used when
 * the template file cannot be located at runtime (bundled binary edge
 * cases, air-gapped installs, etc.).
 *
 * ADR-013 §9 (2026-04-07, T5158): config.json / project-info.json are
 * NOT re-included here. Runtime state files (tasks.db, brain.db,
 * config.json, project-info.json) are protected by explicit deny rules
 * so that nested-.gitignore allow rules cannot unignore them at the
 * project repository level.
 */
export const CLEO_GITIGNORE_FALLBACK = `# .cleo/.gitignore — Deny-by-default for CLEO project data
# Ignore everything, then explicitly allow only tracked files.
#
# ADR-013 §9 (2026-04-07, T5158): config.json + project-info.json are
# runtime snapshots regenerated by \`cleo init\`, not tracked in git.
# Recovery for all four runtime files (tasks.db, brain.db, config.json,
# project-info.json) is provided by \`cleo backup add\` snapshots under
# .cleo/backups/. See .cleo/adrs/ADR-013 for the full recovery story.

# Step 1: Ignore everything
*

# Allow list
!.gitignore
!project-context.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
config.json
project-info.json
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.
 *
 * @param path - Absolute path to the file to check
 * @returns True if the file exists and is readable, false otherwise
 *
 * @remarks
 * Uses `fs.access` with `R_OK` to verify both existence and read permission.
 * Swallows all errors and returns false on failure.
 *
 * @example
 * ```typescript
 * if (await fileExists('/project/.cleo/config.json')) {
 *   // safe to read
 * }
 * ```
 */
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.
 *
 * @param filePath - Absolute path to the file to strip
 *
 * @remarks
 * Handles both bare markers (`<!-- CLEO:START -->`) and versioned markers
 * (`<!-- CLEO:START v0.53.4 -->`). No-op if the file does not exist or
 * contains no CLEO blocks.
 *
 * @example
 * ```typescript
 * await stripCLEOBlocks('/project/CLAUDE.md');
 * ```
 */
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.
 *
 * @param projectRoot - Absolute path to the project root directory
 * @returns Whether any lines were removed from the .gitignore
 *
 * @remarks
 * Filters out lines matching `/.cleo/`, `.cleo/`, `.cleo`, etc. from the
 * root-level `.gitignore`. Returns `{ removed: false }` if the file does not
 * exist or contains no matching entries.
 *
 * @example
 * ```typescript
 * const { removed } = await removeCleoFromRootGitignore('/project');
 * if (removed) console.log('.cleo entries cleaned from .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.
 *
 * @returns Absolute path to the @cleocode/core package root
 *
 * @remarks
 * Uses `import.meta.url` to determine the file location at runtime, which
 * works correctly in both development and installed npm package contexts.
 *
 * @example
 * ```typescript
 * const root = getPackageRoot();
 * const schemaPath = join(root, 'schemas', 'config.schema.json');
 * ```
 */
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.
 *
 * @returns The .cleo/.gitignore template content string
 *
 * @remarks
 * First attempts to read from the package's `templates/cleo-gitignore` file.
 * Falls back to the embedded `CLEO_GITIGNORE_FALLBACK` constant if the
 * template file is missing or unreadable.
 *
 * @example
 * ```typescript
 * const content = getGitignoreContent();
 * await writeFile('.cleo/.gitignore', content);
 * ```
 */
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.
 *
 * @returns Semver version string, or "0.0.0" if unavailable
 *
 * @remarks
 * Reads the `version` field from the @cleocode/core package.json at runtime.
 * Returns "0.0.0" if the file cannot be read or parsed.
 *
 * @example
 * ```typescript
 * const version = getCleoVersion(); // "2026.4.0"
 * ```
 */
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';
    }
}
/**
 * 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/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;
    }
}
/**
 * Create default config.json content.
 *
 * @returns A plain object with the default CLEO configuration structure
 *
 * @remarks
 * Returns a version-stamped config with defaults for output formatting, backup
 * limits, hierarchy depth, session behavior, and lifecycle mode.
 *
 * @example
 * ```typescript
 * const config = createDefaultConfig();
 * await saveJson(configPath, config);
 * ```
 */
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.
 *
 * @param projectRoot - Absolute path to the project root directory
 * @returns Scaffold result indicating whether the directory was created or already existed
 *
 * @remarks
 * Refuses to scaffold inside the global CLEO home to prevent pollution.
 * Creates all directories listed in `REQUIRED_CLEO_SUBDIRS`.
 *
 * @example
 * ```typescript
 * const result = await ensureCleoStructure('/my/project');
 * console.log(result.action); // "created" or "skipped"
 * ```
 */
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.
 *
 * @param projectRoot - Absolute path to the project root directory
 * @returns Scaffold result indicating whether the gitignore was created, repaired, or skipped
 *
 * @remarks
 * Compares normalized content (trimmed, LF line endings) against the template.
 * If the file exists but differs, it is overwritten with the canonical template.
 *
 * @example
 * ```typescript
 * const result = await ensureGitignore('/my/project');
 * if (result.action === 'repaired') console.log('Gitignore updated');
 * ```
 */
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.
 *
 * @param projectRoot - Absolute path to the project root directory
 * @param opts - Optional configuration
 * @param opts.force - When true, overwrite the existing config
 * @returns Scaffold result indicating the action taken
 *
 * @remarks
 * On skip, still checks for and backfills the ADR-029 contributor block
 * if the project is detected as the CLEO source repository.
 *
 * @example
 * ```typescript
 * const result = await ensureConfig('/my/project', { force: true });
 * console.log(result.path);
 * ```
 */
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).
 *
 * @param projectRoot - Absolute path to the project root directory
 * @param opts - Optional configuration
 * @param opts.force - When true, regenerate even if the file exists
 * @returns Scaffold result indicating the action taken
 *
 * @remarks
 * Backfills a `projectId` (UUID) on existing files that lack one (T5333).
 * The generated file includes project hash, CLEO version, schema versions,
 * and initial health/feature flags.
 *
 * @example
 * ```typescript
 * const result = await ensureProjectInfo('/my/project');
 * console.log(result.action); // "created", "repaired", or "skipped"
 * ```
 */
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 };
}
/**
 * No-op. Kept for API compatibility.
 *
 * @param projectRoot - Absolute path to the project root directory
 * @returns Scaffold result with action "skipped"
 *
 * @remarks
 * This function was removed in Phase 2 production readiness but the export
 * is preserved to avoid breaking downstream callers.
 *
 * @example
 * ```typescript
 * const result = await ensureContributorMcp('/my/project');
 * // result.action === 'skipped'
 * ```
 */
export async function ensureContributorMcp(projectRoot) {
    return {
        action: 'skipped',
        path: projectRoot,
        details: 'Removed (Phase 2 production readiness)',
    };
}
/**
 * Detect and write project-context.json.
 * Idempotent: skips if file exists and is less than staleDays old (default: 30).
 *
 * @param projectRoot - Absolute path to the project root directory
 * @param opts - Optional configuration
 * @param opts.force - When true, regenerate even if the file is fresh
 * @param opts.staleDays - Age threshold in days before regeneration (default: 30)
 * @returns Scaffold result indicating the action taken
 *
 * @remarks
 * Runs project type detection (Node, Python, Rust, etc.) and writes the result.
 * Validates against the project-context schema before writing (best-effort).
 *
 * @example
 * ```typescript
 * const result = await ensureProjectContext('/my/project', { staleDays: 7 });
 * if (result.action === 'repaired') console.log('Context refreshed');
 * ```
 */
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.
 *
 * @param projectRoot - Absolute path to the project root directory
 * @returns Scaffold result indicating the action taken
 *
 * @remarks
 * Creates an isolated git repository inside .cleo/ for checkpoint tracking.
 * The repo uses `GIT_DIR` and `GIT_WORK_TREE` environment variables to avoid
 * interfering with the project's own git repository.
 *
 * @example
 * ```typescript
 * const result = await ensureCleoGitRepo('/my/project');
 * console.log(result.action); // "created" or "skipped"
 * ```
 */
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.
 *
 * @param projectRoot - Absolute path to the project root directory
 * @returns Scaffold result indicating the action taken
 *
 * @remarks
 * Initializes the tasks.db SQLite database by calling `getDb()` which runs
 * schema migrations. Returns a skipped result with error details if initialization fails.
 *
 * @example
 * ```typescript
 * const result = await ensureSqliteDb('/my/project');
 * if (result.action === 'created') console.log('Database ready');
 * ```
 */
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.
 *
 * @param projectRoot - Absolute path to the project root directory
 * @returns Check result with status and list of any missing subdirectories
 *
 * @remarks
 * Read-only diagnostic. Checks for the .cleo/ directory and all entries
 * in `REQUIRED_CLEO_SUBDIRS`. Reports "failed" if .cleo/ is missing,
 * "warning" if subdirectories are missing, "passed" otherwise.
 *
 * @example
 * ```typescript
 * const check = checkCleoStructure('/my/project');
 * if (check.status === 'failed') console.log(check.fix);
 * ```
 */
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.
 *
 * @param projectRoot - Absolute path to the project root directory
 * @returns Check result indicating whether the gitignore matches the template
 *
 * @remarks
 * Read-only diagnostic. Normalizes whitespace before comparison.
 * Reports "warning" if the file is missing or drifted from the template.
 *
 * @example
 * ```typescript
 * const check = checkGitignore('/my/project');
 * if (check.status === 'warning') console.log('Gitignore drifted');
 * ```
 */
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.
 *
 * @param projectRoot - Absolute path to the project root directory
 * @returns Check result indicating whether config.json is present and valid
 *
 * @remarks
 * Read-only diagnostic. Reports "failed" if the file is missing or contains
 * invalid JSON. Does not validate config schema, only JSON syntax.
 *
 * @example
 * ```typescript
 * const check = checkConfig('/my/project');
 * if (check.status === 'passed') console.log('Config OK');
 * ```
 */
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.
 *
 * @param projectRoot - Absolute path to the project root directory
 * @returns Check result indicating whether project-info.json is valid
 *
 * @remarks
 * Read-only diagnostic. Checks for presence and required fields: projectHash,
 * cleoVersion, and lastUpdated. Reports "warning" on missing fields, "failed" on
 * missing file or invalid JSON.
 *
 * @example
 * ```typescript
 * const check = checkProjectInfo('/my/project');
 * if (check.status !== 'passed') console.log(check.fix);
 * ```
 */
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).
 *
 * @param projectRoot - Absolute path to the project root directory
 * @param staleDays - Age threshold in days before reporting as stale (default: 30)
 * @returns Check result with freshness assessment
 *
 * @remarks
 * Read-only diagnostic. Checks for the `detectedAt` timestamp and compares
 * its age against the staleness threshold. Reports "warning" if stale or
 * missing, "failed" if the file contains invalid JSON.
 *
 * @example
 * ```typescript
 * const check = checkProjectContext('/my/project', 14);
 * if (check.status === 'warning') console.log('Context is stale');
 * ```
 */
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.
 *
 * @param projectRoot - Absolute path to the project root directory
 * @returns Check result indicating whether the checkpoint repo is present
 *
 * @remarks
 * Read-only diagnostic. Reports "warning" if the .cleo/.git directory is missing.
 *
 * @example
 * ```typescript
 * const check = checkCleoGitRepo('/my/project');
 * console.log(check.status); // "passed" or "warning"
 * ```
 */
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.
 *
 * @param projectRoot - Absolute path to the project root directory
 * @returns Check result with database existence and size information
 *
 * @remarks
 * Read-only diagnostic. Reports "failed" if the file is missing, "warning" if
 * the file exists but is 0 bytes, "passed" otherwise.
 *
 * @example
 * ```typescript
 * const check = checkSqliteDb('/my/project');
 * if (check.status === 'failed') console.log('No database:', check.fix);
 * ```
 */
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.
 *
 * @param projectRoot - Absolute path to the project root directory
 * @returns Scaffold result indicating the action taken
 *
 * @remarks
 * Initializes brain.db with Drizzle schema and eagerly creates FTS5 virtual
 * tables for full-text search (T5698). Also ensures FTS5 tables on pre-existing
 * databases that may lack them.
 *
 * @example
 * ```typescript
 * const result = await ensureBrainDb('/my/project');
 * if (result.action === 'created') console.log('Brain database ready');
 * ```
 */
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.
 *
 * @param projectRoot - Absolute path to the project root directory
 * @returns Check result with database existence and size information
 *
 * @remarks
 * Read-only diagnostic. Reports "failed" if the file is missing, "warning" if
 * the file exists but is 0 bytes, "passed" otherwise.
 *
 * @example
 * ```typescript
 * const check = checkBrainDb('/my/project');
 * console.log(check.status);
 * ```
 */
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.
 *
 * @param projectRoot - Absolute path to the project root directory
 * @returns Check result indicating presence of the memory bridge file
 *
 * @remarks
 * Read-only diagnostic. The memory bridge is auto-generated from brain.db
 * content, so its absence is only a warning (not a failure).
 *
 * @example
 * ```typescript
 * const check = checkMemoryBridge('/my/project');
 * if (check.status === 'warning') console.log('Run: cleo refresh-memory');
 * ```
 */
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,
    };
}
/**
 * Verify .cleo/nexus-bridge.md exists.
 * Warning level if missing (not failure) — it is auto-generated after `cleo nexus analyze`.
 *
 * @param projectRoot - Absolute path to the project root directory
 * @returns Check result indicating presence of the nexus bridge file
 *
 * @remarks
 * Read-only diagnostic. The nexus bridge is auto-generated from nexus.db
 * content after `cleo nexus analyze` runs, so its absence is only a warning.
 *
 * @example
 * ```typescript
 * const check = checkNexusBridge('/my/project');
 * if (check.status === 'warning') console.log('Run: cleo nexus analyze');
 * ```
 */
export function checkNexusBridge(projectRoot) {
    const cleoDir = getCleoDirAbsolute(projectRoot);
    const bridgePath = join(cleoDir, 'nexus-bridge.md');
    if (!existsSync(bridgePath)) {
        return {
            id: 'nexus_bridge',
            category: 'scaffold',
            status: 'warning',
            message: 'nexus-bridge.md not found',
            details: { path: bridgePath, exists: false },
            fix: 'cleo nexus analyze',
        };
    }
    return {
        id: 'nexus_bridge',
        category: 'scaffold',
        status: 'passed',
        message: 'nexus-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',
    // CleoOS hub (Phase 1): cross-project recipe library, Pi extensions, CANT workflows
    'global-recipes',
    'pi-extensions',
    'cant-workflows',
    'agents',
];
/**
 * 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.
 *
 * @returns Scaffold result indicating the action taken
 *
 * @remarks
 * Also creates a global config.json from template if absent. Cleans up
 * stale entries from both the current and legacy (~/.cleo/) global home paths.
 *
 * @example
 * ```typescript
 * const result = await ensureGlobalHome();
 * console.log(result.action); // "created" or "skipped"
 * ```
 */
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.
 *
 * @returns Scaffold result indicating the action taken
 *
 * @remarks
 * Lazy-imports injection.ts to avoid circular dependencies. Compares file
 * content byte-for-byte; repairs if the installed version differs from the
 * bundled template.
 *
 * @example
 * ```typescript
 * const result = await ensureGlobalTemplates();
 * if (result.action === 'repaired') console.log('Template updated');
 * ```
 */
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:
 *   - CLI startup (via startupHealthCheck in health.ts)
 *   - init (for first-time global setup)
 *   - upgrade (for global repair)
 *
 * @returns Combined scaffold results for home and templates
 *
 * @remarks
 * Calls ensureGlobalHome and ensureGlobalTemplates in sequence.
 * Schemas are NOT copied here -- they are read at runtime from the npm
 * package path.
 *
 * @example
 * ```typescript
 * const { home, templates } = await ensureGlobalScaffold();
 * console.log(home.action, templates.action);
 * ```
 */
export async function ensureGlobalScaffold() {
    const home = await ensureGlobalHome();
    const templates = await ensureGlobalTemplates();
    const cleoosHub = await ensureCleoOsHub();
    return { home, templates, cleoosHub };
}
// ── CleoOS Hub scaffolding (Phase 1) ─────────────────────────────────
/**
 * Recursively copy a template tree from {@link srcDir} into {@link dstDir}.
 *
 * Never overwrites existing files: any file that already exists at the
 * destination is left untouched, preserving user/agent edits. Missing
 * subdirectories are created on demand. Symbolic links and special files
 * are skipped silently.
 *
 * @param srcDir - Absolute path to the template source directory
 * @param dstDir - Absolute path to the destination directory
 * @returns Counts of files copied vs files that already existed and were kept
 *
 * @remarks
 * Returns `{ copied: 0, kept: 0 }` if the source directory does not exist.
 * Designed as the workhorse for {@link ensureCleoOsHub} and any other
 * idempotent template scaffolding that ships with the npm package.
 *
 * @example
 * ```typescript
 * const { copied, kept } = await copyTemplateTree(
 *   '/usr/lib/node_modules/@cleocode/cleo/templates/cleoos-hub/pi-extensions',
 *   '/home/me/.local/share/cleo/pi-extensions',
 * );
 * console.log(`copied ${copied}, kept ${kept}`);
 * ```
 */
async function copyTemplateTree(srcDir, dstDir) {
    if (!existsSync(srcDir)) {
        return { copied: 0, kept: 0 };
    }
    await mkdir(dstDir, { recursive: true });
    let copied = 0;
    let kept = 0;
    let entries;
    try {
        entries = await readdir(srcDir, { withFileTypes: true });
    }
    catch (err) {
        // eslint-disable-next-line no-console
        console.warn(`[CLEO] Could not read template dir ${srcDir}: ${err instanceof Error ? err.message : String(err)}`);
        return { copied: 0, kept: 0 };
    }
    for (const entry of entries) {
        const srcPath = join(srcDir, entry.name);
        const dstPath = join(dstDir, entry.name);
        if (entry.isDirectory()) {
            const sub = await copyTemplateTree(srcPath, dstPath);
            copied += sub.copied;
            kept += sub.kept;
            continue;
        }
        if (!entry.isFile()) {
            // Skip symlinks, sockets, devices, etc.
            continue;
        }
        if (existsSync(dstPath)) {
            kept += 1;
            continue;
        }
        try {
            await copyFile(srcPath, dstPath);
            copied += 1;
        }
        catch (err) {
            // eslint-disable-next-line no-console
            console.warn(`[CLEO] Could not copy template file ${srcPath} -> ${dstPath}: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    return { copied, kept };
}
/**
 * Resolve the absolute root directory of the bundled CleoOS hub templates.
 *
 * The templates ship inside the `@cleocode/cleo` package at
 * `templates/cleoos-hub/`. Because `scaffold.ts` is authored in
 * `@cleocode/core` but is also esbuilt directly into the `@cleocode/cleo`
 * CLI bundle, the runtime location of `import.meta.url` (and therefore
 * {@link getPackageRoot}) varies across deployment shapes. The candidate
 * list below covers every layout we ship:
 *
 *   1. **Bundled cleo (npm install)** — `dist/cli/index.js` lives at
 *      `node_modules/@cleocode/cleo/dist/cli/index.js`, so
 *      `getPackageRoot()` resolves to `node_modules/@cleocode/cleo/dist`
 *      and `../templates/cleoos-hub` reaches the package's templates dir.
 *   2. **Bundled cleo (dev workspace via build.mjs)** — `dist/cli/index.js`
 *      lives at `packages/cleo/dist/cli/index.js`; same `../templates/...`
 *      candidate as above.
 *   3. **Unbundled core (workspace tsc)** — scaffold.ts is loaded as
 *      `packages/core/src/scaffold.ts`, so `getPackageRoot()` resolves to
 *      `packages/core/` and `../cleo/templates/cleoos-hub` reaches the
 *      sibling cleo package.
 *   4. **Unbundled core (npm install of just @cleocode/core)** — the
 *      scope-sibling layout puts both packages under
 *      `node_modules/@cleocode/`, so the same `../cleo/templates/...`
 *      candidate works.
 *   5. **Legacy fallback** — if the cleo package ever bundles the templates
 *      directly under core, fall through to
 *      `getPackageRoot()/templates/cleoos-hub`.
 *
 * @returns Absolute path to the existing template root, or `null` if none found
 *
 * @remarks
 * Returning `null` allows callers to skip the scaffold gracefully instead of
 * crashing — important for the never-crash scaffold contract.
 */
function resolveCleoOsHubTemplateRoot() {
    const packageRoot = getPackageRoot();
    const candidates = [
        // Bundled cleo: getPackageRoot() = .../@cleocode/cleo/dist
        join(packageRoot, '..', 'templates', 'cleoos-hub'),
        // Unbundled core in workspace or scope-sibling install
        join(packageRoot, '..', 'cleo', 'templates', 'cleoos-hub'),
        // Workspace from monorepo root resolution (defensive)
        join(packageRoot, '..', '..', 'packages', 'cleo', 'templates', 'cleoos-hub'),
        // Legacy in-place fallback
        join(packageRoot, 'templates', 'cleoos-hub'),
    ];
    return candidates.find((p) => existsSync(p)) ?? null;
}
/**
 * Ensure the CleoOS Hub subdirectories exist under the global CLEO home,
 * and seed all bundled hub templates if they are not already present.
 *
 * This is the Phase 1 scaffolding entry point. Idempotent: re-running is
 * safe and will never overwrite a file that already exists — human and
 * agent edits to the installed copies are always preserved.
 *
 * @returns Scaffold result for the CleoOS hub root
 *
 * @remarks
 * The CleoOS hub currently consists of:
 *   - `global-recipes/` (Justfile Hub: justfile + README)
 *   - `pi-extensions/`  (Pi extensions: orchestrator, stage-guide, cant-bridge)
 *   - `cant-workflows/` (CANT workflow library — created empty for agents)
 *   - `agents/`         (Global CANT agent definitions — created empty)
 *
 * Templates are sourced from the `@cleocode/cleo` package's bundled
 * `templates/cleoos-hub/` tree. The directory is resolved at runtime through
 * {@link resolveCleoOsHubTemplateRoot} so the same code path works in the
 * monorepo workspace and in an installed npm package layout.
 *
 * @example
 * ```typescript
 * const result = await ensureCleoOsHub();
 * console.log(result.action); // "created" or "skipped"
 * ```
 */
export async function ensureCleoOsHub() {
    const recipesDir = getCleoGlobalRecipesDir();
    const piExtDir = getCleoPiExtensionsDir();
    const cantWorkflowsDir = getCleoCantWorkflowsDir();
    const agentsDir = getCleoGlobalAgentsDir();
    // Always make sure the hub directory skeleton exists, even if templates
    // can't be located (so downstream tools writing into them don't ENOENT).
    try {
        await mkdir(recipesDir, { recursive: true });
        await mkdir(piExtDir, { recursive: true });
        await mkdir(cantWorkflowsDir, { recursive: true });
        await mkdir(agentsDir, { recursive: true });
    }
    catch (err) {
        return {
            action: 'skipped',
            path: recipesDir,
            details: `Failed to create CleoOS hub directories: ${err instanceof Error ? err.message : String(err)}`,
        };
    }
    const templateRoot = resolveCleoOsHubTemplateRoot();
    if (!templateRoot) {
        return {
            action: 'skipped',
            path: recipesDir,
            details: 'CleoOS hub template directory not found in any expected location',
        };
    }
    let piResult;
    let recipesResult;
    try {
        piResult = await copyTemplateTree(join(templateRoot, 'pi-extensions'), piExtDir);
        recipesResult = await copyTemplateTree(join(templateRoot, 'global-recipes'), recipesDir);
    }
    catch (err) {
        return {
            action: 'skipped',
            path: recipesDir,
            details: `Failed to seed CleoOS hub templates: ${err instanceof Error ? err.message : String(err)}`,
        };
    }
    const totalCopied = piResult.copied + recipesResult.copied;
    return {
        action: totalCopied > 0 ? 'created' : 'skipped',
        path: recipesDir,
        details: `pi-extensions: ${piResult.copied} created/${piResult.kept} kept, global-recipes: ${recipesResult.copied} created/${recipesResult.kept} kept`,
    };
}
// ── Global check* functions (read-only diagnostics) ──────────────────
/**
 * Check that the global ~/.cleo/ home and its required subdirectories exist.
 * Read-only: no side effects.
 *
 * @returns Check result with status and missing subdirectory details
 *
 * @remarks
 * Reports "failed" if the home directory itself is absent, "warning" if
 * subdirectories are missing, "passed" if all infrastructure is present.
 *
 * @example
 * ```typescript
 * const check = checkGlobalHome();
 * if (check.status !== 'passed') console.log(check.fix);
 * ```
 */
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',
        };
    }
    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.
 *
 * @returns Check result with template version and sync status
 *
 * @remarks
 * Also checks for version drift between the XDG-compliant path and the
 * legacy ~/.cleo/templates/ path. Reports "warning" if versions differ.
 *
 * @example
 * ```typescript
 * const check = checkGlobalTemplates();
 * console.log(check.details.version);
 * ```
 */
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',
        };
    }
    // 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.
 *
 * @param projectRoot - Absolute path to the project root directory
 * @returns Check result indicating whether .cleo/logs/ is present
 *
 * @remarks
 * Reports "warning" if the log directory is absent, "passed" otherwise.
 *
 * @example
 * ```typescript
 * const check = checkLogDir('/my/project');
 * console.log(check.status);
 * ```
 */
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