"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const common_1 = require("@nestjs/common");
const os = require("os");
const _ = require("lodash");
const path = require("path");
const fs = require("fs-extra");
const child_process = require("child_process");
const semver = require("semver");
const rp = require("request-promise-native");
const color = require("bash-color");
const pty = require("node-pty-prebuilt-multiarch");
const logger_service_1 = require("../../core/logger/logger.service");
const config_service_1 = require("../../core/config/config.service");
let PluginsService = class PluginsService {
    constructor(configService, logger) {
        this.configService = configService;
        this.logger = logger;
        this.npm = this.getNpmPath();
        this.paths = this.getBasePaths();
        this.rp = rp.defaults({
            json: true,
            headers: {
                'User-Agent': this.configService.package.name,
            },
            timeout: 5000,
        });
    }
    getInstalledPlugins() {
        return __awaiter(this, void 0, void 0, function* () {
            const plugins = [];
            const modules = yield this.getInstalledModules();
            const homebridgePlugins = modules
                .filter(module => (module.name.indexOf('homebridge-') === 0))
                .filter((module) => __awaiter(this, void 0, void 0, function* () { return (yield fs.pathExists(path.join(module.installPath, 'package.json')).catch(x => null)); }))
                .filter(x => x);
            yield Promise.all(homebridgePlugins.map((pkg) => __awaiter(this, void 0, void 0, function* () {
                try {
                    const pjson = yield fs.readJson(path.join(pkg.installPath, 'package.json'));
                    if (pjson.keywords && pjson.keywords.includes('homebridge-plugin')) {
                        const plugin = yield this.parsePackageJson(pjson, pkg.path);
                        if (!plugins.find(x => plugin.name === x.name)) {
                            plugins.push(plugin);
                        }
                        else if (!plugin.globalInstall && plugins.find(x => plugin.name === x.name && x.globalInstall === true)) {
                            const index = plugins.findIndex(x => plugin.name === x.name && x.globalInstall === true);
                            plugins[index] = plugin;
                        }
                    }
                }
                catch (e) {
                    this.logger.error(`Failed to parse plugin "${pkg.name}": ${e.message}`);
                }
            })));
            this.installedPlugins = plugins;
            return _.orderBy(plugins, ['updateAvailable', 'name'], ['desc', 'asc']);
        });
    }
    getOutOfDatePlugins() {
        return __awaiter(this, void 0, void 0, function* () {
            const plugins = yield this.getInstalledPlugins();
            return plugins.filter(x => x.updateAvailable);
        });
    }
    searchNpmRegistry(query) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.installedPlugins) {
                yield this.getInstalledPlugins();
            }
            const q = ((!query || !query.length) ? '' : query + '+') + 'keywords:homebridge-plugin+not:deprecated&size=30';
            const searchResults = yield this.rp.get(`https://registry.npmjs.org/-/v1/search?text=${q}`);
            const result = searchResults.objects
                .filter(x => x.package.name.indexOf('homebridge-') === 0)
                .map((pkg) => {
                let plugin = {
                    name: pkg.package.name,
                };
                const isInstalled = this.installedPlugins.find(x => x.name === plugin.name);
                if (isInstalled) {
                    plugin = isInstalled;
                    return plugin;
                }
                plugin.publicPackage = true;
                plugin.installedVersion = null;
                plugin.latestVersion = pkg.package.version;
                plugin.lastUpdated = pkg.package.date;
                plugin.description = (pkg.package.description) ?
                    pkg.package.description.replace(/(?:https?|ftp):\/\/[\n\S]+/g, '').trim() : pkg.package.name;
                plugin.links = pkg.package.links;
                plugin.author = (pkg.package.publisher) ? pkg.package.publisher.username : null;
                plugin.certifiedPlugin = (pkg.package.name.indexOf('@homebridge/homebridge-') === 0);
                return plugin;
            });
            if (!result.length && query.indexOf('homebridge-') === 0) {
                return yield this.searchNpmRegistrySingle(query);
            }
            return result;
        });
    }
    searchNpmRegistrySingle(query) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const pkg = yield this.rp.get(`https://registry.npmjs.org/${encodeURIComponent(query).replace('%40', '@')}`);
                if (!pkg.keywords || !pkg.keywords.includes('homebridge-plugin')) {
                    return [];
                }
                let plugin;
                const isInstalled = this.installedPlugins.find(x => x.name === pkg.name);
                if (isInstalled) {
                    plugin = isInstalled;
                    return [plugin];
                }
                plugin = {
                    name: pkg.name,
                    description: (pkg.description) ?
                        pkg.description.replace(/(?:https?|ftp):\/\/[\n\S]+/g, '').trim() : pkg.name,
                    certifiedPlugin: (pkg.name.indexOf('@homebridge/homebridge-') === 0),
                };
                plugin.publicPackage = true;
                plugin.latestVersion = pkg['dist-tags'].latest;
                plugin.lastUpdated = pkg.package.date;
                plugin.updateAvailable = false;
                plugin.links = {
                    npm: `https://www.npmjs.com/package/${plugin.name}`,
                    homepage: pkg.homepage,
                    bugs: (pkg.bugs) ? pkg.bugs.url : null,
                };
                plugin.author = (pkg.maintainers.length) ? pkg.maintainers[0].name : null;
                plugin.certifiedPlugin = (pkg.name.indexOf('@homebridge/homebridge-') === 0);
                return [plugin];
            }
            catch (e) {
                if (e.statusCode !== 404) {
                    this.logger.error('Failed to search npm registry');
                    this.logger.error(e.message);
                }
                return [];
            }
        });
    }
    installPlugin(pluginName, client) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.getInstalledPlugins();
            let installPath = (this.configService.customPluginPath) ?
                this.configService.customPluginPath : this.installedPlugins.find(x => x.name === this.configService.name).installPath;
            const installOptions = [];
            if (installPath === this.configService.customPluginPath && (yield fs.pathExists(path.resolve(installPath, '../package.json')))) {
                installOptions.push('--save');
            }
            installPath = path.resolve(installPath, '../');
            yield this.runNpmCommand([...this.npm, 'install', '--unsafe-perm', ...installOptions, `${pluginName}@latest`], installPath, client);
            return true;
        });
    }
    uninstallPlugin(pluginName, client) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.getInstalledPlugins();
            const plugin = this.installedPlugins.find(x => x.name === pluginName);
            if (!plugin) {
                throw new Error(`Plugin "${pluginName}" Not Found`);
            }
            let installPath = plugin.installPath;
            const installOptions = [];
            if (installPath === this.configService.customPluginPath && (yield fs.pathExists(path.resolve(installPath, '../package.json')))) {
                installOptions.push('--save');
            }
            installPath = path.resolve(installPath, '../');
            yield this.runNpmCommand([...this.npm, 'uninstall', '--unsafe-perm', ...installOptions, pluginName], installPath, client);
            yield this.ensureCustomPluginDirExists();
            return true;
        });
    }
    updatePlugin(pluginName, client) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.getInstalledPlugins();
            const plugin = this.installedPlugins.find(x => x.name === pluginName);
            if (!plugin) {
                throw new Error(`Plugin "${pluginName}" Not Found`);
            }
            let installPath = plugin.installPath;
            const installOptions = [];
            if (installPath === this.configService.customPluginPath && (yield fs.pathExists(path.resolve(installPath, '../package.json')))) {
                installOptions.push('--save');
            }
            installPath = path.resolve(installPath, '../');
            yield this.runNpmCommand([...this.npm, 'install', '--unsafe-perm', ...installOptions, `${pluginName}@latest`], installPath, client);
            return true;
        });
    }
    getHomebridgePackage() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.configService.ui.homebridgePackagePath) {
                const pjsonPath = path.join(this.configService.ui.homebridgePackagePath, 'package.json');
                if (yield fs.pathExists(pjsonPath)) {
                    return yield this.parsePackageJson(yield fs.readJson(pjsonPath), this.configService.ui.homebridgePackagePath);
                }
                else {
                    this.logger.error(`"homebridgePath" (${this.configService.ui.homebridgePackagePath}) does not exist`);
                }
            }
            const modules = yield this.getInstalledModules();
            const homebridgeInstalls = modules.filter(x => x.name === 'homebridge');
            if (homebridgeInstalls.length > 1) {
                this.logger.warn('Multiple Instances Of Homebridge Found Installed');
                homebridgeInstalls.forEach((instance) => {
                    this.logger.warn(instance.installPath);
                });
            }
            if (!homebridgeInstalls.length) {
                this.logger.error('Unable To Find Homebridge Installation');
                throw new Error('Unable To Find Homebridge Installation');
            }
            const homebridgeModule = homebridgeInstalls[0];
            const pjson = yield fs.readJson(path.join(homebridgeModule.installPath, 'package.json'));
            const homebridge = yield this.parsePackageJson(pjson, homebridgeModule.path);
            return homebridge;
        });
    }
    updateHomebridgePackage(client) {
        return __awaiter(this, void 0, void 0, function* () {
            const homebridge = yield this.getHomebridgePackage();
            let installPath = homebridge.installPath;
            const installOptions = [];
            if (installPath === this.configService.customPluginPath && (yield fs.pathExists(path.resolve(installPath, '../package.json')))) {
                installOptions.push('--save');
            }
            installPath = path.resolve(installPath, '../');
            yield this.runNpmCommand([...this.npm, 'install', '--unsafe-perm', ...installOptions, `${homebridge.name}@latest`], installPath, client);
            return true;
        });
    }
    getPluginConfigSchema(pluginName) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.installedPlugins)
                yield this.getInstalledPlugins();
            const plugin = this.installedPlugins.find(x => x.name === pluginName);
            if (!plugin) {
                throw new common_1.NotFoundException();
            }
            if (!plugin.settingsSchema) {
                throw new common_1.NotFoundException();
            }
            const schemaPath = path.resolve(plugin.installPath, pluginName, 'config.schema.json');
            const configSchema = yield fs.readJson(schemaPath);
            if (pluginName === this.configService.name) {
                configSchema.schema.properties.port.default = this.configService.ui.port;
            }
            if (pluginName === 'homebridge-alexa') {
                configSchema.schema.properties.pin.default = this.configService.homebridgeConfig.bridge.pin;
            }
            return configSchema;
        });
    }
    getPluginChangeLog(pluginName) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.getInstalledPlugins();
            const plugin = this.installedPlugins.find(x => x.name === pluginName);
            if (!plugin) {
                throw new common_1.NotFoundException();
            }
            const changeLog = path.resolve(plugin.installPath, plugin.name, 'CHANGELOG.md');
            if (yield fs.pathExists(changeLog)) {
                return {
                    changelog: yield fs.readFile(changeLog, 'utf8'),
                };
            }
            else {
                throw new common_1.NotFoundException();
            }
        });
    }
    getPluginRelease(pluginName) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.installedPlugins)
                yield this.getInstalledPlugins();
            const plugin = this.installedPlugins.find(x => x.name === pluginName);
            if (!plugin) {
                throw new common_1.NotFoundException();
            }
            if (!plugin.links.homepage) {
                throw new common_1.NotFoundException();
            }
            if (!plugin.links.homepage.match(/https:\/\/github.com/)) {
                throw new common_1.NotFoundException();
            }
            try {
                const repo = plugin.links.homepage.split('https://github.com/')[1].split('#readme')[0];
                const release = yield this.rp.get(`https://api.github.com/repos/${repo}/releases/latest`);
                return {
                    name: release.name,
                    changelog: release.body,
                };
            }
            catch (e) {
                throw new common_1.NotFoundException();
            }
        });
    }
    getInstalledModules() {
        return __awaiter(this, void 0, void 0, function* () {
            const allModules = [];
            for (const requiredPath of this.paths) {
                const modules = yield fs.readdir(requiredPath);
                for (const module of modules) {
                    allModules.push({
                        name: module,
                        installPath: path.join(requiredPath, module),
                        path: requiredPath,
                    });
                }
            }
            return allModules;
        });
    }
    getNpmPath() {
        if (os.platform() === 'win32') {
            const windowsNpmPath = [
                path.join(process.env.APPDATA, 'npm/npm.cmd'),
                path.join(process.env.ProgramFiles, 'nodejs/npm.cmd'),
            ]
                .filter(fs.existsSync);
            if (windowsNpmPath.length) {
                return [windowsNpmPath[0], '--no-update-notifier'];
            }
            else {
                this.logger.error(`ERROR: Cannot find npm binary. You will not be able to manage plugins or update homebridge.`);
                this.logger.error(`ERROR: You might be able to fix this problem by running: npm install -g npm`);
            }
        }
        return ['npm', '--no-update-notifier'];
    }
    getBasePaths() {
        let paths = [];
        paths = paths.concat(eval('require').main.paths);
        if (this.configService.customPluginPath) {
            paths.unshift(this.configService.customPluginPath);
        }
        if (process.env.NODE_PATH) {
            paths = process.env.NODE_PATH.split(path.delimiter)
                .filter((p) => !!p)
                .concat(paths);
        }
        else {
            if ((os.platform() === 'win32')) {
                paths.push(path.join(process.env.APPDATA, 'npm/node_modules'));
            }
            else {
                paths.push('/usr/local/lib/node_modules');
                paths.push('/usr/lib/node_modules');
                paths.push(child_process.execSync('/bin/echo -n "$(npm --no-update-notifier -g prefix)/lib/node_modules"').toString('utf8'));
            }
        }
        return _.uniq(paths).filter((requiredPath) => {
            return fs.existsSync(requiredPath);
        });
    }
    parsePackageJson(pjson, installPath) {
        return __awaiter(this, void 0, void 0, function* () {
            const plugin = {
                name: pjson.name,
                displayName: pjson.displayName,
                description: (pjson.description) ?
                    pjson.description.replace(/(?:https?|ftp):\/\/[\n\S]+/g, '').trim() : pjson.name,
                certifiedPlugin: (pjson.name.indexOf('@homebridge/homebridge-') === 0),
                installedVersion: installPath ? (pjson.version || '0.0.1') : null,
                globalInstall: (installPath !== this.configService.customPluginPath),
                settingsSchema: yield fs.pathExists(path.resolve(installPath, pjson.name, 'config.schema.json')),
                installPath,
            };
            return this.getPluginFromNpm(plugin);
        });
    }
    getPluginFromNpm(plugin) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const pkg = yield this.rp.get(`https://registry.npmjs.org/${encodeURIComponent(plugin.name).replace('%40', '@')}`);
                plugin.publicPackage = true;
                plugin.latestVersion = pkg['dist-tags'].latest;
                plugin.updateAvailable = semver.lt(plugin.installedVersion, plugin.latestVersion);
                plugin.links = {
                    npm: `https://www.npmjs.com/package/${plugin.name}`,
                    homepage: pkg.homepage,
                    bugs: (pkg.bugs) ? pkg.bugs.url : null,
                };
                plugin.author = (pkg.maintainers.length) ? pkg.maintainers[0].name : null;
            }
            catch (e) {
                if (e.statusCode !== 404) {
                    this.logger.error(`[${plugin.name}] ${e.message}`);
                }
                plugin.publicPackage = false;
                plugin.latestVersion = null;
                plugin.updateAvailable = false;
                plugin.links = {};
            }
            return plugin;
        });
    }
    runNpmCommand(command, cwd, client) {
        return __awaiter(this, void 0, void 0, function* () {
            let timeoutTimer;
            command = command.filter(x => x.length);
            if (this.configService.ui.sudo) {
                command.unshift('sudo', '-E', '-n');
            }
            else {
                try {
                    yield fs.access(cwd, fs.constants.W_OK);
                }
                catch (e) {
                    client.emit('stdout', color.yellow(`The user "${os.userInfo().username}" does not have write access to the target directory:\n\r\n\r`));
                    client.emit('stdout', `${cwd}\n\r\n\r`);
                    client.emit('stdout', color.yellow(`This may cause the operation to fail.\n\r`));
                    client.emit('stdout', color.yellow(`See the docs for details on how to enable sudo mode:\n\r`));
                    client.emit('stdout', color.yellow(`https://github.com/oznu/homebridge-config-ui-x#sudo-mode\n\r\n\r`));
                }
            }
            this.logger.log(`Running Command: ${command.join(' ')}`);
            if (!semver.satisfies(process.version, `>=${this.configService.minimumNodeVersion}`)) {
                client.emit('stdout', color.yellow(`Node.js v${this.configService.minimumNodeVersion} higher is required for ${this.configService.name}.\n\r`));
                client.emit('stdout', color.yellow(`You may experience issues while running on Node.js ${process.version}.\n\r\n\r`));
            }
            client.emit('stdout', color.cyan(`USER: ${os.userInfo().username}\n\r`));
            client.emit('stdout', color.cyan(`DIR: ${cwd}\n\r`));
            client.emit('stdout', color.cyan(`CMD: ${command.join(' ')}\n\r\n\r`));
            yield new Promise((resolve, reject) => {
                const term = pty.spawn(command.shift(), command, {
                    name: 'xterm-color',
                    cols: 80,
                    rows: 30,
                    cwd,
                    env: process.env,
                });
                term.on('data', (data) => {
                    client.emit('stdout', data);
                });
                term.on('exit', (code) => {
                    if (code === 0) {
                        clearTimeout(timeoutTimer);
                        client.emit('stdout', color.green(`\n\rCommand succeeded!.\n\r`));
                        resolve();
                    }
                    else {
                        clearTimeout(timeoutTimer);
                        reject('Command failed. Please review log for details.');
                    }
                });
                timeoutTimer = setTimeout(() => {
                    term.kill('SIGTERM');
                }, 300000);
            });
        });
    }
    ensureCustomPluginDirExists() {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.configService.customPluginPath) {
                return;
            }
            if (!(yield fs.pathExists(this.configService.customPluginPath))) {
                this.logger.warn(`Custom plugin directory was removed. Re-creating: ${this.configService.customPluginPath}`);
                try {
                    yield fs.ensureDir(this.configService.customPluginPath);
                }
                catch (e) {
                    this.logger.error(`Failed to recreate custom plugin directory`);
                    this.logger.error(e.message);
                }
            }
        });
    }
};
PluginsService = __decorate([
    common_1.Injectable(),
    __metadata("design:paramtypes", [config_service_1.ConfigService,
        logger_service_1.Logger])
], PluginsService);
exports.PluginsService = PluginsService;
//# sourceMappingURL=plugins.service.js.map