#!/bin/sh # Install the mandala command-line client. # # curl -fsSL https://mandala.computer/install.sh | sh # # The CLI ships in the mandala-computer npm package, so this installs that # package with npm. It needs Node.js and npm already present, and says where to # get them if they are missing or too old. It never uses sudo: when npm's global # prefix belongs to root, it installs under ~/.local instead and says how to put # that on your PATH. # # Options, as arguments (curl ... | sh -s -- --version 0.6.0) or environment: # # --version VERSION MANDALA_VERSION the npm version or tag (default: latest) # --prefix DIR MANDALA_PREFIX install under DIR, so the command lands # in DIR/bin (default: npm's global # prefix when writable, else ~/.local) # # Everything runs from main, called on the last line, so a download cut short # runs nothing rather than half of this. set -eu PACKAGE=mandala-computer # The floor the package declares in its engines field. The registry's copy of # that field wins when it can be read; this is the answer when it cannot. DEFAULT_NODE_MAJOR=22 say() { printf '%s\n' "$*" } fail() { printf 'install.sh: %s\n' "$*" >&2 exit 1 } usage() { say "usage: install.sh [--version VERSION] [--prefix DIR]" say "Installs the mandala CLI (npm package $PACKAGE). See the top of this script." } node_help() { # $1 is the major version required. cat >&2 < 0)) best = { e: e, p: p }; }); var range = best && best.e["engines.node"]; range = typeof range === "string" ? range.replace(/\s+/g, " ").trim() : ""; var floor = range ? Math.min.apply(null, range.split("||").map(function (alt) { var m = /^(?:>=?|\^|~>?|=)?\s*v?(\d+)/.exec(alt.trim()); return m ? +m[1] : 0; })) : list && best && best.e.bare ? "?" : ""; process.stdout.write((best ? best.e.version : "") + " " + floor + " " + range); ' 2>/dev/null || true } main() { version=${MANDALA_VERSION:-latest} prefix=${MANDALA_PREFIX:-} while [ $# -gt 0 ]; do case $1 in --version) [ $# -ge 2 ] || fail "--version needs a value" version=$2 shift 2 ;; --version=*) version=${1#--version=} shift ;; --prefix) [ $# -ge 2 ] || fail "--prefix needs a value" prefix=$2 shift 2 ;; --prefix=*) prefix=${1#--prefix=} shift ;; -h | --help) usage exit 0 ;; *) usage >&2 fail "unknown argument: $1" ;; esac done # A version or a dist-tag, nothing else: npm would also take a path, a git # URL or a tarball URL after the @, and none of those is ours to install. case $version in '' | *[!A-Za-z0-9.+-]*) fail "--version must be a version such as 0.6.0 or a tag such as latest" ;; esac case $(uname -s 2>/dev/null || echo unknown) in MINGW* | MSYS* | CYGWIN* | Windows_NT) fail "on Windows, run: npm install --global $PACKAGE" ;; esac if ! command -v node >/dev/null 2>&1; then say "install.sh: Node.js was not found on your PATH." >&2 node_help "$DEFAULT_NODE_MAJOR" exit 1 fi if ! command -v npm >/dev/null 2>&1; then say "install.sh: node is installed but npm was not found on your PATH." >&2 node_help "$DEFAULT_NODE_MAJOR" exit 1 fi # Where it goes is settled before the registry is asked, so the lookup # below and the install both run as global npm commands with the same # --prefix, and so read the same npm configuration. A project .npmrc in the # current directory is not part of it: npm ignores that in global mode, and # a lookup that honoured it could ask a registry the install never would. fallback= if [ -z "$prefix" ]; then global=$(npm prefix --global 2>/dev/null || true) if [ -n "$global" ] && can_write "$global/bin" && can_write "$global/lib/node_modules"; then prefix=$global else [ -n "${HOME:-}" ] || fail "npm's global prefix is not writable and HOME is not set; pass --prefix DIR" prefix=$HOME/.local fallback="npm's global prefix (${global:-unknown}) needs root; installing under $prefix instead." fi fi case $prefix in /*) ;; *) prefix=$(pwd)/$prefix ;; esac [ "$prefix" = / ] || prefix=${prefix%/} bin=${prefix%/}/bin # Ask the registry first: it refuses a version that does not exist before # anything is installed, resolves a tag or a range to the one version the # install below then pins, and gives the Node.js floor that version declares. if ! meta=$(npm view --global --prefix "$prefix" "$PACKAGE@$version" \ version engines.node --json 2>/dev/null); then fail "could not find $PACKAGE@$version in the npm registry (check the version, and your network)" fi info=$(printf '%s' "$meta" | read_meta) resolved=${info%% *} rest=${info#* } need=${rest%% *} engines=${rest#* } case $resolved in '' | *[!A-Za-z0-9.+-]*) fail "the npm registry has no $PACKAGE@$version" ;; esac # npm shapes a range's whole list by its first entry, the oldest match: # when that one has no engines field, every entry comes back as a bare # version string, whatever the others declare. So the version chosen is # asked about on its own, and the default below is used only when it too # has no engines field, or cannot be read. if [ "$need" = "?" ]; then info=$(npm view --global --prefix "$prefix" "$PACKAGE@$resolved" \ version engines.node --json 2>/dev/null | read_meta) need= if [ "${info%% *}" = "$resolved" ]; then rest=${info#* } need=${rest%% *} engines=${rest#* } fi fi case $need in '' | *[!0-9]*) need=$DEFAULT_NODE_MAJOR engines=">=$DEFAULT_NODE_MAJOR" ;; esac have=$(node -p 'process.versions.node' 2>/dev/null || true) have_major=${have%%.*} case $have_major in '' | *[!0-9]*) say "install.sh: could not read the version of $(command -v node)." >&2 node_help "$need" exit 1 ;; esac if [ "$have_major" -lt "$need" ]; then say "install.sh: found Node.js $have at $(command -v node); $PACKAGE@$resolved needs Node.js $engines." >&2 node_help "$need" exit 1 fi [ -z "$fallback" ] || say "$fallback" say "Installing $PACKAGE@$resolved into $prefix with npm..." # --engine-strict: the engines field in the package itself has the last # word, should it ever disagree with the check above. npm install --global --prefix "$prefix" --engine-strict --no-fund --no-audit \ --loglevel=error "$PACKAGE@$resolved" || fail "npm install failed; the output above says why" if [ ! -x "$bin/mandala" ]; then fail "npm reported success but $bin/mandala is missing" fi # --help needs no credentials and no network, so this proves only that the # command starts under this node. "$bin/mandala" --help >/dev/null 2>&1 || fail "$bin/mandala was installed but does not run" say "" say "Installed mandala $resolved at $bin/mandala" found=$(command -v mandala 2>/dev/null || true) case ":$PATH:" in *":$bin:"*) if [ -n "$found" ] && [ "$found" != "$bin/mandala" ]; then say "" say "Note: 'mandala' on your PATH is $found, which comes before $bin." fi ;; *) say "" say "$bin is not on your PATH. Add it, then open a new terminal:" # Each line is a command to paste as it stands, whatever the prefix # holds: a space or a quote in it is quoted for the shell that reads it. case $bin in *[\$\`\"\\]*) line="export PATH=$(quote "$bin"):\"\$PATH\"" ;; *) line="export PATH=\"$bin:\$PATH\"" ;; esac case $(basename "${SHELL:-sh}") in fish) say " fish_add_path $(fish_quote "$bin")" ;; zsh) say " printf '%s\\n' $(quote "$line") >> ~/.zshrc" ;; bash) # A Linux terminal starts bash as an interactive non-login # shell, which reads ~/.bashrc. A macOS terminal starts every # window as a login shell, which never reads ~/.bashrc: it reads # the first of ~/.bash_profile, ~/.bash_login and ~/.profile that # exists. So on macOS the hint names that one, and names a new # ~/.bash_profile only when there is none, because creating it # would stop bash reading the other two. rc=.bashrc if [ "$(uname -s 2>/dev/null || true)" = Darwin ]; then rc=.bash_profile for candidate in .bash_profile .bash_login .profile; do if [ -e "${HOME:-}/$candidate" ]; then rc=$candidate break fi done fi say " printf '%s\\n' $(quote "$line") >> ~/$rc" ;; *) say " $line" ;; esac ;; esac say "" say "Next, sign in:" say " mandala login" say "" say "Documentation: https://github.com/mandalacomputer/typescript-sdk#the-mandala-cli" } main "$@"