#!/bin/sh MODERNE_BASE_URL="https://app.moderne.io" # # Moderne CLI installer. # Downloads the modw wrapper script and places it on PATH. # # Usage: # curl -fsSL "https://app.moderne.io/cli" | bash # # When served through a tenant proxy, MODERNE_BASE_URL is baked in at the top # of this script by the proxy via string substitution. When running the raw # script from GitHub, MODERNE_BASE_URL is unset and no tenant configuration # is performed. # # Environment variables: # MODERNE_CLI_HOME - Override install location (default: ~/.moderne/cli) # MODERNE_BASE_URL - Moderne tenant URL (set by proxy, not by user) # MOD_TENANT_AUTHORIZATION - Moderne personal access token. The CLI reads this at run # time; setting it here logs in with it rather than opening a # browser, so an unattended install ends up configured: # curl -fsSL "https://app.moderne.io/cli" | MOD_TENANT_AUTHORIZATION=mat-... bash # MODERNE_WRAPPER_VERSION - CLI version to record in moderne-wrapper.properties # (RELEASE, LATEST, or a pinned version; default RELEASE) # MODERNE_WRAPPER_DISTRIBUTION_USERNAME - Basic auth username for distribution downloads # MODERNE_WRAPPER_DISTRIBUTION_PASSWORD - Basic auth password for distribution downloads # MODERNE_WRAPPER_DISTRIBUTION_TOKEN - Bearer token for distribution downloads # set -e MODERNE_CLI_HOME="${MODERNE_CLI_HOME:-$HOME/.moderne/cli}" BIN_DIR="$MODERNE_CLI_HOME/bin" DIST_DIR="$MODERNE_CLI_HOME/dist" CODE_GENOME_PROJECT="https://artifacts.codegenomeproject.org/maven" # Customized by the /cli REST endpoint to point at a customer's internal artifact repository. DISTRIBUTION_URL='https://api.app.moderne.io/cli/jar' DISTRIBUTION_URL_EARLY_ACCESS="" # --- Authentication --- DIST_USERNAME="${MODERNE_WRAPPER_DISTRIBUTION_USERNAME:-}" DIST_PASSWORD="${MODERNE_WRAPPER_DISTRIBUTION_PASSWORD:-}" DIST_TOKEN="${MODERNE_WRAPPER_DISTRIBUTION_TOKEN:-}" # Token takes precedence over username/password if [ -n "$DIST_TOKEN" ]; then DIST_USERNAME="" DIST_PASSWORD="" fi # --- HTTP helpers --- http_get() { if command -v curl >/dev/null 2>&1; then if [ -n "$DIST_TOKEN" ]; then curl -fsSL -H "Authorization: Bearer $DIST_TOKEN" -o "$2" "$1" elif [ -n "$DIST_USERNAME" ]; then curl -fsSL --user "$DIST_USERNAME:$DIST_PASSWORD" -o "$2" "$1" else curl -fsSL -o "$2" "$1" fi elif command -v wget >/dev/null 2>&1; then if [ -n "$DIST_TOKEN" ]; then wget -q --header="Authorization: Bearer $DIST_TOKEN" -O "$2" "$1" elif [ -n "$DIST_USERNAME" ]; then wget -q --user="$DIST_USERNAME" --password="$DIST_PASSWORD" -O "$2" "$1" else wget -q -O "$2" "$1" fi else echo "ERROR: Neither curl nor wget found." >&2 exit 1 fi } http_get_stdout() { if command -v curl >/dev/null 2>&1; then if [ -n "$DIST_TOKEN" ]; then curl -fsSL -H "Authorization: Bearer $DIST_TOKEN" "$1" elif [ -n "$DIST_USERNAME" ]; then curl -fsSL --user "$DIST_USERNAME:$DIST_PASSWORD" "$1" else curl -fsSL "$1" fi elif command -v wget >/dev/null 2>&1; then if [ -n "$DIST_TOKEN" ]; then wget -q --header="Authorization: Bearer $DIST_TOKEN" -O - "$1" elif [ -n "$DIST_USERNAME" ]; then wget -q --user="$DIST_USERNAME" --password="$DIST_PASSWORD" -O - "$1" else wget -q -O - "$1" fi else echo "ERROR: Neither curl nor wget found." >&2 exit 1 fi } # --- Version resolution --- resolve_version_from_maven() { METADATA_URL="$1/io/moderne/moderne-cli/maven-metadata.xml" METADATA="$(http_get_stdout "$METADATA_URL" 2>/dev/null)" || return 1 echo "$METADATA" | sed -n 's/.*\(.*\)<\/release>.*/\1/p' } # --- Main --- echo "Installing Moderne CLI..." REPO_BASE="${DISTRIBUTION_URL:-$CODE_GENOME_PROJECT}" MOD_VERSION="$(resolve_version_from_maven "$REPO_BASE" 2>/dev/null)" || true if [ -z "$MOD_VERSION" ]; then echo "ERROR: Could not resolve Moderne CLI version." >&2 exit 1 fi # Download modw MODW_URL="$REPO_BASE/io/moderne/moderne-cli/$MOD_VERSION/moderne-cli-$MOD_VERSION-modw.sh" mkdir -p "$BIN_DIR" "$DIST_DIR" echo "Downloading modw $MOD_VERSION..." http_get "$MODW_URL" "$BIN_DIR/modw" chmod +x "$BIN_DIR/modw" # Create mod symlink ln -sf modw "$BIN_DIR/mod" # Write wrapper properties mkdir -p "$DIST_DIR" echo "version=${MODERNE_WRAPPER_VERSION:-RELEASE}" > "$DIST_DIR/moderne-wrapper.properties" if [ -n "$DISTRIBUTION_URL" ]; then echo "distributionUrl=$DISTRIBUTION_URL/io/moderne/moderne-cli-\${platform}/\${directory}/moderne-cli-\${platform}-\${version}.\${extension}" >> "$DIST_DIR/moderne-wrapper.properties" fi # Resolving LATEST/snapshots against the same repository keeps those downloads # attributed to the tenant instead of falling back to the Code Genome Project. EARLY_ACCESS="${DISTRIBUTION_URL_EARLY_ACCESS:-$DISTRIBUTION_URL}" if [ -n "$EARLY_ACCESS" ]; then echo "distributionUrlEarlyAccess=$EARLY_ACCESS" >> "$DIST_DIR/moderne-wrapper.properties" fi if [ -n "$DIST_USERNAME" ]; then echo "distributionUsername=$DIST_USERNAME" >> "$DIST_DIR/moderne-wrapper.properties" fi if [ -n "$DIST_PASSWORD" ]; then echo "distributionPassword=$DIST_PASSWORD" >> "$DIST_DIR/moderne-wrapper.properties" fi if [ -n "$DIST_TOKEN" ]; then echo "distributionToken=$DIST_TOKEN" >> "$DIST_DIR/moderne-wrapper.properties" fi # --- Make mod available on PATH --- # Under `curl | bash` stdin is the download, so stdout is the only signal that # somebody is watching. Without it, rc-file edits and a browser login are noise. INTERACTIVE="" if [ -t 1 ]; then INTERACTIVE=1 fi # Prefer symlinking into a directory already on PATH so mod works immediately in # the current shell, with no rc-file edit or `source` needed. /usr/local/bin is # last so a user install stays user-local, but it is what makes `curl | bash` in # a container (running as root) leave a mod the image's CMD can find. LINK_DIR="" for _CANDIDATE in "$HOME/.local/bin" "$HOME/bin" "/usr/local/bin"; do case ":$PATH:" in *":$_CANDIDATE:"*) ;; *) continue ;; esac [ -d "$_CANDIDATE" ] || mkdir -p "$_CANDIDATE" 2>/dev/null || continue [ -w "$_CANDIDATE" ] || continue # Never clobber an unrelated mod binary if [ -e "$_CANDIDATE/mod" ] || [ -L "$_CANDIDATE/mod" ]; then case "$(readlink "$_CANDIDATE/mod" 2>/dev/null)" in "$BIN_DIR/mod"|*"/.moderne/"*) ;; *) continue ;; esac fi ln -sf "$BIN_DIR/mod" "$_CANDIDATE/mod" 2>/dev/null || continue ln -sf "$BIN_DIR/modw" "$_CANDIDATE/modw" 2>/dev/null || true LINK_DIR="$_CANDIDATE" break done SHELL_NAME="$(basename "${SHELL:-/bin/sh}")" case "$SHELL_NAME" in zsh) RC_FILE="$HOME/.zshrc" ;; fish) RC_FILE="$HOME/.config/fish/config.fish" ;; *) RC_FILE="$HOME/.bashrc" ;; esac # Fall back to a persistent PATH entry when no symlink location was found if [ -z "$LINK_DIR" ] && [ -n "$INTERACTIVE" ]; then case ":$PATH:" in *".moderne/cli/bin"*) ;; *) if [ "$SHELL_NAME" = "fish" ]; then # fish_add_path persists via universal variables and propagates # to running fish shells, so no restart is needed if command -v fish >/dev/null 2>&1 && fish -c "fish_add_path --append \$HOME/.moderne/cli/bin" 2>/dev/null; then echo "Added ~/.moderne/cli/bin to fish_user_paths" else mkdir -p "$(dirname "$RC_FILE")" 2>/dev/null || true echo "fish_add_path --append \$HOME/.moderne/cli/bin" >> "$RC_FILE" echo "Added ~/.moderne/cli/bin to PATH in $RC_FILE" PATH_UPDATED=1 fi else echo "export PATH=\"\$PATH:\$HOME/.moderne/cli/bin\"" >> "$RC_FILE" echo "Added ~/.moderne/cli/bin to PATH in $RC_FILE" PATH_UPDATED=1 fi ;; esac fi # The completion script is bash/zsh syntax; fish is not supported if [ "$SHELL_NAME" != "fish" ] && [ -n "$INTERACTIVE" ]; then COMPLETION_SOURCE='[ -f "$HOME/.moderne/cli/completion.bash" ] && source "$HOME/.moderne/cli/completion.bash"' if ! grep -qF "moderne/cli/completion.bash" "$RC_FILE" 2>/dev/null; then echo "$COMPLETION_SOURCE" >> "$RC_FILE" echo "Added shell completion to $RC_FILE" fi fi # Warn when another mod earlier on PATH shadows this install (e.g. mono's mod) RESOLVED_MOD="$(command -v mod 2>/dev/null || true)" if [ -n "$RESOLVED_MOD" ] && [ "$RESOLVED_MOD" != "$BIN_DIR/mod" ] && { [ -z "$LINK_DIR" ] || [ "$RESOLVED_MOD" != "$LINK_DIR/mod" ]; }; then echo "WARN: Another 'mod' at $RESOLVED_MOD comes earlier on your PATH and will shadow this install." >&2 fi # Configure tenant if MODERNE_BASE_URL is set (baked in by proxy). # These are the first `mod` runs, so they also download the distribution; in a # container that is 400+ MB for an image that may never run a recipe. if [ -n "$MODERNE_BASE_URL" ]; then if [ -n "$MOD_TENANT_AUTHORIZATION" ]; then # Already authenticated, so no terminal is needed either way. Logging in # persists the token, leaving the tenant configured once the variable is # gone. Errors are not silenced: a rejected token is worth hearing about. "$BIN_DIR/mod" config moderne edit "$MODERNE_BASE_URL" || true if ! printf '%s\n' "$MOD_TENANT_AUTHORIZATION" | "$BIN_DIR/mod" config moderne login -y --with-token; then echo "WARN: MOD_TENANT_AUTHORIZATION was not accepted; run 'mod config moderne login' to authenticate." >&2 fi elif [ -n "$INTERACTIVE" ] && [ -r /dev/tty ]; then # Hand back the terminal: `curl | bash` leaves stdin on the pipe, and the # login flow needs a console to prompt on. "$BIN_DIR/mod" config moderne edit "$MODERNE_BASE_URL" /dev/null || true "$BIN_DIR/mod" login /dev/null || true else echo "Skipping tenant setup: no terminal to complete the browser login on." echo "Re-run with MOD_TENANT_AUTHORIZATION set to configure it unattended, or run" echo "'mod config moderne edit $MODERNE_BASE_URL' and 'mod config moderne login --with-token' by hand." fi fi echo "" echo "Moderne CLI installed successfully!" if [ -n "$LINK_DIR" ]; then echo "Linked into $LINK_DIR (already on your PATH). Run 'mod --version' to verify." elif [ -n "$PATH_UPDATED" ]; then echo "Your PATH was updated in $RC_FILE, but this terminal won't pick it up yet." echo "Run 'source $RC_FILE' or open a new terminal, then run 'mod --version' to verify." else case ":$PATH:" in *".moderne/cli/bin"*) echo "Run 'mod --version' to verify." ;; *) echo "Add $BIN_DIR to PATH, then run 'mod --version' to verify." ;; esac fi