#!/usr/bin/env bash # ripedeps.sh - package.json upgrade recommendations from https://ripedeps.com # Needs only bash, curl and jq. Works with bash 3.2 (macOS). set -u VERSION="1.0.0" HOST="https://api.ripedeps.com" NOTICE="Uploaded package.json files and the resulting recommendations are stored for analysis and for improving the algorithm. Do not send files that contain secrets." usage() { cat < Prints a unified diff; apply it with: ripedeps.sh package.json | patch -p1 Options: --focus only upgrade these packages and their direct neighbours (comma-separated; exact names or * globs; "trpc" also matches @trpc/*; repeatable) --min-age minimum age of a version in days, 0-365 (recommended: 7) --max-bump patch | minor | major (default: minor) --skip-adoptconfig do not write the effective settings into the "ripedeps" entry of package.json --json print the full plan as JSON (errors are printed as their JSON error object) --host API host (default: $HOST) -h, --help show this help Settings you do not pass fall back to the "ripedeps" entry of the file, then to the defaults (min-age 0, max-bump minor). A https:// URL is downloaded by the service (use the raw file URL). Use "-" to read package.json from standard input. Exit codes: 0 success, 1 usage or local error, 2 the API returned an error, 3 the plan timed out (retry: the package data loaded so far is cached). Privacy: $NOTICE USAGE } die() { echo "ripedeps.sh: $*" >&2; exit 1; } FOCUS="" MIN_AGE="" MAX_BUMP="" SKIP_ADOPT="" JSON_OUT="" INPUT="" while [ $# -gt 0 ]; do case "$1" in -h|--help) usage; exit 0 ;; --focus) [ $# -ge 2 ] || die "--focus needs a value" if [ -z "$FOCUS" ]; then FOCUS="$2"; else FOCUS="$FOCUS,$2"; fi shift 2 ;; --focus=*) v="${1#--focus=}"; if [ -z "$FOCUS" ]; then FOCUS="$v"; else FOCUS="$FOCUS,$v"; fi; shift ;; --min-age) [ $# -ge 2 ] || die "--min-age needs a value"; MIN_AGE="$2"; shift 2 ;; --min-age=*) MIN_AGE="${1#--min-age=}"; shift ;; --max-bump) [ $# -ge 2 ] || die "--max-bump needs a value"; MAX_BUMP="$2"; shift 2 ;; --max-bump=*) MAX_BUMP="${1#--max-bump=}"; shift ;; --skip-adoptconfig) SKIP_ADOPT="1"; shift ;; --json) JSON_OUT="1"; shift ;; --host) [ $# -ge 2 ] || die "--host needs a value"; HOST="$2"; shift 2 ;; --host=*) HOST="${1#--host=}"; shift ;; --) shift; if [ $# -gt 0 ]; then INPUT="$1"; shift; fi ;; -) INPUT="-"; shift ;; -*) die "unknown option: $1 (see --help)" ;; *) [ -z "$INPUT" ] || die "only one input is accepted"; INPUT="$1"; shift ;; esac done [ -n "$INPUT" ] || { usage >&2; exit 1; } command -v curl >/dev/null 2>&1 || die "curl is required" command -v jq >/dev/null 2>&1 || die "jq is required" if [ -n "$MIN_AGE" ]; then case "$MIN_AGE" in ''|*[!0-9]*) die "--min-age must be an integer between 0 and 365" ;; esac [ "$MIN_AGE" -le 365 ] || die "--min-age must be an integer between 0 and 365" fi if [ -n "$MAX_BUMP" ]; then case "$MAX_BUMP" in patch|minor|major) ;; *) die "--max-bump must be patch, minor or major" ;; esac fi # Only the settings the user gave are sent, so the "ripedeps" entry of the file can apply. JQ_ARGS=() JQ_FILTER='{}' case "$INPUT" in https://*) JQ_ARGS+=(--arg url "$INPUT"); JQ_FILTER="$JQ_FILTER + {packageJsonUrl: \$url}" ;; http://*) die "only https:// URLs are accepted" ;; *) TMP_IN="" if [ "$INPUT" = "-" ]; then TMP_IN="$(mktemp "${TMPDIR:-/tmp}/ripedeps.XXXXXX")" || die "cannot create a temporary file" cat > "$TMP_IN" FILE="$TMP_IN" else [ -f "$INPUT" ] || die "no such file: $INPUT" [ -r "$INPUT" ] || die "cannot read: $INPUT" FILE="$INPUT" fi JQ_ARGS+=(--rawfile pj "$FILE"); JQ_FILTER="$JQ_FILTER + {packageJson: \$pj}" ;; esac if [ -n "$FOCUS" ]; then JQ_ARGS+=(--arg focus "$FOCUS") JQ_FILTER="$JQ_FILTER + {focus: (\$focus | split(\",\") | map(sub(\"^ +\"; \"\") | sub(\" +\$\"; \"\")) | map(select(length > 0)))}" fi if [ -n "$MIN_AGE" ]; then JQ_ARGS+=(--argjson minAge "$MIN_AGE"); JQ_FILTER="$JQ_FILTER + {minAgeDays: \$minAge}"; fi if [ -n "$MAX_BUMP" ]; then JQ_ARGS+=(--arg bump "$MAX_BUMP"); JQ_FILTER="$JQ_FILTER + {maxBump: \$bump}"; fi if [ -n "$SKIP_ADOPT" ]; then JQ_FILTER="$JQ_FILTER + {skipAdoptConfig: true}"; fi BODY="$(jq -n "${JQ_ARGS[@]}" "$JQ_FILTER")" || { [ -z "${TMP_IN:-}" ] || rm -f "$TMP_IN"; die "could not build the request (is the input readable text?)"; } [ -z "${TMP_IN:-}" ] || rm -f "$TMP_IN" RESP_FILE="$(mktemp "${TMPDIR:-/tmp}/ripedeps-resp.XXXXXX")" || die "cannot create a temporary file" trap 'rm -f "$RESP_FILE"' EXIT # Longer than the service's plan timeout (60 s by default); the request waits for the finished plan. STATUS="$(printf '%s' "$BODY" | curl -sS --max-time 120 -o "$RESP_FILE" -w '%{http_code}' \ -X POST -H 'Content-Type: application/json' -H 'Accept: application/json' -A "ripedeps.sh/$VERSION" \ --data-binary @- "${HOST%/}/plan")" CURL_EXIT=$? if [ $CURL_EXIT -ne 0 ]; then echo "ripedeps.sh: request to ${HOST%/}/plan failed (curl exit $CURL_EXIT)" >&2 exit 1 fi if [ "$STATUS" -ge 200 ] 2>/dev/null && [ "$STATUS" -lt 300 ]; then if [ -n "$JSON_OUT" ]; then cat "$RESP_FILE" else jq -j '.diff' "$RESP_FILE" || die "unexpected response from the service" # warnings go to stderr so that the diff on stdout stays patchable jq -r '.warnings[]? | "warning: " + .message' "$RESP_FILE" >&2 fi exit 0 fi if [ -n "$JSON_OUT" ]; then if jq -e . "$RESP_FILE" >/dev/null 2>&1; then cat "$RESP_FILE"; else jq -n --arg s "$STATUS" '{error: {code: "internal_error", message: ("HTTP " + $s)}}'; fi else echo "ripedeps.sh: $(jq -r '.error | "\(.code): \(.message)"' "$RESP_FILE" 2>/dev/null || echo "HTTP $STATUS")" >&2 fi CODE="$(jq -r '.error.code // empty' "$RESP_FILE" 2>/dev/null || true)" [ "$STATUS" = "504" ] && [ "$CODE" = "plan_timeout" ] && exit 3 exit 2