// engine/local-first/detector.ts — SPEC-047
// Detects CRDT libraries, sync backends, local storage, and offline signals.
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { glob } from 'glob';
// === CRDT library package names ===
const CRDT_PACKAGES = new Map([
    ['yjs', 'yjs'],
    ['@automerge/automerge', 'automerge'],
    ['automerge', 'automerge'],
    ['loro-crdt', 'loro-crdt'],
    ['diamond-types', 'diamond-types'],
]);
// === Sync backend package names ===
const SYNC_PACKAGES = new Map([
    ['y-websocket', 'y-websocket'],
    ['@hocuspocus/server', 'hocuspocus'],
    ['hocuspocus', 'hocuspocus'],
    ['@electric-sql/client', 'electric-sql'],
    ['electric-sql', 'electric-sql'],
    ['@powersync/service', 'powersync'],
    ['powersync', 'powersync'],
    ['@liveblocks/client', 'liveblocks'],
    ['liveblocks', 'liveblocks'],
]);
// === Local storage package names mapped to engine ===
const STORAGE_PACKAGES = new Map([
    ['sql.js', 'SQLite-WASM'],
    ['@sqlite.org/sqlite-wasm', 'SQLite-WASM'],
    ['wa-sqlite', 'SQLite-WASM'],
    ['better-sqlite3', 'SQLite-native'],
    ['sqlite3', 'SQLite-native'],
    ['react-native-sqlite-storage', 'SQLite-native'],
    ['expo-sqlite', 'SQLite-native'],
    ['op-sqlite', 'SQLite-native'],
    ['leveldb', 'LevelDB'],
    ['levelup', 'LevelDB'],
    ['level', 'LevelDB'],
]);
// === Offline support signals ===
const OFFLINE_PACKAGES = ['workbox-core', 'workbox-strategies', 'workbox-routing', 'workbox'];
const OFFLINE_KEYWORDS = ['offline', 'local-first', 'local_first', 'pwa'];
const OFFLINE_FILES = ['sw.js', 'service-worker.js', 'sw.ts', 'service-worker.ts'];
// === IndexedDB signals (browser-native, no npm package) ===
const INDEXEDDB_PACKAGES = ['idb', 'idb-keyval', 'dexie', 'localforage', 'rxdb', 'pouchdb'];
// === LevelDB additional packages ===
const LEVELDB_PACKAGES = ['abstract-level', 'memory-level', 'classic-level'];
async function readFileOrNull(filePath) {
    try {
        return await readFile(filePath, 'utf-8');
    }
    catch {
        return null;
    }
}
async function readPackageJson(projectPath) {
    const content = await readFileOrNull(join(projectPath, 'package.json'));
    if (!content) {
        return null;
    }
    try {
        const pkg = JSON.parse(content);
        const all = {
            ...(pkg.dependencies ?? {}),
            ...(pkg.devDependencies ?? {}),
        };
        return all;
    }
    catch {
        return null;
    }
}
async function readPackageJsonFull(projectPath) {
    const content = await readFileOrNull(join(projectPath, 'package.json'));
    if (!content) {
        return null;
    }
    try {
        const pkg = JSON.parse(content);
        return {
            deps: {
                ...(pkg.dependencies ?? {}),
                ...(pkg.devDependencies ?? {}),
            },
            keywords: pkg.keywords ?? [],
            description: pkg.description ?? '',
        };
    }
    catch {
        return null;
    }
}
/** Detect CRDT library from package.json dependencies. */
async function detectCrdtLibrary(projectPath) {
    const deps = await readPackageJson(projectPath);
    if (!deps) {
        return null;
    }
    for (const [pkg, lib] of CRDT_PACKAGES) {
        if (pkg in deps) {
            return lib;
        }
    }
    return null;
}
/** Detect sync backend from package.json dependencies. */
async function detectSyncBackend(projectPath) {
    const deps = await readPackageJson(projectPath);
    if (!deps) {
        return null;
    }
    for (const [pkg, backend] of SYNC_PACKAGES) {
        if (pkg in deps) {
            return backend;
        }
    }
    return null;
}
/** Detect local storage engine from package.json dependencies. */
async function detectLocalStorageEngine(projectPath) {
    const deps = await readPackageJson(projectPath);
    if (!deps) {
        return null;
    }
    // STORAGE_PACKAGES covers SQLite variants and LevelDB
    for (const [pkg, engine] of STORAGE_PACKAGES) {
        if (pkg in deps) {
            return engine;
        }
    }
    // LevelDB alternative packages
    for (const pkg of LEVELDB_PACKAGES) {
        if (pkg in deps) {
            return 'LevelDB';
        }
    }
    // IndexedDB wrapper libraries (browser native engine)
    for (const pkg of INDEXEDDB_PACKAGES) {
        if (pkg in deps) {
            return 'IndexedDB';
        }
    }
    return null;
}
/** Detect offline support signals (service workers, workbox, keywords). */
async function detectOfflineSupport(projectPath) {
    const pkg = await readPackageJsonFull(projectPath);
    if (pkg) {
        // Check workbox packages
        for (const offlinePkg of OFFLINE_PACKAGES) {
            if (offlinePkg in pkg.deps) {
                return true;
            }
        }
        // Check package.json keywords and description
        const combined = pkg.keywords.join(' ').toLowerCase() + ' ' + pkg.description.toLowerCase();
        for (const kw of OFFLINE_KEYWORDS) {
            if (combined.includes(kw)) {
                return true;
            }
        }
    }
    // Check for service worker files
    for (const swFile of OFFLINE_FILES) {
        const content = await readFileOrNull(join(projectPath, 'public', swFile));
        if (content !== null) {
            return true;
        }
        const rootContent = await readFileOrNull(join(projectPath, swFile));
        if (rootContent !== null) {
            return true;
        }
    }
    // Scan src files for service worker registration
    try {
        const files = await glob('**/*.{ts,js}', {
            cwd: projectPath,
            nodir: true,
            maxDepth: 3,
            ignore: ['node_modules/**', 'dist/**', 'build/**', '.git/**'],
        });
        for (const f of files.slice(0, 20)) {
            const content = await readFileOrNull(join(projectPath, f));
            if (content?.includes('serviceWorker') || content?.includes('service-worker')) {
                return true;
            }
        }
    }
    catch {
        // ignore
    }
    return false;
}
/** Collect human-readable signals that triggered detection. */
function collectSignals(crdtLibrary, syncBackend, localStorageEngine, hasOfflineSupport) {
    const signals = [];
    if (crdtLibrary) {
        signals.push(`CRDT library detected: ${crdtLibrary}`);
    }
    if (syncBackend) {
        signals.push(`Sync backend detected: ${syncBackend}`);
    }
    if (localStorageEngine) {
        signals.push(`Local storage engine detected: ${localStorageEngine}`);
    }
    if (hasOfflineSupport) {
        signals.push('Offline support signals detected (service worker or workbox)');
    }
    return signals;
}
/**
 * Detect local-first architecture signals in a project.
 * Returns a LocalFirstDetectionResult with all detected components.
 */
export async function detectLocalFirst(projectPath) {
    const [crdtLibrary, syncBackend, localStorageEngine, hasOfflineSupport] = await Promise.all([
        detectCrdtLibrary(projectPath),
        detectSyncBackend(projectPath),
        detectLocalStorageEngine(projectPath),
        detectOfflineSupport(projectPath),
    ]);
    const isLocalFirst = crdtLibrary !== null || syncBackend !== null;
    const localFirstSignals = collectSignals(crdtLibrary, syncBackend, localStorageEngine, hasOfflineSupport);
    return {
        isLocalFirst,
        crdtLibrary,
        syncBackend,
        localStorageEngine,
        hasOfflineSupport,
        localFirstSignals,
    };
}
//# sourceMappingURL=detector.js.map