return vm.upgradePackageDefinitions(current, options).spread(async (upgraded, latest) => {
        const {newPkgData, selectedNewDependencies} = await vm.upgradePackageData(pkgData, current, upgraded, latest, options);

        const output = options.jsonAll ? jph.parse(newPkgData) :
            options.jsonDeps ?
                _.pick(jph.parse(newPkgData), 'dependencies', 'devDependencies', 'optionalDependencies') :
                selectedNewDependencies;

        // print
        if (options.json) {
            // use the selectedNewDependencies dependencies data to generate new package data
            // INVARIANT: we don't need try-catch here because pkgData has already been parsed as valid JSON, and vm.upgradePackageData simply does a find-and-replace on that
            printJson(options, output);
        } else {
            printUpgrades(options, {
                current,
                upgraded: selectedNewDependencies,
                latest
            });
        }

        // write

        // TODO: All this is just to get numUpgraded here. This is repeated in printUpgrades.
        const deps = Object.keys(upgraded);
        const satisfied = cint.toObject(deps, dep =>
            cint.keyValue(dep, vm.isSatisfied(latest[dep], current[dep]))
        );
        const isSatisfied = _.propertyOf(satisfied);
        const filteredUpgraded = options.minimal ? cint.filterObject(upgraded, cint.not(isSatisfied)) : upgraded;
        const numUpgraded = Object.keys(filteredUpgraded).length;

        if (numUpgraded > 0) {

            // if error-level is 2, immediately exit with error code
            if (options.errorLevel === 2) {
                programError(options, '\nDependencies not up-to-date');
            }

            // if there is a package file, write the new package data
            // otherwise, suggest ncu -u
            if (pkgFile) {
                if (options.upgrade) {
                    // short-circuit return in order to wait for write operation, but still return the same output
                    return writePackageFile(pkgFile, newPkgData)
                        .then(() => {
                            print(options, `\nRun ${chalk.cyan('npm install')} to install new versions.\n`);
                            return output;
                        });
                } else {
                    print(options, `\nRun ${chalk.cyan('ncu -u')} to upgrade ${getPackageFileName(options)}`);
                }
            }
        }

        return output;
    });
/**
 * @param args.current
 * @param args.upgraded
 * @param args.latest (optional)
 */
function printUpgrades(options, args) {

    // split the deps into satisfied and unsatisfied to display in two separate tables
    const deps = Object.keys(args.upgraded);
    const satisfied = cint.toObject(deps, dep =>
        cint.keyValue(dep, vm.isSatisfied(args.latest[dep], args.current[dep]))
    );

    const isSatisfied = _.propertyOf(satisfied);
    const upgraded = options.minimal ? cint.filterObject(args.upgraded, cint.not(isSatisfied)) : args.upgraded;
    const numUpgraded = Object.keys(upgraded).length;

    print(options, '');

    // print everything is up-to-date
    if (numUpgraded === 0) {
        const smiley = chalk.green.bold(':)');
        if (Object.keys(args.current).length === 0) {
            print(options, 'No dependencies.');
        } else if (options.global) {
            print(options, `All global packages are up-to-date ${smiley}`);
        } else {
            print(options, `All dependencies match the ${vm.getVersionTarget(options)} package versions ${smiley}`);
        }
    }

    // print table
    if (numUpgraded > 0) {
        // ToDo this code seems wrong, there is parameter mismatch
        const table = toDependencyTable({
            from: args.current,
            to: upgraded
        });
        print(options, table.toString());
    }
}
// TODO: printUpgrades and analyzeProjectDependencies need to be refactored. They are tightly coupled and monolithic.
/**
 * @param args.current
 * @param args.upgraded
 * @param args.installed (optional)
 * @param args.latest (optional)
 * @param args.pkgData
 * @param args.pkgFile (optional)
 * @param args.isUpgrade (optional)
 */
function printUpgrades(args) {

    // split the deps into satisfied and unsatisfied to display in two separate tables
    var deps = Object.keys(args.upgraded);
    var satisfied = cint.toObject(deps, function (dep) {
        return cint.keyValue(dep, vm.isSatisfied(args.latest[dep], args.current[dep]));
    });

    var isSatisfied = _.propertyOf(satisfied);
    var satisfiedUpgraded = cint.filterObject(args.upgraded, function (dep) {
        return isSatisfied(dep) && (!args.latest || !args.installed || args.latest[dep] !== args.installed[dep]);
    });
    var unsatisfiedUpgraded = cint.filterObject(args.upgraded, cint.not(isSatisfied));
    var numSatisfied =   Object.keys(satisfiedUpgraded).length;
    var numUnsatisfied = Object.keys(unsatisfiedUpgraded).length;

    print('');

    // print everything is up-to-date
    if (numSatisfied === 0 && numUnsatisfied === 0) {
        var smiley = chalk.yellow(':)');
        if (options.global) {
            print('All global packages are up-to-date ' + smiley);
        } else {
            print('All dependencies match the ' + getVersionTarget(options) + ' package versions ' +
                smiley);
        }
    }

    // print unsatisfied table
    if (numUnsatisfied > 0) {
        var unsatisfiedTable = toDependencyTable({
            from: args.current,
            to: unsatisfiedUpgraded
        }, {
            greatest: options.greatest,
            newest: options.newest
        });
        print(unsatisfiedTable.toString());
    }

    // print satisfied table
    if (numSatisfied > 0) {
        var satisfiedTable = toDependencyTable({
            from: args.current,
            to: satisfiedUpgraded
        }, {
            greatest: options.greatest,
            newest: options.newest
        });
        print((numUnsatisfied > 0 ? '\n' : '') + 'The following dependenc' + (numSatisfied === 1 ? 'y is' : 'ies are') + ' satisfied by ' + (numSatisfied === 1 ? 'its' : 'their') + ' declared version range, but the installed version' + (numSatisfied === 1 ? ' is' : 's are') + ' behind. You can install the latest version' + (numSatisfied === 1 ? '' : 's') + ' without modifying your package file by using ' + chalk.blue(options.packageManager + ' update') + '. If you want to update the dependenc' + (numSatisfied === 1 ? 'y' : 'ies') + ' in your package file anyway, use ' + chalk.cyan('ncu ') + chalk.blue('-a/--upgradeAll') + '.\n');
        print(satisfiedTable.toString());
    }

    var numToUpgrade = numUnsatisfied + (options.upgradeAll ? numSatisfied : 0);

    if (args.pkgFile && numToUpgrade > 0) {
        if (options.errorLevel >= 2) {
            programError('Dependencies not up-to-date');
        } else if (args.isUpgrade) {
            var newPkgData = vm.updatePackageData(args.pkgData, args.current, args.upgraded, args.latest, options);
            writePackageFile(args.pkgFile, newPkgData)
                .then(function () {
                    print('Upgraded ' + args.pkgFile + '\n');
                });
        } else {
            print('\nRun ' + chalk.cyan('ncu') + ' with ' + chalk.blue(numUnsatisfied > 0 ? '-u' : '-a') + ' to upgrade ' + getPackageFileName());
        }
    }

    print('');
}
/** Creates a new object with only the properties of the given that are not undefined. */
function pruned(obj) {
    return _.pick(obj, cint.not(_.isUndefined))
}