import fg from 'fast-glob';
import path from 'node:path';
import { existsSync } from 'node:fs';
import { readFile, stat } from 'node:fs/promises';
import { CODE_EXTENSIONS, LANGUAGE_SPECS } from '../code/languages.js';
import { DEFAULT_EXCLUDE_DIRS, OFFICE_EXTENSIONS, buildExcludeDirGlobs, compareCodeUnits, createIgnoreMatcher, getExtension, normalizeRelativePath, } from '../utils.js';
const CATEGORIES = ['code', 'office', 'prose', 'data', 'config', 'image', 'binary', 'other'];
const PROSE_EXTENSIONS = new Set(['md', 'markdown', 'rst', 'txt', 'adoc']);
const DATA_EXTENSIONS = new Set(['json', 'yaml', 'yml', 'toml', 'csv', 'tsv', 'parquet', 'ipynb', 'xml']);
const IMAGE_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico']);
const BINARY_EXTENSIONS = new Set(['exe', 'dll', 'so', 'dylib', 'zip', 'tar', 'gz', 'tgz', '7z']);
const CODE_EXTENSION_SET = new Set(CODE_EXTENSIONS);
const OFFICE_EXTENSION_SET = new Set(OFFICE_EXTENSIONS);
const CONFIG_FILENAMES = [
    '.env',
    '.gitignore',
    '.npmrc',
    '.nvmrc',
    '.eslintrc',
    '.prettierrc',
    'Makefile',
    'Dockerfile',
    'package.json',
    'package-lock.json',
    'pnpm-lock.yaml',
    'pnpm-workspace.yaml',
    'yarn.lock',
    'bun.lock',
    'bun.lockb',
    'deno.lock',
    'tsconfig.json',
    'pyproject.toml',
    'requirements.txt',
    'setup.py',
    'Pipfile',
    'go.mod',
    'Cargo.toml',
    'pom.xml',
    'build.gradle',
    'build.gradle.kts',
    'Gemfile',
    'composer.json',
    'mix.exs',
    'mkdocs.yml',
    '_config.yml',
    'hugo.toml',
    'CMakeLists.txt',
];
const CONFIG_PREFIXES = ['.env.', '.eslintrc.', '.prettierrc.', 'tsconfig.'];
const MANIFEST_RULES = [
    { file: 'package.json', kind: 'node', label: 'Node.js package', ecosystem: 'node', codeEcosystem: true },
    {
        file: 'pnpm-workspace.yaml',
        kind: 'node-monorepo',
        label: 'pnpm workspace',
        ecosystem: 'node',
        codeEcosystem: true,
    },
    {
        file: 'tsconfig.json',
        kind: 'typescript',
        label: 'TypeScript config',
        ecosystem: 'typescript',
        codeEcosystem: true,
    },
    { file: 'pyproject.toml', kind: 'python', label: 'Python project', ecosystem: 'python', codeEcosystem: true },
    { file: 'requirements.txt', kind: 'python', label: 'Python requirements', ecosystem: 'python', codeEcosystem: true },
    { file: 'setup.py', kind: 'python', label: 'Python setup', ecosystem: 'python', codeEcosystem: true },
    { file: 'Pipfile', kind: 'python', label: 'Pipenv project', ecosystem: 'python', codeEcosystem: true },
    { file: 'go.mod', kind: 'go', label: 'Go module', ecosystem: 'go', codeEcosystem: true },
    { file: 'Cargo.toml', kind: 'rust', label: 'Rust crate', ecosystem: 'rust', codeEcosystem: true },
    { file: 'pom.xml', kind: 'java', label: 'Maven project', ecosystem: 'java', codeEcosystem: true },
    { file: 'build.gradle', kind: 'java', label: 'Gradle project', ecosystem: 'java', codeEcosystem: true },
    { file: 'build.gradle.kts', kind: 'java', label: 'Gradle Kotlin project', ecosystem: 'java', codeEcosystem: true },
    { file: 'Gemfile', kind: 'ruby', label: 'Ruby bundle', ecosystem: 'ruby', codeEcosystem: true },
    { file: 'composer.json', kind: 'php', label: 'Composer package', ecosystem: 'php', codeEcosystem: true },
    { file: 'mix.exs', kind: 'elixir', label: 'Elixir Mix project', ecosystem: 'elixir', codeEcosystem: true },
    { file: 'Dockerfile', kind: 'docker', label: 'Docker project' },
    { file: 'mkdocs.yml', kind: 'mkdocs', label: 'MkDocs site', siteGenerator: 'MkDocs' },
    { file: '_config.yml', kind: 'jekyll', label: 'Jekyll site', siteGenerator: 'Jekyll' },
    { file: 'hugo.toml', kind: 'hugo', label: 'Hugo site', siteGenerator: 'Hugo' },
];
// Marker kinds that indicate a code manifest. Includes the synthesized `.sln`
// (dotnet) rule, which is not part of MANIFEST_RULES because its filename varies.
const CODE_ECOSYSTEM_KINDS = new Set([
    ...MANIFEST_RULES.filter(rule => rule.codeEcosystem).map(rule => rule.kind),
    'dotnet',
]);
const NODE_FRAMEWORKS = {
    next: 'next',
    react: 'react',
    vue: 'vue',
    svelte: 'svelte',
    '@angular/core': 'angular',
    '@nestjs/core': 'nest',
    express: 'express',
    fastify: 'fastify',
    vite: 'vite',
    astro: 'astro',
    electron: 'electron',
};
const NODE_TEST_FRAMEWORKS = ['vitest', 'jest', 'mocha', 'ava', 'tap', 'playwright', 'cypress'];
const PYTHON_FRAMEWORKS = ['django', 'flask', 'fastapi', 'streamlit', 'jupyter'];
const RUBY_FRAMEWORKS = ['rails', 'sinatra'];
const PHP_FRAMEWORKS = {
    'laravel/framework': 'laravel',
    'symfony/framework-bundle': 'symfony',
    'symfony/symfony': 'symfony',
};
const LOCKFILE_PACKAGE_MANAGERS = {
    'package-lock.json': 'npm',
    'npm-shrinkwrap.json': 'npm',
    'pnpm-lock.yaml': 'pnpm',
    'yarn.lock': 'yarn',
    'bun.lock': 'bun',
    'bun.lockb': 'bun',
    'deno.lock': 'deno',
};
const LANGUAGE_BY_EXTENSION = new Map();
for (const spec of LANGUAGE_SPECS) {
    for (const extension of spec.extensions) {
        LANGUAGE_BY_EXTENSION.set(extension, spec.name);
    }
}
function emptyCategoryBreakdown() {
    return Object.fromEntries(CATEGORIES.map(category => [category, { files: 0, bytes: 0, pct: 0 }]));
}
function categoryForPath(relativePath) {
    const baseName = path.basename(relativePath);
    const ext = getExtension(relativePath);
    if (CONFIG_FILENAMES.includes(baseName) ||
        CONFIG_PREFIXES.some(prefix => baseName.startsWith(prefix)) ||
        baseName.endsWith('.sln')) {
        return 'config';
    }
    if (CODE_EXTENSION_SET.has(ext))
        return 'code';
    if (OFFICE_EXTENSION_SET.has(ext))
        return 'office';
    if (PROSE_EXTENSIONS.has(ext))
        return 'prose';
    if (DATA_EXTENSIONS.has(ext))
        return 'data';
    if (IMAGE_EXTENSIONS.has(ext))
        return 'image';
    if (BINARY_EXTENSIONS.has(ext))
        return 'binary';
    return 'other';
}
function markerRuleForPath(relativePath) {
    const baseName = path.basename(relativePath);
    if (baseName.endsWith('.sln')) {
        return { file: baseName, kind: 'dotnet', label: '.NET solution', ecosystem: 'dotnet', codeEcosystem: true };
    }
    return MANIFEST_RULES.find(rule => rule.file === baseName);
}
function projectRootForMarker(relativePath) {
    const dir = normalizeRelativePath(path.dirname(relativePath));
    return dir === '.' ? '.' : dir;
}
function titleCase(value) {
    if (value === 'javascript')
        return 'JavaScript';
    if (value === 'typescript')
        return 'TypeScript';
    if (value === 'dotnet')
        return '.NET';
    return value.slice(0, 1).toUpperCase() + value.slice(1);
}
function dedupe(values) {
    return [...new Set(values.filter(Boolean))];
}
function sortByCountDesc(entries) {
    return entries.sort((left, right) => right.files - left.files || compareCodeUnits(left.ext, right.ext));
}
function dominantLanguage(files) {
    const counts = new Map();
    for (const file of files) {
        if (file.category !== 'code')
            continue;
        const language = LANGUAGE_BY_EXTENSION.get(file.ext) ?? file.ext;
        counts.set(language, (counts.get(language) ?? 0) + 1);
    }
    const [top] = [...counts.entries()].sort((left, right) => right[1] - left[1] || compareCodeUnits(left[0], right[0]));
    return top?.[0];
}
function dominantOfficeExtension(files) {
    const counts = new Map();
    for (const file of files) {
        if (file.category !== 'office')
            continue;
        counts.set(file.ext, (counts.get(file.ext) ?? 0) + 1);
    }
    const [top] = [...counts.entries()].sort((left, right) => right[1] - left[1] || compareCodeUnits(left[0], right[0]));
    return top?.[0];
}
function packageManagerFromPackageJson(value) {
    if (!value || typeof value !== 'object')
        return undefined;
    const record = value;
    const packageManager = record.packageManager;
    if (typeof packageManager === 'string' && packageManager.length > 0) {
        const name = packageManager.split('@')[0];
        if (name)
            return name;
    }
    const devEngines = record.devEngines;
    if (devEngines && typeof devEngines === 'object') {
        const packageManagerConfig = devEngines.packageManager;
        if (packageManagerConfig && typeof packageManagerConfig === 'object') {
            const name = packageManagerConfig.name;
            if (typeof name === 'string' && name.length > 0)
                return name;
        }
    }
    return undefined;
}
function dependencyKeys(value) {
    if (!value || typeof value !== 'object')
        return [];
    return Object.keys(value);
}
function recordKeys(value) {
    if (!value || typeof value !== 'object')
        return [];
    return Object.keys(value);
}
function collectPackageEntryPoints(parsed) {
    const points = [];
    for (const field of ['main', 'module', 'types', 'typings', 'browser']) {
        const value = parsed[field];
        if (typeof value === 'string' && value.length > 0)
            points.push(value);
    }
    const bin = parsed.bin;
    if (typeof bin === 'string' && bin.length > 0)
        points.push(bin);
    if (bin && typeof bin === 'object') {
        for (const value of Object.values(bin)) {
            if (typeof value === 'string' && value.length > 0)
                points.push(value);
        }
    }
    const exportsField = parsed.exports;
    if (typeof exportsField === 'string' && exportsField.length > 0)
        points.push(exportsField);
    if (exportsField && typeof exportsField === 'object') {
        points.push(...Object.keys(exportsField).filter(key => key.startsWith('.')));
    }
    return dedupe(points).slice(0, 20);
}
function detectNodeBuildSystem(names, scripts) {
    const haystack = `${names.join(' ')} ${scripts.join(' ')}`.toLowerCase();
    if (haystack.includes('vite'))
        return 'vite';
    if (haystack.includes('webpack'))
        return 'webpack';
    if (haystack.includes('turbo'))
        return 'turbo';
    if (haystack.includes('nx'))
        return 'nx';
    if (haystack.includes('tsup'))
        return 'tsup';
    if (haystack.includes('rollup'))
        return 'rollup';
    if (haystack.includes('esbuild'))
        return 'esbuild';
    if (haystack.includes('tsc'))
        return 'tsc';
    return undefined;
}
function detectNodeTestFramework(names, scripts) {
    const haystack = `${names.join(' ')} ${scripts.join(' ')}`.toLowerCase();
    return NODE_TEST_FRAMEWORKS.find(name => haystack.includes(name));
}
function detectNodePlatforms(rootDir, names) {
    const platforms = [];
    if (names.includes('electron'))
        platforms.push('electron');
    if (names.includes('@tauri-apps/api') ||
        names.includes('@tauri-apps/cli') ||
        existsSync(path.join(rootDir, 'src-tauri', 'Cargo.toml'))) {
        platforms.push('tauri');
    }
    if (names.includes('react-native') || names.includes('expo'))
        platforms.push('mobile');
    if (names.includes('@capacitor/core'))
        platforms.push('capacitor');
    return dedupe(platforms);
}
async function readSmallText(filePath, maxBytes = 1024 * 1024) {
    try {
        const fileStat = await stat(filePath);
        if (!fileStat.isFile() || fileStat.size > maxBytes)
            return undefined;
        return await readFile(filePath, 'utf8');
    }
    catch {
        return undefined;
    }
}
async function detectNodeFrameworks(rootDir) {
    const packageJson = await readSmallText(path.join(rootDir, 'package.json'));
    if (!packageJson)
        return detectLockfilePackageManagers(rootDir);
    try {
        const parsed = JSON.parse(packageJson);
        const names = [
            ...dependencyKeys(parsed.dependencies),
            ...dependencyKeys(parsed.devDependencies),
            ...dependencyKeys(parsed.peerDependencies),
            ...dependencyKeys(parsed.optionalDependencies),
        ];
        const scripts = recordKeys(parsed.scripts);
        const frameworks = dedupe(names.flatMap(name => (NODE_FRAMEWORKS[name] ? [NODE_FRAMEWORKS[name]] : [])));
        const packageManager = packageManagerFromPackageJson(parsed) ?? detectPackageManagerByLockfile(rootDir);
        const entryPoints = collectPackageEntryPoints(parsed);
        const buildSystem = detectNodeBuildSystem(names, scripts);
        const testFramework = detectNodeTestFramework(names, scripts);
        const platforms = detectNodePlatforms(rootDir, names);
        return {
            frameworks,
            packageManager,
            ...(entryPoints.length > 0 ? { entryPoints } : {}),
            ...(scripts.length > 0 ? { scripts } : {}),
            ...(buildSystem ? { buildSystem } : {}),
            ...(testFramework ? { testFramework } : {}),
            ...(platforms.length > 0 ? { platforms } : {}),
        };
    }
    catch {
        return { frameworks: [], packageManager: detectPackageManagerByLockfile(rootDir) };
    }
}
async function detectPythonFrameworks(rootDir) {
    const texts = await Promise.all([
        readSmallText(path.join(rootDir, 'pyproject.toml')),
        readSmallText(path.join(rootDir, 'requirements.txt')),
    ]);
    const combined = texts
        .filter((text) => Boolean(text))
        .join('\n')
        .toLowerCase();
    if (!combined)
        return [];
    return PYTHON_FRAMEWORKS.filter(name => new RegExp(`(^|[^a-z0-9_-])${name}([^a-z0-9_-]|$)`).test(combined));
}
async function detectRubyFrameworks(rootDir) {
    const gemfile = (await readSmallText(path.join(rootDir, 'Gemfile')))?.toLowerCase();
    if (!gemfile)
        return [];
    return RUBY_FRAMEWORKS.filter(name => new RegExp(`gem\\s+['"]${name}['"]`).test(gemfile));
}
async function detectPhpFrameworks(rootDir) {
    const composer = await readSmallText(path.join(rootDir, 'composer.json'));
    if (!composer)
        return [];
    try {
        const parsed = JSON.parse(composer);
        const names = [...dependencyKeys(parsed.require), ...dependencyKeys(parsed['require-dev'])];
        return dedupe(names.flatMap(name => (PHP_FRAMEWORKS[name] ? [PHP_FRAMEWORKS[name]] : [])));
    }
    catch {
        return [];
    }
}
function detectPackageManagerByLockfile(rootDir) {
    for (const [fileName, manager] of Object.entries(LOCKFILE_PACKAGE_MANAGERS)) {
        if (existsSync(path.join(rootDir, fileName)))
            return manager;
    }
    return undefined;
}
function detectLockfilePackageManagers(rootDir) {
    return { frameworks: [], packageManager: detectPackageManagerByLockfile(rootDir) };
}
async function detectFrameworks(rootDir, markerKinds) {
    const [node, python, ruby, php] = await Promise.all([
        markerKinds.has('node') || markerKinds.has('node-monorepo')
            ? detectNodeFrameworks(rootDir)
            : detectLockfilePackageManagers(rootDir),
        markerKinds.has('python') ? detectPythonFrameworks(rootDir) : Promise.resolve([]),
        markerKinds.has('ruby') ? detectRubyFrameworks(rootDir) : Promise.resolve([]),
        markerKinds.has('php') ? detectPhpFrameworks(rootDir) : Promise.resolve([]),
    ]);
    return {
        frameworks: dedupe([...node.frameworks, ...python, ...ruby, ...php]),
        packageManager: node.packageManager,
        ...(node.entryPoints ? { entryPoints: node.entryPoints } : {}),
        ...(node.scripts ? { scripts: node.scripts } : {}),
        ...(node.buildSystem ? { buildSystem: node.buildSystem } : {}),
        ...(node.testFramework ? { testFramework: node.testFramework } : {}),
        ...(node.platforms ? { platforms: node.platforms } : {}),
    };
}
function classify(input) {
    const category = emptyCategoryBreakdown();
    let totalBytes = 0;
    for (const file of input.files) {
        category[file.category].files++;
        category[file.category].bytes += file.size;
        totalBytes += file.size;
    }
    const markerKinds = new Set(input.manifests.map(marker => marker.kind));
    const hasCodeManifest = [...markerKinds].some(kind => CODE_ECOSYSTEM_KINDS.has(kind));
    const siteGenerator = MANIFEST_RULES.find(rule => rule.siteGenerator && markerKinds.has(rule.kind))?.siteGenerator;
    const language = dominantLanguage(input.files);
    const officeExt = dominantOfficeExtension(input.files);
    const labels = dedupe([
        ...input.manifests.flatMap(marker => marker.kind === 'typescript' ? ['typescript'] : marker.kind.includes('node') ? ['node'] : [marker.kind]),
        ...(language ? [language] : []),
        ...input.frameworks,
        ...(input.packageManager ? [input.packageManager] : []),
    ].map(label => label.toLowerCase()));
    let primaryType;
    let confidence = 'medium';
    let summary;
    if (input.files.length === 0) {
        primaryType = 'empty-directory';
        confidence = 'high';
        summary = 'Empty directory';
    }
    else if (category.code.files > 0 && category.office.files > 0) {
        primaryType = 'mixed-project';
        confidence = 'high';
        summary = 'Mixed project: code + documents';
    }
    else if (siteGenerator && category.prose.files > 0) {
        primaryType = 'documentation-project';
        confidence = 'high';
        summary = `Documentation project (${siteGenerator})`;
    }
    else if (hasCodeManifest && category.code.files > 0) {
        primaryType = 'coding-project';
        confidence = 'high';
        const languageLabel = language ? titleCase(language) : 'Code';
        const frameworkSuffix = input.frameworks.length > 0 ? ` - ${input.frameworks.slice(0, 2).map(titleCase).join(', ')}` : '';
        summary = `${languageLabel} code project${frameworkSuffix}`;
    }
    else if (category.code.files > 0) {
        primaryType = 'coding-project';
        confidence = 'medium';
        const languageLabel = language ? titleCase(language) : 'Code';
        summary = `${languageLabel} code project`;
    }
    else if (category.office.files > 0) {
        primaryType = 'office-project';
        confidence = category.office.files === input.files.length ? 'high' : 'medium';
        summary = officeExt ? `Office project (${officeExt.toUpperCase()}-dominant)` : 'Office project';
    }
    else if (category.data.files > 0 && category.code.files === 0 && category.office.files === 0) {
        primaryType = 'data-project';
        confidence = category.data.files === input.files.length ? 'high' : 'medium';
        const hasNotebook = input.files.some(file => file.ext === 'ipynb');
        summary = hasNotebook ? 'Data / notebook project' : 'Data project';
    }
    else {
        primaryType = 'general-folder';
        confidence = totalBytes > 0 ? 'low' : 'medium';
        summary = 'General folder';
    }
    return { primaryType, confidence, summary, labels };
}
function recommendedCommands(primaryType) {
    if (primaryType === 'coding-project')
        return ['occ code index --path . --format json'];
    if (primaryType === 'office-project')
        return ['occ workspace analyze --no-code --format json'];
    if (primaryType === 'documentation-project')
        return ['occ workspace documents --format json'];
    if (primaryType === 'mixed-project')
        return ['occ workspace analyze --format json'];
    if (primaryType === 'data-project')
        return ['occ --quick --format json .'];
    return ['occ --quick .'];
}
function recommendedCalls(primaryType) {
    if (primaryType === 'coding-project') {
        return [
            { namespace: 'code', method: 'previewSize', args: { repoRoot: '.' } },
            { namespace: 'code', method: 'buildIndexIsolated', args: { repoRoot: '.', contentMode: 'excerpt' } },
        ];
    }
    if (primaryType === 'office-project') {
        return [
            { namespace: 'workspace', method: 'analyze', args: { rootDir: '.', includeCode: false } },
            { namespace: 'workspace', method: 'inspectDocuments', args: { rootDir: '.' } },
        ];
    }
    if (primaryType === 'documentation-project') {
        return [{ namespace: 'workspace', method: 'inspectDocuments', args: { rootDir: '.', includeMarkdown: true } }];
    }
    if (primaryType === 'mixed-project') {
        return [
            { namespace: 'workspace', method: 'analyze', args: { rootDir: '.' } },
            { namespace: 'code', method: 'previewSize', args: { repoRoot: '.' } },
            { namespace: 'workspace', method: 'inspectDocuments', args: { rootDir: '.' } },
        ];
    }
    if (primaryType === 'data-project') {
        return [{ namespace: 'workspace', method: 'describe', args: { rootDir: '.' } }];
    }
    return [{ namespace: 'workspace', method: 'describe', args: { rootDir: '.' } }];
}
function buildPresenceSignals(files) {
    return {
        hasCode: files.some(file => file.category === 'code'),
        hasDocuments: files.some(file => file.category === 'office' || file.category === 'prose'),
        hasOfficeDocuments: files.some(file => file.category === 'office'),
        hasTables: files.some(file => ['xlsx', 'xlsm', 'xls', 'ods', 'csv', 'tsv'].includes(file.ext)),
        hasNotebooks: files.some(file => file.ext === 'ipynb'),
    };
}
async function discoverFiles(rootDir, options) {
    const excludeDir = options.excludeDir ?? [...DEFAULT_EXCLUDE_DIRS];
    const ignoreMatcher = await createIgnoreMatcher(rootDir, {
        excludeDir,
        noGitignore: options.noGitignore,
        ignorePatterns: options.ignorePatterns,
        overlayIgnorePatterns: options.overlayIgnorePatterns,
    });
    const discovered = await fg('**/*', {
        cwd: rootDir,
        absolute: true,
        dot: true,
        onlyFiles: true,
        followSymbolicLinks: false,
        ignore: buildExcludeDirGlobs(excludeDir),
        unique: true,
    });
    const files = [];
    const sorted = discovered.sort();
    const batchSize = 100;
    for (let i = 0; i < sorted.length; i += batchSize) {
        const batch = sorted.slice(i, i + batchSize);
        const stats = await Promise.allSettled(batch.map(filePath => stat(filePath)));
        for (let j = 0; j < batch.length; j++) {
            const filePath = batch[j];
            const fileStat = stats[j];
            if (fileStat.status === 'rejected' || !fileStat.value.isFile())
                continue;
            const relativePath = normalizeRelativePath(path.relative(rootDir, filePath));
            if (ignoreMatcher.ignores(relativePath))
                continue;
            const ext = getExtension(filePath);
            files.push({
                path: filePath,
                relativePath,
                size: fileStat.value.size,
                ext,
                category: categoryForPath(relativePath),
            });
        }
    }
    return files;
}
function collectMarkers(files) {
    const manifests = [];
    const markers = [];
    for (const file of files) {
        const rule = markerRuleForPath(file.relativePath);
        if (rule) {
            manifests.push({ path: file.relativePath, kind: rule.kind, label: rule.label });
            continue;
        }
        const baseName = path.basename(file.relativePath);
        if (baseName.toLowerCase() === 'readme.md') {
            markers.push({ path: file.relativePath, kind: 'readme', label: 'README' });
        }
    }
    return { manifests, markers };
}
function buildComposition(files) {
    const byCategory = emptyCategoryBreakdown();
    const byExtension = {};
    let bytes = 0;
    for (const file of files) {
        bytes += file.size;
        byCategory[file.category].files++;
        byCategory[file.category].bytes += file.size;
        const ext = file.ext || '[none]';
        byExtension[ext] = (byExtension[ext] ?? 0) + 1;
    }
    for (const breakdown of Object.values(byCategory)) {
        breakdown.pct = bytes === 0 ? 0 : Number(((breakdown.bytes / bytes) * 100).toFixed(1));
    }
    return {
        totals: {
            files: files.length,
            bytes,
        },
        byCategory,
        byExtension,
        dominantExtensions: sortByCountDesc(Object.entries(byExtension).map(([ext, count]) => ({ ext, files: count }))).slice(0, 5),
    };
}
async function buildProjects(rootDir, manifests, maxProjects, rootDetection) {
    const byRoot = new Map();
    for (const marker of manifests) {
        const projectRoot = projectRootForMarker(marker.path);
        const current = byRoot.get(projectRoot) ?? [];
        current.push(marker);
        byRoot.set(projectRoot, current);
    }
    const total = byRoot.size;
    const sortedRoots = [...byRoot.entries()].sort((left, right) => compareCodeUnits(left[0], right[0]));
    // Detection is comparatively expensive (reads and parses manifests), so only
    // run it for the roots that survive the maxProjects cut.
    const selectedRoots = maxProjects <= 0 ? [] : sortedRoots.slice(0, maxProjects);
    const projects = [];
    for (const [projectPath, projectMarkers] of selectedRoots) {
        const markerKinds = new Set(projectMarkers.map(marker => marker.kind));
        const absoluteProjectPath = projectPath === '.' ? rootDir : path.join(rootDir, projectPath);
        const frameworkDetection = projectPath === '.' ? rootDetection : await detectFrameworks(absoluteProjectPath, markerKinds);
        projects.push({
            path: projectPath,
            kinds: dedupe(projectMarkers.map(marker => marker.kind)),
            markers: projectMarkers.map(marker => marker.path),
            frameworks: frameworkDetection.frameworks,
            ...(frameworkDetection.packageManager ? { packageManager: frameworkDetection.packageManager } : {}),
            ...(frameworkDetection.entryPoints ? { entryPoints: frameworkDetection.entryPoints } : {}),
            ...(frameworkDetection.scripts ? { scripts: frameworkDetection.scripts } : {}),
            ...(frameworkDetection.buildSystem ? { buildSystem: frameworkDetection.buildSystem } : {}),
            ...(frameworkDetection.testFramework ? { testFramework: frameworkDetection.testFramework } : {}),
            ...(frameworkDetection.platforms ? { platforms: frameworkDetection.platforms } : {}),
            confidence: 'high',
        });
    }
    return {
        projects,
        summary: {
            total,
            shown: projects.length,
            truncated: projects.length < total,
        },
    };
}
export async function describeWorkspace(rootDir, options = {}) {
    const resolvedRoot = path.resolve(rootDir);
    const rootStat = await stat(resolvedRoot);
    if (!rootStat.isDirectory()) {
        throw new Error(`Not a directory: ${resolvedRoot}`);
    }
    const files = await discoverFiles(resolvedRoot, options);
    const { manifests, markers } = collectMarkers(files);
    const markerKinds = new Set(manifests.filter(marker => projectRootForMarker(marker.path) === '.').map(marker => marker.kind));
    const frameworkDetection = await detectFrameworks(resolvedRoot, markerKinds);
    const maxProjects = Math.max(0, options.maxProjects ?? 25);
    const projectResult = await buildProjects(resolvedRoot, manifests, maxProjects, frameworkDetection);
    const composition = buildComposition(files);
    const presenceSignals = buildPresenceSignals(files);
    const classification = classify({
        files,
        manifests,
        frameworks: frameworkDetection.frameworks,
        packageManager: frameworkDetection.packageManager,
    });
    return {
        schemaVersion: 1,
        rootDir: resolvedRoot,
        classification,
        composition,
        signals: {
            manifests,
            markers,
            frameworks: frameworkDetection.frameworks,
            ...(frameworkDetection.packageManager ? { packageManager: frameworkDetection.packageManager } : {}),
            ...presenceSignals,
        },
        projects: projectResult.projects,
        projectSummary: projectResult.summary,
        recommendedCommands: recommendedCommands(classification.primaryType),
        recommendedCalls: recommendedCalls(classification.primaryType),
    };
}
//# sourceMappingURL=describe.js.map