コード例 #1
0
ファイル: main.py プロジェクト: timcharper/dcos-cli
def _update():
    """
    :returns: Deprecation notice
    :rtype: str
    """

    get_package_manager()
    notice = ("This command has been deprecated. "
              "Repositories will be automatically updated after they are added"
              " by `dcos package repo add`")
    raise DCOSException(notice)
コード例 #2
0
def _uninstall(package_name, remove_all, app_id, cli, app, skip_confirmation):
    """Uninstall the specified package.

    :param package_name: The package to uninstall
    :type package_name: str
    :param remove_all: Whether to remove all instances of the named package
    :type remove_all: boolean
    :param app_id: App ID of the package instance to uninstall
    :type app_id: str
    :param skip_confirmation: Skip confirmation for uninstall
    :type skip_confirmation: boolean
    :returns: Process status
    :rtype: int
    """

    # Don't gate CLI uninstalls.
    # Don't gate uninstalls if the user wants to skip confirmation.
    if not cli and not skip_confirmation and not _confirm_uninstall(
            package_name, remove_all, app_id):
        return 0

    package_manager = get_package_manager()
    err = package.uninstall(package_manager, package_name, remove_all, app_id,
                            cli, app)
    if err is not None:
        emitter.publish(err)
        return 1

    return 0
コード例 #3
0
ファイル: main.py プロジェクト: sschneid/dcos-cli
def _add(json, dcos_package, package_name, package_version):
    """
    Add a DC/OS package to DC/OS

    :param json: wether to output json
    :type json: bool
    :param dcos_package: path to the DC/OS package
    :type dcos_package: None | str
    :param package_name: the name of a remote DC/OS package
    :type package_name: None | str
    :param package_version: the version of a remote DC/OS package
    :type package_version: None | str
    :return: process status
    :rtype: int
    """
    package_manager = get_package_manager()
    if dcos_package:
        response = package_manager.package_add_local(dcos_package)
    else:
        response = (package_manager
                    .package_add_remote(package_name, package_version))

    response_json = response.json()

    if json:
        emitter.publish(response_json)
    else:
        message = (
            'The package [{}] version [{}] has been added to DC/OS'.format(
                response_json['name'], response_json['version']))
        emitter.publish(message)

    return 0
コード例 #4
0
ファイル: main.py プロジェクト: timcharper/dcos-cli
def _add(json, dcos_package, package_name, package_version):
    """
    Adds a DC/OS package to DC/OS

    :param json: wether to output json
    :type json: bool
    :param dcos_package: path to the DC/OS package
    :type dcos_package: None | str
    :param package_name: the name of a remote DC/OS package
    :type package_name: None | str
    :param package_version: the version of a remote DC/OS package
    :type package_version: None | str
    :return: process status
    :rtype: int
    """
    package_manager = get_package_manager()
    if dcos_package:
        response = package_manager.package_add_local(dcos_package)
    else:
        response = (package_manager.package_add_remote(package_name,
                                                       package_version))

    response_json = response.json()

    if json:
        emitter.publish(response_json)
    else:
        message = (
            'The package [{}] version [{}] has been added to DC/OS'.format(
                response_json['name'], response_json['version']))
        emitter.publish(message)

    return 0
コード例 #5
0
ファイル: main.py プロジェクト: sschneid/dcos-cli
def _search(json_, query):
    """Search for matching packages.

    :param json_: output json if True
    :type json_: bool
    :param query: The search term
    :type query: str
    :returns: Process status
    :rtype: int
    """

    if not query:
        query = ''

    package_manager = get_package_manager()
    results = package_manager.search_sources(query)

    if json_ or results['packages']:
        emitting.publish_table(emitter,
                               results,
                               tables.package_search_table,
                               json_)
    else:
        raise DCOSException('No packages found.')
    return 0
コード例 #6
0
def _list_repos(is_json):
    """List configured package repositories.

    :param is_json: output json if True
    :type is_json: bool
    :returns: Process status
    :rtype: int
    """

    package_manager = get_package_manager()
    repos = package_manager.get_repos()

    if is_json:
        return emitter.publish(repos)
    elif repos.get("repositories"):
        repos = [
            "{}: {}".format(repo.get("name"), repo.get("uri"))
            for repo in repos.get("repositories")
        ]
        emitter.publish("\n".join(repos))
    else:
        msg = ("There are currently no repos configured. "
               "Please use `dcos package repo add` to add a repo")
        raise DCOSException(msg)

    return 0
コード例 #7
0
ファイル: main.py プロジェクト: sschneid/dcos-cli
def _list(json_, app_id, cli_only, package_name):
    """List installed apps

    :param json_: output json if True
    :type json_: bool
    :param app_id: App ID of app to show
    :type app_id: str
    :param cli_only: if True, only show packages with installed subcommands
    :type cli_only: bool
    :param package_name: The package to show
    :type package_name: str
    :returns: process return code
    :rtype: int
    """

    package_manager = get_package_manager()
    if app_id is not None:
        app_id = util.normalize_marathon_id_path(app_id)
    results = package.installed_packages(
        package_manager, app_id, package_name, cli_only)

    # only emit those packages that match the provided package_name and app_id
    if results or json_:
        emitting.publish_table(emitter, results, tables.package_table, json_)
    else:
        msg = ("There are currently no installed packages. "
               "Please use `dcos package install` to install a package.")
        raise DCOSException(msg)
    return 0
コード例 #8
0
def _import_repos(repos_file):
    """Add package repos from a JSON file

    :param repos_file: path to JSON file containing repos.
    :type repos_file: str
    :rtype: int
    """

    package_manager = get_package_manager()

    with open(repos_file) as f:
        try:
            data = json.load(f)
        except json.JSONDecodeError:
            _raise_invalid_repos_file()
        repositories = data.get('repositories')
        if not repositories:
            _raise_invalid_repos_file()

        for index, repo in enumerate(data['repositories']):
            repo_name = repo.get('name')
            repo_uri = repo.get('uri')
            if not repo_name or not repo_uri:
                emitter.publish('Repo missing name or uri. Skipping.')
                continue
            try:
                package_manager.add_repo(repo['name'], repo['uri'], index)
                emitter.publish('Added repo "%s" (%s) at index %d' %
                                (repo_name, repo_uri, index))
            except DCOSException as e:
                emitter.publish('Error (%s) while adding repo "%s" (%s). '
                                'Skipping.' % (e, repo_name, repo_uri))
                continue

    return 0
コード例 #9
0
def _list(json_, app_id, cli_only, package_name):
    """List installed apps

    :param json_: output json if True
    :type json_: bool
    :param app_id: App ID of app to show
    :type app_id: str
    :param cli_only: if True, only show packages with installed subcommands
    :type cli_only: bool
    :param package_name: The package to show
    :type package_name: str
    :returns: process return code
    :rtype: int
    """

    package_manager = get_package_manager()
    if app_id is not None:
        app_id = util.normalize_marathon_id_path(app_id)
    results = package.installed_packages(package_manager, app_id, package_name,
                                         cli_only)

    # only emit those packages that match the provided package_name and app_id
    if results or json_:
        emitting.publish_table(emitter, results, tables.package_table, json_)
    else:
        msg = ("There are currently no installed packages. "
               "Please use `dcos package install` to install a package.")
        raise DCOSException(msg)
    return 0
コード例 #10
0
ファイル: main.py プロジェクト: jeffwhite530/dcos-core-cli
def _describe(package_name, app, cli, options_path, render, package_versions,
              package_version, config, app_id):
    """Describe the specified package.

    :param package_name: The package to describe
    :type package_name: str
    :param app: If True, marathon.json will be printed
    :type app: boolean
    :param cli: If True, command.json | resource.json's cli property should
                be printed
    :type cli: boolean
    :param options_path: Path to json file with options to override
                         config.json defaults.
    :type options_path: str
    :param render: If True, marathon.json will be rendered
    :type render: boolean
    :param package_versions: If True, a list of all package versions will
                             be printed
    :type package_versions: boolean
    :param package_version: package version
    :type package_version: str | None
    :param config: If True, config.json will be printed
    :type config: boolean
    :param app_id: app ID to specify which app to describe
    :type app_id: str | None
    :returns: Process status
    :rtype: int
    """

    # If the user supplied template options, they definitely want to
    # render the template
    if options_path:
        render = True

    # Fail early if options file isn't valid
    user_options = util.read_file_json(options_path)

    package_manager = get_package_manager()
    pkg = package_manager.get_package_version(package_name, package_version)

    if package_versions:
        emitter.publish(pkg.package_versions())
    elif cli or app or config:
        if cli:
            emitter.publish(pkg.cli_definition())
        if app:
            if render:
                app_output = pkg.marathon_json(user_options, app_id)
            else:
                app_output = pkg.marathon_template().decode("UTF-8")
                if app_output and app_output[-1] == '\n':
                    app_output = app_output[:-1]
            emitter.publish(app_output)
        if config:
            config_output = pkg.config_json()
            emitter.publish(config_output)
    else:
        emitter.publish(pkg.package_response())

    return 0
コード例 #11
0
ファイル: main.py プロジェクト: timcharper/dcos-cli
def _search(json_, query):
    """Search for matching packages.

    :param json_: output json if True
    :type json_: bool
    :param query: The search term
    :type query: str
    :returns: Process status
    :rtype: int
    """

    if not query:
        query = ''

    package_manager = get_package_manager()
    results = package_manager.search_sources(query)

    if json_ or results['packages']:
        emitting.publish_table(emitter,
                               results,
                               tables.package_search_table,
                               json_)
    else:
        raise DCOSException('No packages found.')
    return 0
コード例 #12
0
ファイル: main.py プロジェクト: timcharper/dcos-cli
def _remove_repo(repo_name):
    """Remove package repo and update repo with new repo

    :param repo_name: name to call repo
    :type repo_name: str
    :returns: Process status
    :rtype: int
    """

    package_manager = get_package_manager()
    package_manager.remove_repo(repo_name)

    return 0
コード例 #13
0
def _remove_repo(repo_names):
    """Remove package repo and update repo with new repo

    :param repo_names: names of repos
    :type repo_name: [str]
    :returns: Process status
    :rtype: int
    """

    package_manager = get_package_manager()
    for repo_name in repo_names:
        package_manager.remove_repo(repo_name)

    return 0
コード例 #14
0
ファイル: main.py プロジェクト: sschneid/dcos-cli
def _remove_repo(repo_names):
    """Remove package repo and update repo with new repo

    :param repo_names: names of repos
    :type repo_name: [str]
    :returns: Process status
    :rtype: int
    """

    package_manager = get_package_manager()
    for repo_name in repo_names:
        package_manager.remove_repo(repo_name)

    return 0
コード例 #15
0
def _add_repo(repo_name, repo_url, index):
    """Add package repo and update repo with new repo

    :param repo_name: name to call repo
    :type repo_name: str
    :param repo_url: location of repo to add
    :type repo_url: str
    :param index: index to add this repo
    :type index: int
    :rtype: None
    """

    package_manager = get_package_manager()
    package_manager.add_repo(repo_name, repo_url, index)

    return 0
コード例 #16
0
ファイル: main.py プロジェクト: sschneid/dcos-cli
def _add_repo(repo_name, repo_url, index):
    """Add package repo and update repo with new repo

    :param repo_name: name to call repo
    :type repo_name: str
    :param repo_url: location of repo to add
    :type repo_url: str
    :param index: index to add this repo
    :type index: int
    :rtype: None
    """

    package_manager = get_package_manager()
    package_manager.add_repo(repo_name, repo_url, index)

    return 0
コード例 #17
0
ファイル: main.py プロジェクト: sschneid/dcos-cli
def _uninstall(package_name, remove_all, app_id, cli, app):
    """Uninstall the specified package.

    :param package_name: The package to uninstall
    :type package_name: str
    :param remove_all: Whether to remove all instances of the named package
    :type remove_all: boolean
    :param app_id: App ID of the package instance to uninstall
    :type app_id: str
    :returns: Process status
    :rtype: int
    """

    package_manager = get_package_manager()
    err = package.uninstall(
        package_manager, package_name, remove_all, app_id, cli, app)
    if err is not None:
        emitter.publish(err)
        return 1

    return 0
コード例 #18
0
ファイル: main.py プロジェクト: timcharper/dcos-cli
def _uninstall(package_name, remove_all, app_id, cli, app):
    """Uninstall the specified package.

    :param package_name: The package to uninstall
    :type package_name: str
    :param remove_all: Whether to remove all instances of the named package
    :type remove_all: boolean
    :param app_id: App ID of the package instance to uninstall
    :type app_id: str
    :returns: Process status
    :rtype: int
    """

    package_manager = get_package_manager()
    err = package.uninstall(
        package_manager, package_name, remove_all, app_id, cli, app)
    if err is not None:
        emitter.publish(err)
        return 1

    return 0
コード例 #19
0
ファイル: main.py プロジェクト: kvish/dcos-core-cli
def _uninstall(package_name, remove_all, app_id, cli, app, skip_confirmation, manager_id):
    """Uninstall the specified package.

    :param package_name: The package to uninstall
    :type package_name: str
    :param remove_all: Whether to remove all instances of the named package
    :type remove_all: boolean
    :param app_id: App ID of the package instance to uninstall
    :type app_id: str
    :param skip_confirmation: Skip confirmation for uninstall
    :type skip_confirmation: boolean
    :returns: Process status
    :rtype: int
    :param manager_id: defines the custom manager to forward this operation to
    :manager_id: str

    """

    package_manager = get_package_manager()

    # Don't gate CLI uninstalls.
    # Don't gate uninstalls if the user wants to skip confirmation.
    if not cli and not skip_confirmation:
        installed = package.installed_packages(
            package_manager, None, package_name, cli_only=False)
        installed_pkg = next(iter(installed), None)
        if not installed_pkg or not installed_pkg.get('name'):
            msg = "Package '{}' is not installed.".format(package_name)
            emitter.publish(msg)
            return 1

        if not _confirm_uninstall(installed_pkg, remove_all, app_id):
            return 0

    package.uninstall(
        package_manager, package_name, remove_all, app_id, cli, app, manager_id)

    return 0
コード例 #20
0
ファイル: main.py プロジェクト: sschneid/dcos-cli
def _import_repos(repos_file):
    """Add package repos from a JSON file

    :param repos_file: path to JSON file containing repos.
    :type repos_file: str
    :rtype: int
    """

    package_manager = get_package_manager()

    with open(repos_file) as f:
        try:
            data = json.load(f)
        except json.JSONDecodeError:
            _raise_invalid_repos_file()
        repositories = data.get('repositories')
        if not repositories:
            _raise_invalid_repos_file()

        for index, repo in enumerate(data['repositories']):
            repo_name = repo.get('name')
            repo_uri = repo.get('uri')
            if not repo_name or not repo_uri:
                emitter.publish('Repo missing name or uri. Skipping.')
                continue
            try:
                package_manager.add_repo(repo['name'], repo['uri'], index)
                emitter.publish('Added repo "%s" (%s) at index %d'
                                % (repo_name, repo_uri, index))
            except DCOSException as e:
                emitter.publish('Error (%s) while adding repo "%s" (%s). '
                                'Skipping.'
                                % (e, repo_name, repo_uri))
                continue

    return 0
コード例 #21
0
ファイル: main.py プロジェクト: sschneid/dcos-cli
def _list_repos(is_json):
    """List configured package repositories.

    :param is_json: output json if True
    :type is_json: bool
    :returns: Process status
    :rtype: int
    """

    package_manager = get_package_manager()
    repos = package_manager.get_repos()

    if is_json:
        return emitter.publish(repos)
    elif repos.get("repositories"):
        repos = ["{}: {}".format(repo.get("name"), repo.get("uri"))
                 for repo in repos.get("repositories")]
        emitter.publish("\n".join(repos))
    else:
        msg = ("There are currently no repos configured. "
               "Please use `dcos package repo add` to add a repo")
        raise DCOSException(msg)

    return 0
コード例 #22
0
ファイル: main.py プロジェクト: sschneid/dcos-cli
def _describe(package_name,
              app,
              cli,
              options_path,
              render,
              package_versions,
              package_version,
              config):
    """Describe the specified package.

    :param package_name: The package to describe
    :type package_name: str
    :param app: If True, marathon.json will be printed
    :type app: boolean
    :param cli: If True, command.json | resource.json's cli property should
                be printed
    :type cli: boolean
    :param options_path: Path to json file with options to override
                         config.json defaults.
    :type options_path: str
    :param render: If True, marathon.json will be rendered
    :type render: boolean
    :param package_versions: If True, a list of all package versions will
                             be printed
    :type package_versions: boolean
    :param package_version: package version
    :type package_version: str | None
    :param config: If True, config.json will be printed
    :type config: boolean
    :returns: Process status
    :rtype: int
    """

    # If the user supplied template options, they definitely want to
    # render the template
    if options_path:
        render = True

    # Fail early if options file isn't valid
    user_options = util.read_file_json(options_path)

    package_manager = get_package_manager()
    pkg = package_manager.get_package_version(package_name, package_version)

    pkg_json = pkg.package_json()

    if package_versions:
        emitter.publish(pkg.package_versions())
    elif cli or app or config:
        if cli:
            emitter.publish(pkg.cli_definition())
        if app:
            if render:
                app_output = pkg.marathon_json(user_options)
            else:
                app_output = pkg.marathon_template()
                if app_output and app_output[-1] == '\n':
                    app_output = app_output[:-1]
            emitter.publish(app_output)
        if config:
            config_output = pkg.config_json()
            emitter.publish(config_output)
    else:
        emitter.publish(pkg_json)

    return 0
コード例 #23
0
def _install(package_name, package_version, options_path, app_id, cli, global_,
             app, yes):
    """Install the specified package.

    :param package_name: the package to install
    :type package_name: str
    :param package_version: package version to install
    :type package_version: str
    :param options_path: path to file containing option values
    :type options_path: str
    :param app_id: app ID for installation of this package
    :type app_id: str
    :param cli: indicates if the cli should be installed
    :type cli: bool
    :param global_: indicates that the cli should be installed globally
    :type global_: bool
    :param app: indicate if the application should be installed
    :type app: bool
    :param yes: automatically assume yes to all prompts
    :type yes: bool
    :returns: process status
    :rtype: int
    """

    if cli is False and app is False:
        # Install both if neither flag is specified
        cli = app = True

    # Fail early if options file isn't valid
    user_options = util.read_file_json(options_path)

    package_manager = get_package_manager()
    pkg = package_manager.get_package_version(package_name, package_version)

    pkg_json = pkg.package_json()

    selected = pkg_json.get('selected')
    if selected:
        link = ('https://mesosphere.com/'
                'catalog-terms-conditions/#certified-services')
    else:
        link = ('https://mesosphere.com/'
                'catalog-terms-conditions/#community-services')
    emitter.publish(('By Deploying, you agree to '
                     'the Terms and Conditions ' + link))

    pre_install_notes = pkg_json.get('preInstallNotes')
    if app and pre_install_notes:
        emitter.publish(pre_install_notes)

    if not confirm('Continue installing?', yes):
        emitter.publish('Exiting installation.')
        return 0

    if app and pkg.marathon_template():

        # Even though package installation will check for template rendering
        # errors, we want to fail early, before trying to install.
        pkg.options(user_options)

        # Install in Marathon
        msg = 'Installing Marathon app for package [{}] version [{}]'.format(
            pkg.name(), pkg.version())
        if app_id is not None:
            msg += ' with app id [{}]'.format(app_id)

        emitter.publish(msg)

        if app_id is not None:
            msg = "Usage of --app-id is deprecated. Use --options instead " \
                  "and specify a file that contains [service.name] property"
            emitter.publish(msg)

        package_manager.install_app(pkg, user_options, app_id)

    if cli and pkg.cli_definition():
        # Install subcommand
        msg = 'Installing CLI subcommand for package [{}] version [{}]'.format(
            pkg.name(), pkg.version())
        emitter.publish(msg)

        subcommand.install(pkg, global_)

        subcommand_paths = subcommand.get_package_commands(package_name)
        new_commands = [
            os.path.basename(p).replace('-', ' ', 1) for p in subcommand_paths
        ]

        if new_commands:
            commands = ', '.join(new_commands)
            plural = "s" if len(new_commands) > 1 else ""
            emitter.publish("New command{} available: {}".format(
                plural, commands))

    post_install_notes = pkg_json.get('postInstallNotes')
    if app and post_install_notes:
        emitter.publish(post_install_notes)

    return 0
コード例 #24
0
ファイル: main.py プロジェクト: sschneid/dcos-cli
def _install(package_name, package_version, options_path, app_id, cli, app,
             yes):
    """Install the specified package.

    :param package_name: the package to install
    :type package_name: str
    :param package_version: package version to install
    :type package_version: str
    :param options_path: path to file containing option values
    :type options_path: str
    :param app_id: app ID for installation of this package
    :type app_id: str
    :param cli: indicates if the cli should be installed
    :type cli: bool
    :param app: indicate if the application should be installed
    :type app: bool
    :param yes: automatically assume yes to all prompts
    :type yes: bool
    :returns: process status
    :rtype: int
    """

    if cli is False and app is False:
        # Install both if neither flag is specified
        cli = app = True

    # Fail early if options file isn't valid
    user_options = util.read_file_json(options_path)

    package_manager = get_package_manager()
    pkg = package_manager.get_package_version(package_name, package_version)

    pkg_json = pkg.package_json()
    pre_install_notes = pkg_json.get('preInstallNotes')
    if app and pre_install_notes:
        emitter.publish(pre_install_notes)
        if not confirm('Continue installing?', yes):
            emitter.publish('Exiting installation.')
            return 0

    if app and pkg.has_mustache_definition():

        # Even though package installation will check for template rendering
        # errors, we want to fail early, before trying to install.
        pkg.options(user_options)

        # Install in Marathon
        msg = 'Installing Marathon app for package [{}] version [{}]'.format(
            pkg.name(), pkg.version())
        if app_id is not None:
            msg += ' with app id [{}]'.format(app_id)

        emitter.publish(msg)

        package_manager.install_app(pkg, user_options, app_id)

    if cli and pkg.has_cli_definition():
        # Install subcommand
        msg = 'Installing CLI subcommand for package [{}] version [{}]'.format(
            pkg.name(), pkg.version())
        emitter.publish(msg)

        subcommand.install(pkg)

        subcommand_paths = subcommand.get_package_commands(package_name)
        new_commands = [os.path.basename(p).replace('-', ' ', 1)
                        for p in subcommand_paths]

        if new_commands:
            commands = ', '.join(new_commands)
            plural = "s" if len(new_commands) > 1 else ""
            emitter.publish("New command{} available: {}".format(plural,
                                                                 commands))

    post_install_notes = pkg_json.get('postInstallNotes')
    if app and post_install_notes:
        emitter.publish(post_install_notes)

    return 0
コード例 #25
0
ファイル: main.py プロジェクト: timcharper/dcos-cli
def _install(package_name, package_version, options_path, app_id, cli, app,
             yes):
    """Install the specified package.

    :param package_name: the package to install
    :type package_name: str
    :param package_version: package version to install
    :type package_version: str
    :param options_path: path to file containing option values
    :type options_path: str
    :param app_id: app ID for installation of this package
    :type app_id: str
    :param cli: indicates if the cli should be installed
    :type cli: bool
    :param app: indicate if the application should be installed
    :type app: bool
    :param yes: automatically assume yes to all prompts
    :type yes: bool
    :returns: process status
    :rtype: int
    """

    if cli is False and app is False:
        # Install both if neither flag is specified
        cli = app = True

    # Fail early if options file isn't valid
    user_options = util.read_file_json(options_path)

    package_manager = get_package_manager()
    pkg = package_manager.get_package_version(package_name, package_version)

    pkg_json = pkg.package_json()
    pre_install_notes = pkg_json.get('preInstallNotes')
    if app and pre_install_notes:
        emitter.publish(pre_install_notes)
        if not confirm('Continue installing?', yes):
            emitter.publish('Exiting installation.')
            return 0

    if app and pkg.has_mustache_definition():

        # Even though package installation will check for template rendering
        # errors, we want to fail early, before trying to install.
        pkg.options(user_options)

        # Install in Marathon
        msg = 'Installing Marathon app for package [{}] version [{}]'.format(
            pkg.name(), pkg.version())
        if app_id is not None:
            msg += ' with app id [{}]'.format(app_id)

        emitter.publish(msg)

        package_manager.install_app(pkg, user_options, app_id)

    if cli and pkg.has_cli_definition():
        # Install subcommand
        msg = 'Installing CLI subcommand for package [{}] version [{}]'.format(
            pkg.name(), pkg.version())
        emitter.publish(msg)

        subcommand.install(pkg)

        subcommand_paths = subcommand.get_package_commands(package_name)
        new_commands = [os.path.basename(p).replace('-', ' ', 1)
                        for p in subcommand_paths]

        if new_commands:
            commands = ', '.join(new_commands)
            plural = "s" if len(new_commands) > 1 else ""
            emitter.publish("New command{} available: {}".format(plural,
                                                                 commands))

    post_install_notes = pkg_json.get('postInstallNotes')
    if app and post_install_notes:
        emitter.publish(post_install_notes)

    return 0