diff options
| -rw-r--r-- | .dir-locals.el | 13 | ||||
| -rw-r--r-- | .gitignore | 5 | ||||
| -rw-r--r-- | .nvm_packages | 8 | ||||
| -rw-r--r-- | Makefile | 6 | ||||
| -rw-r--r-- | README.rst | 2 | ||||
| -rwxr-xr-x | manage | 19 | ||||
| -rw-r--r-- | package.json | 6 | ||||
| -rw-r--r-- | pyrightconfig-ci.json | 10 | ||||
| -rw-r--r-- | pyrightconfig.json | 9 | ||||
| -rw-r--r-- | requirements.txt | 2 | ||||
| -rw-r--r-- | searx/autocomplete.py | 52 | ||||
| -rw-r--r-- | searx/help/about.md | 71 | ||||
| -rw-r--r-- | searx/search/checker/impl.py | 3 | ||||
| -rw-r--r-- | searx/shared/redisdb.py | 5 | ||||
| -rw-r--r-- | searx/templates/__common__/about.html | 102 | ||||
| -rw-r--r-- | searx/templates/oscar/about.html | 3 | ||||
| -rw-r--r-- | searx/templates/simple/about.html | 3 | ||||
| -rw-r--r-- | searx/user_help.py | 48 | ||||
| -rwxr-xr-x | searx/webapp.py | 4 | ||||
| -rwxr-xr-x | utils/lib_nvm.sh | 12 |
20 files changed, 232 insertions, 151 deletions
diff --git a/.dir-locals.el b/.dir-locals.el index 43d6fa551..13145010c 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -32,6 +32,10 @@ ;; Jedi, flycheck & other python stuff should use the 'python-shell-interpreter' ;; from the local py3 environment. ;; +;; For pyright support you need to install:: +;; +;; M-x package-install lsp-pyright +;; ;; Other useful jedi stuff you might add to your ~/.emacs:: ;; ;; (global-set-key [f6] 'flycheck-mode) @@ -99,12 +103,19 @@ ;; use nodejs from the (local) NVM environment (see nvm-dir) (nvm-use-for-buffer) (setq-local js-indent-level 2) + ;; flycheck should use the eslint checker from developer tools + (setq-local flycheck-javascript-eslint-executable + (expand-file-name "node_modules/.bin/eslint" prj-root)) + (flycheck-mode) )))) (python-mode . ((eval . (progn - + ;; use nodejs from the (local) NVM environment (see nvm-dir) + (nvm-use-for-buffer) + (if (featurep 'lsp-pyright) + (lsp)) (setq-local python-environment-virtualenv (list (expand-file-name "bin/virtualenv" python-shell-virtualenv-root) ;;"--system-site-packages" diff --git a/.gitignore b/.gitignore index efea6c872..9b3eff158 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# to sync with .dockerignore +# to sync with .dockerignore & pyrightconfig.json *.pyc */*.pyc @@ -16,3 +16,6 @@ dist/ local/ gh-pages/ *.egg-info/ + +/package-lock.json +/node_modules/ diff --git a/.nvm_packages b/.nvm_packages deleted file mode 100644 index 13eabc5c2..000000000 --- a/.nvm_packages +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8; mode: conf-unix -*- -# -# Developement tools pre-installed in NVM's node installation [1] -# -# [1] https://github.com/nvm-sh/nvm#default-global-packages-from-file-while-installing - -eslint - @@ -50,8 +50,8 @@ search.checker.%: install $(Q)./manage pyenv.cmd searx-checker -v "$(subst _, ,$(patsubst search.checker.%,%,$@))" PHONY += test ci.test test.shell -ci.test: test.yamllint test.black test.pylint test.unit test.robot test.rst -test: test.yamllint test.black test.pylint test.unit test.robot test.rst test.shell +ci.test: test.yamllint test.black test.pyright test.pylint test.unit test.robot test.rst +test: test.yamllint test.black test.pyright test.pylint test.unit test.robot test.rst test.shell test.shell: $(Q)shellcheck -x -s dash \ dockerfiles/docker-entrypoint.sh @@ -86,7 +86,7 @@ MANAGE += py.build py.clean MANAGE += pyenv pyenv.install pyenv.uninstall MANAGE += pypi.upload pypi.upload.test MANAGE += format.python -MANAGE += test.yamllint test.pylint test.black test.unit test.coverage test.robot test.rst test.clean +MANAGE += test.yamllint test.pylint test.pyright test.black test.unit test.coverage test.robot test.rst test.clean MANAGE += themes.all themes.oscar themes.simple themes.simple.test pygments.less MANAGE += static.build.commit static.build.drop static.build.restore MANAGE += nvm.install nvm.clean nvm.status nvm.nodejs diff --git a/README.rst b/README.rst index 38a50a62f..e512ff5c2 100644 --- a/README.rst +++ b/README.rst @@ -29,7 +29,7 @@ our homepage_. .. _searx.space: https://searx.space .. _user: https://docs.searxng.org/user -.. _admin: https://docs.searxng.org/user/admin +.. _admin: https://docs.searxng.org/admin .. _developer: https://docs.searxng.org/dev .. _homepage: https://docs.searxng.org/ .. _metasearch engine: https://en.wikipedia.org/wiki/Metasearch_engine @@ -20,6 +20,8 @@ source "$(dirname "${BASH_SOURCE[0]}")/utils/lib_go.sh" # shellcheck source=utils/lib_redis.sh source "$(dirname "${BASH_SOURCE[0]}")/utils/lib_redis.sh" +PATH="${REPO_ROOT}/node_modules/.bin:${PATH}" + # config PYOBJECTS="searx" @@ -103,6 +105,7 @@ format.: test.: yamllint : lint YAML files (YAMLLINT_FILES) pylint : lint PYLINT_FILES, searx/engines, searx & tests + pyright : static type check of python sources black : check black code format unit : run unit tests coverage : run unit tests with coverage @@ -558,6 +561,12 @@ node.env() { dump_return $? } +node.env.devtools() { + nodejs.ensure + build_msg INSTALL "package.json: developer and CI tools" + npm install +} + node.clean() { if ! required_commands npm 2>/dev/null; then build_msg CLEAN "npm is not installed / ignore npm dependencies" @@ -682,6 +691,16 @@ test.pylint() { dump_return $? } +test.pyright() { + build_msg TEST "[pyright] static type check of python sources" + node.env.devtools + # We run Pyright in the virtual environment because Pyright + # executes "python" to determine the Python version. + pyenv.cmd npx --no-install pyright -p pyrightconfig-ci.json + dump_return $? +} + + test.black() { build_msg TEST "[black] \$BLACK_TARGETS" pyenv.cmd black --check --diff "${BLACK_OPTIONS[@]}" "${BLACK_TARGETS[@]}" diff --git a/package.json b/package.json new file mode 100644 index 000000000..e65e0585d --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "eslint": "^8.7.0", + "pyright": "^1.1.212" + } +} diff --git a/pyrightconfig-ci.json b/pyrightconfig-ci.json new file mode 100644 index 000000000..9082b0790 --- /dev/null +++ b/pyrightconfig-ci.json @@ -0,0 +1,10 @@ +{ + "venvPath": "local", + "venv": "py3", + "include": [ + "searx", + "searxng_extra", + "tests" + ], + "typeCheckingMode": "off" +} diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 000000000..56573c75f --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,9 @@ +{ + "venvPath": "local", + "venv": "py3", + "include": [ + "searx", + "searxng_extra", + "tests" + ] +} diff --git a/requirements.txt b/requirements.txt index 20aa4e34c..95b85578e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,5 @@ uvloop==0.16.0 httpx-socks[asyncio]==0.7.2 langdetect==1.0.9 setproctitle==1.2.2 +redis==4.1.1 +mistletoe==0.8.1 diff --git a/searx/autocomplete.py b/searx/autocomplete.py index b8d272c32..8a67f8139 100644 --- a/searx/autocomplete.py +++ b/searx/autocomplete.py @@ -1,33 +1,23 @@ -''' -searx is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. +# SPDX-License-Identifier: AGPL-3.0-or-later +# lint: pylint +"""This module implements functions needed for the autocompleter. -searx is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. +""" -You should have received a copy of the GNU Affero General Public License -along with searx. If not, see < http://www.gnu.org/licenses/ >. - -(C) 2013- by Adam Tauber, <asciimoo@gmail.com> -''' - - -from lxml import etree from json import loads from urllib.parse import urlencode +from lxml import etree from httpx import HTTPError - from searx import settings from searx.data import ENGINES_LANGUAGES from searx.network import get as http_get from searx.exceptions import SearxEngineResponseException +# a fetch_supported_languages() for XPath engines isn't available right now +# _brave = ENGINES_LANGUAGES['brave'].keys() + def get(*args, **kwargs): if 'timeout' not in kwargs: @@ -36,7 +26,26 @@ def get(*args, **kwargs): return http_get(*args, **kwargs) -def dbpedia(query, lang): +def brave(query, _lang): + # brave search autocompleter + url = 'https://search.brave.com/api/suggest?' + url += urlencode({'q': query}) + country = 'all' + # if lang in _brave: + # country = lang + kwargs = {'cookies': {'country': country}} + resp = get(url, **kwargs) + + results = [] + + if resp.ok: + data = resp.json() + for item in data[1]: + results.append(item) + return results + + +def dbpedia(query, _lang): # dbpedia autocompleter, no HTTPS autocomplete_url = 'https://lookup.dbpedia.org/api/search.asmx/KeywordSearch?' @@ -51,7 +60,7 @@ def dbpedia(query, lang): return results -def duckduckgo(query, lang): +def duckduckgo(query, _lang): # duckduckgo autocompleter url = 'https://ac.duckduckgo.com/ac/?{0}&type=list' @@ -85,7 +94,7 @@ def startpage(query, lang): return [e['text'] for e in data.get('suggestions', []) if 'text' in e] -def swisscows(query, lang): +def swisscows(query, _lang): # swisscows autocompleter url = 'https://swisscows.ch/api/suggest?{query}&itemsCount=5' @@ -128,6 +137,7 @@ backends = { 'swisscows': swisscows, 'qwant': qwant, 'wikipedia': wikipedia, + 'brave': brave, } diff --git a/searx/help/about.md b/searx/help/about.md new file mode 100644 index 000000000..521022414 --- /dev/null +++ b/searx/help/about.md @@ -0,0 +1,71 @@ +# About [searxng][url_for:index] + +SearXNG is a fork from the well-known [searx] [metasearch engine], aggregating +the results of other [search engines][url_for:preferences] while not storing +information about its users. + +More about SearXNG ... + +* [SearXNG sources][brand.git_url] +* [weblate] + +--- + +## Why use it? + +* SearXNG may not offer you as personalised results as Google, + but it doesn't generate a profile about you. + +* SearXNG doesn't care about what you search for, never shares anything + with a third party, and it can't be used to compromise you. + +* SearXNG is free software, the code is 100% open and you can help + to make it better. See more on [SearXNG sources][brand.git_url]. + +If you do care about privacy, want to be a conscious user, or otherwise +believe in digital freedom, make SearXNG your default search engine or run +it on your own server + +## Technical details - How does it work? + +SearXNG is a [metasearch engine], inspired by the [seeks project]. It provides +basic privacy by mixing your queries with searches on other platforms without +storing search data. Queries are made using a POST request on every browser +(except Chromium-based browsers*). Therefore they show up in neither our logs, +nor your url history. In the case of Chromium-based browser users there is an +exception: searx uses the search bar to perform GET requests. SearXNG can be +added to your browser's search bar; moreover, it can be set as the default +search engine. + +<span id='add to browser'></span> +## How to set as the default search engine? + +SearXNG supports [OpenSearch]. For more information on changing your default +search engine, see your browser's documentation: + +* [Firefox](https://support.mozilla.org/en-US/kb/add-or-remove-search-engine-firefox) +* [Microsoft Edge](https://support.microsoft.com/en-us/help/4028574/microsoft-edge-change-the-default-search-engine) +* Chromium-based browsers [only add websites that the user navigates to without a path.](https://www.chromium.org/tab-to-search) + +## Where to find anonymous usage statistics of this instance ? + +[Stats page][url_for:stats] contains some useful data about the engines used. + +## How can I make it my own? + +SearXNG appreciates your concern regarding logs, so take the code from +the [SearXNG project][brand.git_url] and run it yourself! + +Add your instance to this [list of public instances][brand.public_instances] to +help other people reclaim their privacy and make the Internet freer! The more +decentralized the Internet is, the more freedom we have! + +## Where are the docs & code of this instance? + +See the [SearXNG docs][brand.docs_url] and [SearXNG sources][brand.git_url] + +[searx]: https://github.com/searx/searx +[metasearch engine]: https://en.wikipedia.org/wiki/Metasearch_engine +[weblate]: https://weblate.bubu1.eu/projects/searxng/ +[seeks project]: https://beniz.github.io/seeks/ +[OpenSearch]: https://github.com/dewitt/opensearch/blob/master/opensearch-1-1-draft-6.md diff --git a/searx/search/checker/impl.py b/searx/search/checker/impl.py index c0dd966d0..bc5cdf968 100644 --- a/searx/search/checker/impl.py +++ b/searx/search/checker/impl.py @@ -74,7 +74,7 @@ def _download_and_check_if_image(image_url: str) -> bool: try: # use "image_proxy" (avoid HTTP/2) network.set_context_network_name('image_proxy') - stream = network.stream( + r, stream = network.stream( 'GET', image_url, timeout=10.0, @@ -91,7 +91,6 @@ def _download_and_check_if_image(image_url: str) -> bool: 'Cache-Control': 'max-age=0', }, ) - r = next(stream) r.close() if r.status_code == 200: is_image = r.headers.get('content-type', '').startswith('image/') diff --git a/searx/shared/redisdb.py b/searx/shared/redisdb.py index d6a4dc3bf..da71d169c 100644 --- a/searx/shared/redisdb.py +++ b/searx/shared/redisdb.py @@ -20,6 +20,7 @@ A redis DB connect can be tested by:: """ import logging +import redis from searx import get_setting logger = logging.getLogger('searx.shared.redis') @@ -28,8 +29,6 @@ _client = None def client(): global _client # pylint: disable=global-statement - import redis # pylint: disable=import-error, import-outside-toplevel - if _client is None: # not thread safe: in the worst case scenario, two or more clients are # initialized only one is kept, the others are garbage collected. @@ -38,8 +37,6 @@ def client(): def init(): - import redis # pylint: disable=import-error, import-outside-toplevel - try: c = client() logger.info("connected redis DB --> %s", c.acl_whoami()) diff --git a/searx/templates/__common__/about.html b/searx/templates/__common__/about.html deleted file mode 100644 index 5a9065f03..000000000 --- a/searx/templates/__common__/about.html +++ /dev/null @@ -1,102 +0,0 @@ -<div{% if rtl %} dir="ltr"{% endif %}> - <h1>About <a href="{{ url_for('index') }}">searxng</a></h1> - - <p> - SearXNG is a fork from the well-known <a href="https://github.com/searx/searx">searx</a> - <a href="https://en.wikipedia.org/wiki/Metasearch_engine"> metasearch engine</a>, - aggregating the results of other <a href="{{ url_for('preferences') }}">search engines</a> - while not storing information about its users. - </p> - - <p>More about SearXNG ...</p> - - <ul> - <li><a href="{{ searx_git_url }}">SearXNG sources</a></li> - <li><a href="https://weblate.bubu1.eu/projects/searxng/">weblate</a></li> - </ul> - - <hr /> - - <h2>Why use it?</h2> - - <ul> - <li> - SearXNG may not offer you as personalised results as Google, but it doesn't - generate a profile about you. - </li> - <li> - SearXNG doesn't care about what you search for, never shares anything with a - third party, and it can't be used to compromise you. - </li> - <li> - SearXNG is free software, the code is 100% open and you can help to make - it better. See more on <a href="{{ searx_git_url - }}">SearXNG sources</a>. - </li> - </ul> - - <p> - If you do care about privacy, want to be a conscious user, or otherwise - believe in digital freedom, make SearXNG your default search engine or run - it on your own server - </p> - - <h2>Technical details - How does it work?</h2> - - <p> - SearXNG is a <a href="https://en.wikipedia.org/wiki/Metasearch_engine">metasearch engine</a>, - inspired by the <a href="https://beniz.github.io/seeks/">seeks project</a>. - - It provides basic privacy by mixing your queries with searches on other - platforms without storing search data. Queries are made using a POST request - on every browser (except Chromium-based browsers*). Therefore they show up - in neither our logs, nor your url history. In the case of Chromium-based - browser users there is an exception: searx uses the search bar to perform GET - requests. - - SearXNG can be added to your browser's search bar; moreover, it can be set as - the default search engine. - </p> - - <h2 id='add to browser'>How to set as the default search engine?</h2> - - <p> - SearXNG supports <a href="https://github.com/dewitt/opensearch/blob/master/opensearch-1-1-draft-6.md">OpenSearch</a>. - For more information on changing your default search engine, see your browser's documentation: - </p> - - <ul> - <li><a href="https://support.mozilla.org/en-US/kb/add-or-remove-search-engine-firefox">Firefox</a></li> - <li><a href="https://support.microsoft.com/en-us/help/4028574/microsoft-edge-change-the-default-search-engine">Microsoft Edge</a></li> - <li>Chromium-based browsers <a href="https://www.chromium.org/tab-to-search">only add websites that the user navigates to without a path.</a> - </ul> - - <h2>Where to find anonymous usage statistics of this instance ?</h2> - - <p> - <a href="{{ url_for('stats') }}">Stats page</a> contains some useful data about the engines used. - </p> - - <h2>How can I make it my own?</h2> - - <p> - SearXNG appreciates your concern regarding logs, so take the code from - the <a href="{{ searx_git_url }}">SearXNG project</a> and - run it yourself! - </p> - <p> - Add your instance to this <a href="{{ get_setting('brand.public_instances') }}"> list of - public instances</a> to help other people reclaim their privacy and make the - Internet freer! The more decentralized the Internet is, the more freedom we - have! - </p> - - <h2>Where are the docs & code of this instance?</h2> - - <p> - See the <a href="{{ get_setting('brand.docs_url') }}">SearXNG docs</a> - and <a href="{{ searx_git_url }}">SearXNG sources</a> - </p> - -</div> -{% include "__common__/aboutextend.html" ignore missing %} diff --git a/searx/templates/oscar/about.html b/searx/templates/oscar/about.html index bc7fed8e1..a644761b6 100644 --- a/searx/templates/oscar/about.html +++ b/searx/templates/oscar/about.html @@ -1,5 +1,6 @@ {% extends "oscar/base.html" %} {% block title %}{{ _('about') }} - {% endblock %} {% block content %} -{% include '__common__/about.html' %} +{{ help.about | safe }} +{% include "__common__/aboutextend.html" ignore missing %} {% endblock %} diff --git a/searx/templates/simple/about.html b/searx/templates/simple/about.html index 1913879da..9f6a10ced 100644 --- a/searx/templates/simple/about.html +++ b/searx/templates/simple/about.html @@ -1,4 +1,5 @@ {% extends 'simple/base.html' %} {% block content %} -{% include '__common__/about.html' %} +{{ help.about | safe }} +{% include "__common__/aboutextend.html" ignore missing %} {% endblock %} diff --git a/searx/user_help.py b/searx/user_help.py new file mode 100644 index 000000000..bf7336777 --- /dev/null +++ b/searx/user_help.py @@ -0,0 +1,48 @@ +from typing import Dict +import os.path +import pkg_resources + +import flask +from flask.helpers import url_for +import mistletoe + +from . import get_setting +from .version import GIT_URL + +HELP: Dict[str, str] = {} +""" Maps a filename under help/ without the file extension to the rendered HTML. """ + + +def render(app: flask.Flask): + """ + Renders the user documentation. Must be called after all Flask routes have been + registered, because the documentation might try to link to them with Flask's `url_for`. + + We render the user documentation once on startup to improve performance. + """ + + link_targets = { + 'brand.git_url': GIT_URL, + 'brand.public_instances': get_setting('brand.public_instances'), + 'brand.docs_url': get_setting('brand.docs_url'), + } + + base_url = get_setting('server.base_url') or None + # we specify base_url so that url_for works for base_urls that have a non-root path + + with app.test_request_context(base_url=base_url): + link_targets['url_for:index'] = url_for('index') + link_targets['url_for:preferences'] = url_for('preferences') + link_targets['url_for:stats'] = url_for('stats') + + define_link_targets = ''.join(f'[{name}]: {url}\n' for name, url in link_targets.items()) + + for filename in pkg_resources.resource_listdir(__name__, 'help'): + rootname, ext = os.path.splitext(filename) + + if ext != '.md': + continue + + markdown = pkg_resources.resource_string(__name__, 'help/' + filename).decode() + markdown = define_link_targets + markdown + HELP[rootname] = mistletoe.markdown(markdown) diff --git a/searx/webapp.py b/searx/webapp.py index f509fea24..099a42996 100755 --- a/searx/webapp.py +++ b/searx/webapp.py @@ -55,6 +55,7 @@ from searx import ( get_setting, settings, searx_debug, + user_help, ) from searx.data import ENGINE_DESCRIPTIONS from searx.results import Timing, UnresponsiveEngine @@ -867,7 +868,7 @@ def __get_translated_errors(unresponsive_engines: Iterable[UnresponsiveEngine]): @app.route('/about', methods=['GET']) def about(): """Render about page""" - return render('about.html') + return render('about.html', help=user_help.HELP) @app.route('/autocompleter', methods=['GET', 'POST']) @@ -1359,6 +1360,7 @@ werkzeug_reloader = flask_run_development or (searx_debug and __name__ == "__mai if not werkzeug_reloader or (werkzeug_reloader and os.environ.get("WERKZEUG_RUN_MAIN") == "true"): plugin_initialize(app) search_initialize(enable_checker=True, check_network=True, enable_metrics=settings['general']['enable_metrics']) + user_help.render(app) def run(): diff --git a/utils/lib_nvm.sh b/utils/lib_nvm.sh index 267ba8a4a..04aa8a92e 100755 --- a/utils/lib_nvm.sh +++ b/utils/lib_nvm.sh @@ -26,11 +26,12 @@ NVM_LOCAL_FOLDER=.nvm nvm.env() { source "${NVM_DIR}/nvm.sh" source "${NVM_DIR}/bash_completion" + [ "$VERBOSE" = "1" ] && info_msg "sourced NVM environment from ${NVM_DIR}" } nvm.is_installed() { # is true if NVM is installed / in $HOME or even in <repo-root>/.nvm - [[ -d "${NVM_DIR}" ]] + [[ -f "${NVM_DIR}/nvm.sh" ]] } if [[ -z "${NVM_DIR}" ]]; then @@ -40,7 +41,6 @@ fi export NVM_DIR if nvm.is_installed; then - [ "$VERBOSE" = "1" ] && info_msg "source NVM environment from ${NVM_DIR}" nvm.env else # if nvm is not installed, use this function as a wrapper @@ -58,7 +58,7 @@ nvm.is_local() { [ "${NVM_DIR}" = "$(git rev-parse --show-toplevel)/${NVM_LOCAL_FOLDER}" ] } -nvm.min_node(){ +nvm.min_node() { # usage: nvm.min_node 16.3.0 # @@ -87,7 +87,7 @@ nvm.min_node(){ # implement nvm command line # -------------------------- -nvm.help(){ +nvm.help() { cat <<EOF nvm.: use nvm (without dot) to execute nvm commands directly install : install NVM locally at $(git rev-parse --show-toplevel)/${NVM_LOCAL_FOLDER} @@ -117,7 +117,9 @@ nvm.install() { info_msg "checkout ${NVM_VERSION_TAG}" git checkout "${NVM_VERSION_TAG}" 2>&1 | prefix_stdout " ${_Yellow}||${_creset} " popd &> /dev/null - cp "${REPO_ROOT}/.nvm_packages" "${NVM_DIR}/default-packages" + if [ -f "${REPO_ROOT}/.nvm_packages" ]; then + cp "${REPO_ROOT}/.nvm_packages" "${NVM_DIR}/default-packages" + fi nvm.env } |