#!/bin/sh # text-to-cad's installer for macOS and Linux: # # curl -fsSL https://texttocad.dev/install.sh | sh # # It finds uv, or installs it with uv's own installer, then runs `cad setup` from the newest # release of text-to-cad's server through uvx: text-to-cad set up or updated in every agent app on this machine, # with nothing asked. Run it again to update; with --uninstall it runs `cad uninstall` instead. # Run with --help for its options; they go after `sh -s --` when piped. # Everything happens inside main, called on the last line, so a download cut short runs nothing. set -eu # The release this script was published with, installed when the version feed cannot be read. CAD_VERSION="0.10.3" VERSION_FEED="https://api.texttocad.dev/v1/versions" RELEASES="https://github.com/earthtojake/text-to-cad/releases/download" say() { printf '%s\n' "$*" >&2 } die() { say "error: $*" exit 1 } usage() { cat </dev/null 2>&1; then command -v uvx return 0 fi for dir in "${UV_INSTALL_DIR:-}" "${UV_INSTALL_DIR:+$UV_INSTALL_DIR/bin}" "${XDG_BIN_HOME:-}" \ "$HOME/.local/bin" "$HOME/.cargo/bin"; do if [ -n "$dir" ] && [ -x "$dir/uvx" ]; then printf '%s\n' "$dir/uvx" return 0 fi done return 1 } install_uv() { say "uv is not installed: installing it with uv's own installer (https://docs.astral.sh/uv/):" if command -v curl >/dev/null 2>&1; then say " curl -LsSf https://astral.sh/uv/install.sh | sh" curl -LsSf https://astral.sh/uv/install.sh | sh elif command -v wget >/dev/null 2>&1; then say " wget -qO- https://astral.sh/uv/install.sh | sh" wget -qO- https://astral.sh/uv/install.sh | sh else die "neither curl nor wget is here to download uv with; install uv (https://docs.astral.sh/uv/), then run this again" fi } # The newest release, from the version feed: {"latest": "X.Y.Z"}. Nothing when it cannot be read in 3 s. latest() { if command -v curl >/dev/null 2>&1; then body=$(curl -fsS --max-time 3 "$VERSION_FEED" 2>/dev/null) || return 0 elif command -v wget >/dev/null 2>&1; then body=$(wget -qO- --timeout=3 "$VERSION_FEED" 2>/dev/null) || return 0 else return 0 fi printf '%s\n' "$body" | sed -n 's/.*"latest"[[:space:]]*:[[:space:]]*"\([0-9][0-9A-Za-z.+-]*\)".*/\1/p' | head -n 1 } # Whether release $1 is older than release $2: the first three dotted numbers compared, a # suffix (-dev, rc1) ignored. The feed lags a release by up to an hour, and this script never # installs a release older than the one it was published with: that one has no `cad setup`. older() { i=1 while [ "$i" -le 3 ]; do x=$(printf '%s\n' "$1" | awk -F. -v i="$i" '{ v = $i; sub(/[^0-9].*/, "", v); print (v == "" ? 0 : v) }') y=$(printf '%s\n' "$2" | awk -F. -v i="$i" '{ v = $i; sub(/[^0-9].*/, "", v); print (v == "" ? 0 : v) }') [ "$x" -lt "$y" ] && return 0 [ "$x" -gt "$y" ] && return 1 i=$((i + 1)) done return 1 } # A release's version: a digit first, then digits, letters, dots, + and - alone. release() { case $1 in [0-9]*) ;; *) return 1 ;; esac case $1 in *[!0-9A-Za-z.+-]*) return 1 ;; esac return 0 } main() { version="" from="" verb=setup # --version picks the release and --uninstall the verb; every other argument goes to it, in order. count=$# while [ "$count" -gt 0 ]; do argument=$1 shift count=$((count - 1)) case $argument in -h | --help) usage exit 0 ;; --version) [ "$count" -gt 0 ] || die "--version needs a release, as in --version $CAD_VERSION" version=$1 shift count=$((count - 1)) ;; --version=*) version=${argument#--version=} ;; --from) [ "$count" -gt 0 ] || die "--from needs a wheel, a folder or a requirement, as in --from ./cad_server-$CAD_VERSION-py3-none-any.whl" from=$1 shift count=$((count - 1)) ;; --from=*) from=${argument#--from=} ;; --uninstall) verb=uninstall ;; *) set -- "$@" "$argument" ;; esac done [ -n "$from" ] || from=${CAD_INSTALL_FROM:-} if [ -n "$from" ]; then # A build named outright (a wheel, a folder, a requirement): uvx takes it as it is, nothing refreshed. requirement=$from refresh="" version=$from origin="as --from asked" else if [ -n "$version" ]; then origin="as --version asked" else version=$(latest) if [ -z "$version" ]; then version=$CAD_VERSION origin="this script's own: the version feed could not be read" elif older "$version" "$CAD_VERSION"; then origin="this script's own: the version feed still names $version" version=$CAD_VERSION else origin="the newest release" fi fi release "$version" || die "not a release: $version" requirement="cad-server==$version" # The server, and the library it pins at its own release: uv reads both anew from the index. refresh="--refresh-package cad-server --refresh-package cad" fi if ! uvx=$(find_uvx); then install_uv uvx=$(find_uvx) || die "uv was installed, but uvx was not found: open a new terminal and run this again" fi # uvx's folder on PATH: `cad setup` writes uvx's absolute path into the apps' configs. PATH="$(dirname "$uvx"):$PATH" export PATH say "text-to-cad: $version ($origin). The first run downloads it, which takes a minute." # cad asks nothing, so it reads no terminal. $refresh is four words or none. say "Running: $uvx --no-config --managed-python --python 3.14 ${refresh:+$refresh }--from $requirement cad $verb${*:+ $*}" exec "$uvx" --no-config --managed-python --python 3.14 $refresh --from "$requirement" cad "$verb" "$@" } main "$@"