"use strict";
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PLUGIN_MAIN_FILE = exports.pluginsConfig = exports.suspendPlugins = exports.enablePlugins = exports.pluginsWatcher = exports.SERVER_CODE_ID = exports.Plugin = exports.pluginsMiddleware = exports.pluginsScanned = exports.STORAGE_FOLDER = exports.DELETE_ME_SUFFIX = exports.DISABLING_SUFFIX = exports.PATH = void 0;
exports.isPluginRunning = isPluginRunning;
exports.isPluginEnabled = isPluginEnabled;
exports.enablePlugin = enablePlugin;
exports.stopPlugin = stopPlugin;
exports.startPlugin = startPlugin;
exports.setPluginConfig = setPluginConfig;
exports.getPluginInfo = getPluginInfo;
exports.findPluginByRepo = findPluginByRepo;
exports.getPluginConfigFields = getPluginConfigFields;
exports.mapPlugins = mapPlugins;
exports.firstPlugin = firstPlugin;
exports.getInactivePlugins = getInactivePlugins;
exports.rescan = rescan;
exports.parsePluginSource = parsePluginSource;
exports.getMissingDependencies = getMissingDependencies;
const fast_glob_1 = __importDefault(require("fast-glob"));
const watchLoad_1 = require("./watchLoad");
const lodash_1 = __importDefault(require("lodash"));
const const_1 = require("./const");
const Const = __importStar(require("./const"));
const misc_1 = require("./misc");
const misc = __importStar(require("./misc"));
const config_1 = require("./config");
const vfs_1 = require("./vfs");
const serveFile_1 = require("./serveFile");
const events_1 = __importDefault(require("./events"));
const promises_1 = require("fs/promises");
const fs_1 = require("fs");
const connections_1 = require("./connections");
const path_1 = require("path");
const customHtml_1 = require("./customHtml");
const kvstorage_1 = require("@rejetto/kvstorage");
const first_1 = require("./first");
const frontEndApis_1 = require("./frontEndApis");
const index_1 = require("./index");
const block_1 = require("./block");
const lang_1 = require("./lang");
const i18n_1 = require("./i18n");
const perm_1 = require("./perm");
const auth_1 = require("./auth");
const icons_1 = require("./icons");
const listen_1 = require("./listen");
exports.PATH = 'plugins';
exports.DISABLING_SUFFIX = '-disabled';
exports.DELETE_ME_SUFFIX = '-delete_me' + exports.DISABLING_SUFFIX;
exports.STORAGE_FOLDER = 'storage';
exports.pluginsScanned = (0, misc_1.pendingPromise)();
setTimeout(async () => {
    for (const x of await (0, promises_1.readdir)(exports.PATH))
        if (x.endsWith(exports.DELETE_ME_SUFFIX))
            await (0, promises_1.rm)((0, path_1.join)(exports.PATH, x), { recursive: true, force: true }).catch(() => { });
}, 1000);
const plugins = new Map(); // now that we care about the order, a simple object wouldn't do, because numbers are always at the beginning
function isPluginRunning(id) {
    return Boolean(plugins.get(id)?.started);
}
function isPluginEnabled(id, considerSuspension = false) {
    return (!considerSuspension || !exports.suspendPlugins.get()) && exports.enablePlugins.get().includes(id);
}
function enablePlugin(id, state = true) {
    if (state && !getPluginInfo(id))
        throw Error('miss');
    exports.enablePlugins.set(arr => {
        if (arr.includes(id) === state)
            return arr;
        console.log("Switching plugin", id, state ? "on" : "off");
        return arr.includes(id) === state ? arr
            : state ? [...arr, id]
                : arr.filter((x) => x !== id);
    });
}
async function stopPlugin(id) {
    enablePlugin(id, false);
    await waitRunning(id, false);
}
async function startPlugin(id) {
    if (getPluginInfo(id)?.isTheme)
        await Promise.all(mapPlugins((pl, id) => pl.isTheme && stopPlugin(id)));
    enablePlugin(id);
    await waitRunning(id);
}
async function waitRunning(id, state = true) {
    while (isPluginRunning(id) !== state) {
        await (0, misc_1.wait)(500);
        const error = getError(id);
        if (error)
            throw Error(error);
    }
}
// nullish values are equivalent to defaultValues
function setPluginConfig(id, changes) {
    exports.pluginsConfig.set(allConfigs => {
        const fields = getPluginConfigFields(id);
        const oldConfig = allConfigs[id];
        const newConfig = changes && lodash_1.default.pickBy({ ...oldConfig, ...changes }, (v, k) => v != null && !(0, misc_1.same)(v, fields?.[k]?.defaultValue));
        return { ...allConfigs, [id]: lodash_1.default.isEmpty(newConfig) ? undefined : newConfig };
    });
}
function getPluginInfo(id) {
    const running = plugins.get(id);
    return running && { ...running.getData(), ...running } || inactivePlugins[id];
}
function findPluginByRepo(repo) {
    for (const pl of plugins.values())
        if (match(pl.getData()))
            return pl;
    return lodash_1.default.find(inactivePlugins, match);
    function match(rec) {
        return repo === (rec?.repo?.main ?? rec?.repo);
    }
}
function getPluginConfigFields(id) {
    return plugins.get(id)?.getData().config;
}
async function initPlugin(pl, morePassedToInit) {
    const undoEvents = [];
    const timeouts = [];
    const controlledEvents = Object.create(events_1.default, (0, misc_1.objFromKeys)(['on', 'once', 'multi'], k => ({
        value() {
            if (k === 'multi')
                arguments[0] = lodash_1.default.mapValues(arguments[0], trap);
            else
                arguments[1] = trap(arguments[1]);
            const ret = events_1.default[k](...arguments);
            undoEvents.push(ret);
            return ret;
            function trap(cb) {
                return (...args) => {
                    try {
                        if (!lodash_1.default.isFunction(cb))
                            return;
                        const ret = cb(...args);
                        return ret instanceof Promise ? ret.catch(printError) : ret;
                    }
                    catch (e) {
                        printError(e);
                    }
                    function printError(e) {
                        const { event } = args.at(-1);
                        console.error(`plugin ${morePassedToInit?.id || '?'} on event ${event}:`, e);
                    }
                };
            }
        }
    })));
    const res = await pl.init?.({
        Const, require,
        // intercept all subscriptions, so to be able to undo them on unload
        events: controlledEvents,
        log: console.log,
        setError(msg) { setError(morePassedToInit?.id || 'server_code', msg); },
        getHfsConfig: config_1.getConfig,
        setInterval() {
            const ret = setInterval(...arguments);
            timeouts.push(ret); // intervals can be canceled by clearTimeout (source: MDN)
            return ret;
        },
        setTimeout(cb, delay = 0, ...args) {
            let ret;
            if (lodash_1.default.isFunction(cb)) {
                const original = cb;
                cb = function () {
                    lodash_1.default.pull(timeouts, ret);
                    return original.apply(this, args);
                };
            }
            ret = setTimeout(cb, delay, ...args); // 'any' to allow build of frontend and admin
            timeouts.push(ret);
            return ret;
        },
        async onServer(cb) {
            const res = await (0, listen_1.getServerStatus)();
            if (res.http.srv)
                cb(res.http.srv);
            if (res.https.srv)
                cb(res.https.srv);
            controlledEvents.on('listening', ({ server }) => cb(server));
        },
        misc, _: lodash_1.default,
        customApiCall, notifyClient: frontEndApis_1.notifyClient, addBlock: block_1.addBlock, ctxBelongsTo: perm_1.ctxBelongsTo, getConnections: connections_1.getConnections, normalizeFilename: vfs_1.normalizeFilename,
        getCurrentUsername: auth_1.getCurrentUsername, getAccount: perm_1.getAccount, getUsernames: perm_1.getUsernames, addAccount: perm_1.addAccount, delAccount: perm_1.delAccount, updateAccount: perm_1.updateAccount, renameAccount: perm_1.renameAccount,
        ...morePassedToInit
    });
    Object.assign(pl, typeof res === 'function' ? { unload: res } : res);
    (0, misc_1.patchKey)(pl, 'unload', was => () => {
        for (const x of timeouts)
            clearTimeout(x);
        for (const cb of undoEvents)
            cb();
        if (typeof was === 'function')
            return was(...arguments);
    });
    events_1.default.emit('pluginInitialized', pl);
    return pl;
}
const pluginsMiddleware = async (ctx, next) => {
    const after = {};
    // run middleware plugins
    let lastStatus = ctx.status;
    let lastBody = ctx.body;
    const res = await events_1.default.emitAsync('request', { ctx });
    if (ctx.isAborted() || res?.isDefaultPrevented())
        return;
    if (res?.length)
        after['/event'] = () => res.forEach(misc_1.callable);
    await Promise.all(mapPlugins(async (pl, id) => {
        try {
            const res = await pl.middleware?.(ctx);
            printChange(id);
            if (ctx.isAborted())
                ctx.stop();
            // don't just check ctx.isStopped, as the async plugin that called ctx.stop will reach here after sync ones
            if (ctx.isStopped && !ctx.pluginBlockedRequest)
                console.debug("Plugin blocked request", ctx.pluginBlockedRequest = id);
            if (typeof res === 'function')
                after[id] = res;
        }
        catch (e) {
            printError(id, e);
        }
    }));
    // expose public plugins' files
    if (!ctx.isStopped) {
        const { path } = ctx;
        if (path.startsWith(const_1.PLUGINS_PUB_URI)) {
            const a = path.substring(const_1.PLUGINS_PUB_URI.length).split('/');
            const name = a.shift();
            if (plugins.has(name)) { // do it only if the plugin is loaded
                if (ctx.get('referer')?.endsWith('/'))
                    ctx.state.considerAsGui = true;
                await (0, serveFile_1.serveFile)(ctx, plugins.get(name).folder + '/public/' + a.join('/'), const_1.MIME_AUTO);
            }
            return;
        }
        if (ctx.body === undefined && ctx.status === const_1.HTTP_NOT_FOUND) // no response was provided by plugins, so we'll do
            await next();
    }
    lastStatus = ctx.status;
    lastBody = ctx.body;
    for (const [id, f] of Object.entries(after))
        try {
            await f();
            printChange(id);
        }
        catch (e) {
            printError(id, e);
        }
    function printChange(id) {
        if (id === exports.SERVER_CODE_ID || (lastStatus === ctx.status && lastBody === ctx.body))
            return;
        console.debug("Plugin changed response:", id);
        lastStatus = ctx.status;
        lastBody = ctx.body;
    }
    function printError(id, e) {
        console.log(`Error middleware plugin ${id}: ${e?.message || e}`);
        console.debug(e);
    }
};
exports.pluginsMiddleware = pluginsMiddleware;
events_1.default.once('app', () => Object.assign(index_1.app.context, {
    isStopped: false,
    stop() { return this.isStopped = true; }
}));
class Plugin {
    id;
    folder;
    data;
    onUnload;
    started = new Date();
    icons;
    log;
    constructor(id, folder, data, onUnload) {
        this.id = id;
        this.folder = folder;
        this.data = data;
        this.onUnload = onUnload;
        if (!data)
            throw 'invalid data';
        this.log = [];
        this.data = data = { ...data }; // clone to make object modifiable. Objects coming from import are not.
        // some validation
        for (const k of ['frontend_css', 'frontend_js']) {
            const v = data[k];
            if (typeof v === 'string')
                data[k] = [v];
            else if (v && !Array.isArray(v)) {
                delete data[k];
                console.warn('Invalid', k);
            }
        }
        plugins.set(id, this);
        const keys = Array.from(plugins.keys());
        const idx = keys.indexOf(id);
        // initialize moveDown with existing plugins that want to be after this
        const moveDown = (0, misc_1.onlyTruthy)(mapPlugins(((pl, plId, plIdx) => pl.afterPlugin === id && plIdx < idx && plId)));
        const { beforePlugin, afterPlugin } = data; // then consider what this plugin wants
        if (afterPlugin && keys.indexOf(afterPlugin) > idx)
            moveDown.push(id); // move down this plugin
        if (beforePlugin && keys.indexOf(beforePlugin) < idx)
            moveDown.push(beforePlugin); // move down the other plugin
        for (const k of moveDown) {
            const temp = plugins.get(k);
            if (!temp)
                continue;
            plugins.delete(k);
            plugins.set(k, temp);
        }
    }
    get version() { return this.data?.version; }
    get description() { return this.data?.description; }
    get apiRequired() { return this.data?.apiRequired; }
    get isTheme() { return this.data?.isTheme; }
    get repo() { return this.data?.repo; }
    get depend() { return this.data?.depend; }
    get afterPlugin() { return this.data?.afterPlugin; }
    get beforePlugin() { return this.data?.beforePlugin; }
    get middleware() {
        return this.data?.middleware;
    }
    get frontend_css() {
        return this.data?.frontend_css;
    }
    get frontend_js() {
        return this.data?.frontend_js;
    }
    get onDirEntry() {
        return this.data?.onDirEntry;
    }
    getData() {
        return this.data;
    }
    async unload(reloading = false) {
        if (!this.started)
            return;
        this.started = null;
        const { id } = this;
        try {
            await this.data?.unload?.();
        }
        catch (e) {
            console.log('Error unloading plugin', id, String(e));
        }
        await this.onUnload();
        if (!reloading && id !== exports.SERVER_CODE_ID) // we already printed 'reloading'
            console.log('Unloaded plugin', id);
        if (this.data)
            this.data.unload = undefined;
    }
}
exports.Plugin = Plugin;
exports.SERVER_CODE_ID = '.'; // a name that will surely be not found among plugin folders
const serverCode = (0, config_1.defineConfig)('server_code', '', async (script, { k }) => {
    try {
        (await serverCode.compiled())?.unload();
    }
    catch { }
    const res = {};
    try {
        new Function('exports,require', script)(res, require); // parse
        await initPlugin(res);
        res.getCustomHtml = () => (0, misc_1.callable)(res.customHtml) || {};
        return new Plugin(exports.SERVER_CODE_ID, '', res, lodash_1.default.noop);
    }
    catch (e) {
        return console.error(k + ':', e.message || String(e));
    }
});
function mapPlugins(cb, includeServerCode = true) {
    let i = 0;
    return Array.from(plugins).map(([plName, pl]) => {
        if (!includeServerCode && plName === exports.SERVER_CODE_ID)
            return;
        try {
            return cb(pl, plName, i++);
        }
        catch (e) {
            console.log('Plugin error', plName, String(e));
        }
    }).filter(x => x !== undefined);
}
function firstPlugin(cb, includeServerCode = true) {
    for (const [plName, pl] of plugins.entries()) {
        if (!includeServerCode && plName === exports.SERVER_CODE_ID)
            continue;
        try {
            const ret = cb(pl, plName);
            if (ret !== undefined)
                return ret;
        }
        catch (e) {
            console.log('Plugin error', plName, String(e));
        }
    }
}
let inactivePlugins = {};
function getInactivePlugins() {
    return Object.values(inactivePlugins);
}
const rescanAsap = (0, misc_1.debounceAsync)(rescan, { wait: 1000 });
if (!(0, fs_1.existsSync)(exports.PATH))
    try {
        (0, fs_1.mkdirSync)(exports.PATH);
    }
    catch { }
exports.pluginsWatcher = (0, misc_1.watchDir)(exports.PATH, rescanAsap);
exports.enablePlugins = (0, config_1.defineConfig)('enable_plugins', ['antibrute']);
exports.enablePlugins.sub(rescanAsap);
exports.suspendPlugins = (0, config_1.defineConfig)(misc_1.CFG.suspend_plugins, false);
exports.pluginsConfig = (0, config_1.defineConfig)('plugins_config', {});
exports.PLUGIN_MAIN_FILE = 'plugin.js';
const pluginWatchers = new Map();
async function rescan() {
    console.debug('Scanning plugins');
    const patterns = [exports.PATH + '/*'];
    if (const_1.APP_PATH !== process.cwd())
        patterns.unshift((0, misc_1.escapeGlobPath)(const_1.APP_PATH) + '/' + patterns[0]); // first search bundled plugins, because otherwise they won't be loaded because of the folders with same name in .hfs/plugins (used for storage)
    const existing = new Set();
    for (const { path, dirent } of await (0, fast_glob_1.default)(patterns, { onlyFiles: false, suppressErrors: true, objectMode: true })) {
        if (!dirent.isDirectory() || path.endsWith(exports.DISABLING_SUFFIX))
            continue;
        const id = path.split('/').slice(-1)[0];
        existing.add(id);
        if (!pluginWatchers.has(id))
            pluginWatchers.set(id, watchPlugin(id, (0, path_1.join)(path, exports.PLUGIN_MAIN_FILE)));
    }
    for (const [id, cancelWatcher] of pluginWatchers.entries())
        if (!existing.has(id)) {
            enablePlugin(id, false);
            cancelWatcher();
            pluginWatchers.delete(id);
        }
    exports.pluginsScanned.resolve();
}
function watchPlugin(id, path) {
    console.debug('Plugin watch', id);
    const module = (0, path_1.resolve)(path);
    let starting;
    const unsub = (0, config_1.subMultipleConfigs)(() => {
        if (!getPluginInfo(id))
            return; // not loaded yet
        const should = isPluginEnabled(id, true);
        if (should === isPluginRunning(id))
            return;
        if (should)
            return start();
        stop();
    }, [exports.enablePlugins, exports.suspendPlugins]);
    const { unwatch } = (0, watchLoad_1.watchLoad)(module, async (source) => {
        const notRunning = inactivePlugins[id];
        if (!source)
            return onUninstalled();
        if (isPluginEnabled(id, true))
            return start();
        const p = parsePluginSource(id, source); // plugin not running = json parsing
        if ((0, misc_1.same)(notRunning, p))
            return;
        inactivePlugins[id] = p;
        events_1.default.emit(notRunning ? 'pluginUpdated' : 'pluginInstalled', p);
    });
    return () => {
        console.debug('Plugin unwatch', id);
        unsub();
        unwatch();
        return onUninstalled();
    };
    async function onUninstalled() {
        await stop();
        const info = getPluginInfo(id);
        if (!info)
            return; // already missing
        delete inactivePlugins[id];
        events_1.default.emit('pluginUninstalled', id, info.repo);
    }
    async function markItInactive() {
        plugins.delete(id);
        inactivePlugins[id] = await parsePlugin();
    }
    async function parsePlugin() {
        return parsePluginSource(id, await (0, promises_1.readFile)(module, 'utf8'));
    }
    async function stop() {
        await starting;
        const p = plugins.get(id);
        if (!p)
            return;
        await p.unload();
        await markItInactive().catch(() => events_1.default.emit('pluginUninstalled', id, p.repo)); // when a running plugin is deleted, avoid error and report
        events_1.default.emit('pluginStopped', p);
    }
    async function start() {
        if (starting)
            return;
        try {
            starting = (0, misc_1.pendingPromise)();
            // if dependencies are not ready right now, we give some time. Not super-solid but good enough for now.
            const info = await parsePlugin();
            if (!await (0, misc_1.waitFor)(() => lodash_1.default.isEmpty(getMissingDependencies(info)), { timeout: 5_000 }))
                throw Error("plugin missing dependencies: " + lodash_1.default.map(getMissingDependencies(info), x => x.repo).join(', '));
            if (getPluginInfo(id))
                setError(id, '');
            const alreadyRunning = plugins.get(id);
            console.log(alreadyRunning ? "Reloading plugin" : "Loading plugin", id);
            const pluginData = require(module);
            deleteModule(require.resolve(module)); // avoid caching at next import
            calculateBadApi(pluginData);
            if (pluginData.badApi)
                throw Error(pluginData.badApi);
            await alreadyRunning?.unload(true);
            console.debug("Starting plugin", id);
            const storageDir = (0, path_1.resolve)(exports.PATH, id, exports.STORAGE_FOLDER) + (const_1.IS_WINDOWS ? '\\' : '/');
            await (0, promises_1.mkdir)(storageDir, { recursive: true });
            const openDbs = [];
            const subbedConfigs = [];
            const pluginReady = (0, misc_1.pendingPromise)();
            const MAX_LOG = 100;
            await initPlugin(pluginData, {
                id,
                srcDir: __dirname,
                storageDir,
                async openDb(filename, options) {
                    if (!filename)
                        throw Error("missing filename");
                    const db = new kvstorage_1.KvStorage(options);
                    await db.open((0, path_1.join)(storageDir, filename));
                    openDbs.push(db);
                    return db;
                },
                log(...args) {
                    console.log('Plugin', id + ':', ...args);
                    pluginReady.then(() => {
                        if (!plugin)
                            return;
                        const msg = { ts: new Date, msg: args.map(x => x && typeof x === 'object' ? JSON.stringify(x) : String(x)).join(' ') };
                        plugin.log.push(msg);
                        if (plugin.log.length > MAX_LOG)
                            plugin.log.splice(0, 10); // truncate
                        events_1.default.emit('pluginLog:' + id, msg);
                        events_1.default.emit('pluginLog', id, msg);
                    });
                },
                getConfig(cfgKey) {
                    const cur = exports.pluginsConfig.get()?.[id];
                    return cfgKey ? cur?.[cfgKey] ?? pluginData.config?.[cfgKey]?.defaultValue
                        : lodash_1.default.defaults(cur, lodash_1.default.mapValues(pluginData.config, x => x.defaultValue));
                },
                setConfig: (cfgKey, value) => setPluginConfig(id, { [cfgKey]: value }),
                subscribeConfig(cfgKey, cb) {
                    const get = () => Array.isArray(cfgKey) ? (0, misc_1.objFromKeys)(cfgKey, k => this.getConfig(k))
                        : this.getConfig(cfgKey);
                    let last = get();
                    cb(last);
                    const ret = exports.pluginsConfig.sub(() => {
                        const now = get();
                        if ((0, misc_1.same)(now, last))
                            return;
                        try {
                            cb(last = now);
                        }
                        catch (e) {
                            this.log(String(e));
                        }
                    });
                    subbedConfigs.push(ret);
                    return ret;
                },
                async i18n(ctx) {
                    return (0, i18n_1.i18nFromTranslations)(await (0, lang_1.getLangData)(ctx), const_1.EMBEDDED_LANGUAGE);
                },
            });
            const folder = (0, path_1.dirname)(module);
            const { sections, unwatch } = (0, customHtml_1.watchLoadCustomHtml)(folder);
            pluginData.getCustomHtml = () => Object.assign(Object.fromEntries(sections), (0, misc_1.callable)(pluginData.customHtml) || {});
            const unwatchIcons = (0, icons_1.watchIconsFolder)(folder, v => plugin.icons = v);
            const plugin = new Plugin(id, folder, pluginData, async () => {
                unwatchIcons();
                unwatch();
                for (const x of subbedConfigs)
                    x();
                await Promise.allSettled(openDbs.map(x => x.close()));
                openDbs.length = 0;
            });
            pluginReady.resolve();
            if (alreadyRunning)
                events_1.default.emit('pluginUpdated', Object.assign(lodash_1.default.pick(plugin, 'started'), getPluginInfo(id)));
            else {
                const wasInstalled = inactivePlugins[id];
                if (wasInstalled)
                    delete inactivePlugins[id];
                events_1.default.emit(wasInstalled ? 'pluginStarted' : 'pluginInstalled', plugin);
            }
            events_1.default.emit('pluginStarted:' + id);
        }
        catch (e) {
            await markItInactive();
            const parsed = e.stack?.split('\n\n'); // this form is used by syntax-errors inside the plugin, which is useful to show
            const where = parsed?.length > 1 ? `\n${parsed[0]}` : '';
            e = (0, misc_1.prefix)('', e.message, where) || String(e);
            setError(id, e);
        }
        finally {
            starting?.resolve();
            starting = undefined;
        }
    }
}
function customApiCall(method, ...params) {
    return mapPlugins(pl => pl.getData().customApi?.[method]?.(...params));
}
function getError(id) {
    return getPluginInfo(id)?.error;
}
// returns true if there's an error, and it has changed
function setError(id, error) {
    const info = getPluginInfo(id);
    if (!info)
        return;
    if (info.error === error)
        return;
    info.error = error;
    events_1.default.emit('pluginUpdated', info);
    if (!error)
        return;
    console.warn(`Plugin error: ${id}:`, error);
    return true;
}
function deleteModule(id) {
    const { cache } = require;
    // build reversed map of dependencies
    const requiredBy = { '.': ['.'] }; // don't touch main entry
    for (const k in cache)
        if (k !== id)
            for (const child of (0, misc_1.wantArray)(cache[k]?.children))
                (requiredBy[child.id] ||= []).push(k);
    const deleted = [];
    (function deleteCache(id) {
        const mod = cache[id];
        if (!mod)
            return;
        delete cache[id];
        deleted.push(id);
        for (const child of mod.children)
            if (!lodash_1.default.difference(requiredBy[child.id], deleted).length)
                deleteCache(child.id);
    })(id);
}
(0, first_1.onProcessExit)(() => Promise.allSettled(mapPlugins(pl => pl.unload())));
function parsePluginSource(id, source) {
    const pl = { id };
    pl.description = (0, misc_1.tryJson)(/exports.description\s*=\s*(".*")/.exec(source)?.[1]);
    pl.repo = (0, misc_1.tryJson)(/exports.repo\s*=\s*([^\s;]+)/.exec(source)?.[1]);
    pl.version = Number(/exports.version\s*=\s*(\d*\.?\d+)/.exec(source)?.[1]) ?? undefined;
    pl.apiRequired = (0, misc_1.tryJson)(/exports.apiRequired\s*=\s*([ \d.,[\]]+)/.exec(source)?.[1]) ?? undefined;
    pl.isTheme = (0, misc_1.tryJson)(/exports.isTheme\s*=\s*(true|false|"light"|"dark")/.exec(source)?.[1]) ?? (id.endsWith('-theme') || undefined);
    pl.preview = (0, misc_1.tryJson)(/exports.preview\s*=\s*("(?:[^"\\]|\\.)*"|\[[\s\S]*?\])/.exec(source)?.[1]) ?? undefined;
    pl.depend = (0, misc_1.tryJson)(/exports.depend\s*=\s*(\[[\s\S]*?])/m.exec(source)?.[1])?.filter((x) => typeof x.repo === 'string' && x.version === undefined || typeof x.version === 'number'
        || console.warn("Plugin dependency discarded", x));
    pl.changelog = (0, misc_1.tryJson)(/exports.changelog\s*=\s*(\[[\s\S]*?])/m.exec(source)?.[1]);
    if (Array.isArray(pl.apiRequired) && (pl.apiRequired.length !== 2 || !pl.apiRequired.every(lodash_1.default.isFinite))) // validate [from,to] form
        pl.apiRequired = undefined;
    calculateBadApi(pl);
    return pl;
}
function calculateBadApi(data) {
    const r = data.apiRequired;
    const [min = 0, max = Infinity] = Array.isArray(r) ? r : [r]; // normalize data type
    data.badApi = !r ? "missing mandatory property apiRequired"
        : min > const_1.API_VERSION ? "may not work correctly as it is designed for a newer version of HFS - check for updates"
            : min < const_1.COMPATIBLE_API_VERSION || max < const_1.API_VERSION ? "may not work correctly as it is designed for an older version of HFS - check for updates"
                : undefined;
}
function getMissingDependencies(plugin) {
    return (0, misc_1.onlyTruthy)((plugin?.depend || []).map((dep) => {
        const res = findPluginByRepo(dep.repo);
        const error = !res ? 'missing'
            : (res.version || 0) < dep.version ? 'version'
                : !isPluginEnabled(res.id) ? 'disabled'
                    : !isPluginRunning(res.id) ? 'stopped'
                        : '';
        return error && { repo: dep.repo, error, id: res?.id };
    }));
}
