Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

182 lines · 8.5 KBRaw History
1#!/usr/bin/env bash
2# Installs the Diffinery CLI on Linux and macOS:
3#
4# curl -fsSL https://diffinery.com/install | bash
5#
6# The CLI goes in ~/.diffinery (set DIFFINERY_INSTALL to change that), and ~/.diffinery/bin is
7# added to PATH in your shells' startup files, with a link in ~/.local/bin or ~/bin when one of
8# those is on PATH already so it works at once (DIFFINERY_NO_MODIFY_PATH=1 skips both). It runs on
9# Node.js 22 or later: the one on your PATH when it's new enough, otherwise a private copy of the
10# Node.js LTS from nodejs.org, checked against its published SHA-256, in ~/.diffinery/node.
11# Run it again, or `diffinery upgrade`, to update. Served by Diffinery from apps/cli/install/.
12
13# Piped into sh instead of bash, a shell like dash stops at `set -o pipefail` with no hint why.
14if [ -z "${BASH_VERSION:-}" ]; then
15 echo "This installer needs bash: curl -fsSL __DIFFINERY_URL__/install | bash" >&2
16 exit 1
17fi
18set -euo pipefail
19
20server=${DIFFINERY_URL:-__DIFFINERY_URL__}
21case $server in __*) server=https://diffinery.com ;; esac
22server=${server%/}
23root=${DIFFINERY_INSTALL:-$HOME/.diffinery}
24bin=$root/bin
25node_line=24
26
27if [ -t 2 ] && [ -z "${NO_COLOR:-}" ]; then
28 bold=$'\033[1m' dim=$'\033[2m' green=$'\033[32m' red=$'\033[31m' reset=$'\033[0m'
29else
30 bold='' dim='' green='' red='' reset=''
31fi
32say() { printf '%s\n' "$*" >&2; }
33die() {
34 say "${red}error${reset}: $*"
35 exit 1
36}
37tilde() { case $1 in "$HOME"/*) printf '~%s' "${1#"$HOME"}" ;; *) printf '%s' "$1" ;; esac; }
38
39command -v curl >/dev/null || die "curl is required"
40case $(uname -s) in
41 Linux) os=linux ;;
42 Darwin) os=darwin ;;
43 MINGW* | MSYS* | CYGWIN*) die "on Windows, run this in PowerShell instead: irm $server/install.ps1 | iex" ;;
44 *) die "$(uname -s) isn't supported by this script; install Node.js 22 or later and download $server/-/cli/diffinery.mjs" ;;
45esac
46case $(uname -m) in
47 x86_64 | amd64) arch=x64 ;;
48 arm64 | aarch64) arch=arm64 ;;
49 *) arch='' ;;
50esac
51# A shell under Rosetta on Apple silicon still gets the arm64 Node.js.
52if [ "$os" = darwin ] && [ "$arch" = x64 ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" = 1 ]; then arch=arm64; fi
53
54tmp=$(mktemp -d)
55trap 'rm -rf "$tmp"' EXIT
56
57node_ok() { "$1" -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 22 ? 0 : 1)' >/dev/null 2>&1; }
58
59# 1. Node.js for the CLI to run on.
60private=$root/node/bin/node
61if [ -x "$private" ] && node_ok "$private"; then
62 node=$private
63elif command -v node >/dev/null && node_ok node; then
64 node=''
65else
66 [ -n "$arch" ] || die "the CLI needs Node.js 22 or later, and nodejs.org has no build for $(uname -m); install Node.js and run this again"
67 if [ "$os" = linux ] && ldd --version 2>&1 | grep -qi musl; then die "the CLI needs Node.js 22 or later; on Alpine and other musl systems install it with your package manager (apk add nodejs) and run this again"; fi
68 say "Installing Node.js $node_line LTS for the CLI, from nodejs.org…"
69 dist=https://nodejs.org/dist/latest-v$node_line.x
70 sums=$(curl -fsSL "$dist/SHASUMS256.txt") || die "couldn't reach nodejs.org"
71 line=$(printf '%s\n' "$sums" | grep -E " node-v[0-9.]+-$os-$arch\.tar\.gz$" | head -n 1 || true)
72 [ -n "$line" ] || die "nodejs.org has no Node.js build for $os-$arch"
73 want=${line%% *}
74 file=${line##* }
75 curl -fsSL "$dist/$file" -o "$tmp/node.tar.gz" || die "couldn't download $file"
76 if command -v sha256sum >/dev/null; then got=$(sha256sum "$tmp/node.tar.gz"); else got=$(shasum -a 256 "$tmp/node.tar.gz"); fi
77 [ "${got%% *}" = "$want" ] || die "$file doesn't match its published SHA-256, so it wasn't installed"
78 mkdir -p "$tmp/node" "$root"
79 tar -xzf "$tmp/node.tar.gz" -C "$tmp/node" --strip-components 1
80 rm -rf "$root/node"
81 mv "$tmp/node" "$root/node"
82 node=$private
83fi
84
85# 2. The CLI itself: one JavaScript file.
86curl -fsSL "$server/-/cli/diffinery.mjs" -o "$tmp/diffinery.mjs" || die "couldn't download the CLI from $server"
87head -n 1 "$tmp/diffinery.mjs" | grep -q '^#!/usr/bin/env node' || die "$server/-/cli/diffinery.mjs isn't the CLI"
88mkdir -p "$root/lib" "$bin"
89chmod 755 "$tmp/diffinery.mjs"
90mv -f "$tmp/diffinery.mjs" "$root/lib/diffinery.mjs"
91
92# 3. The diffinery command: the private Node.js when there is one, else the node on PATH.
93q=$(printf '%q' "$root")
94cat >"$tmp/diffinery" <<EOF
95#!/bin/sh
96# The Diffinery CLI, installed from $server/install. Update it with: diffinery upgrade
97root=$q
98node=\$root/node/bin/node
99[ -x "\$node" ] || node=node
100exec "\$node" "\$root/lib/diffinery.mjs" "\$@"
101EOF
102chmod 755 "$tmp/diffinery"
103mv -f "$tmp/diffinery" "$bin/diffinery"
104version=$("$bin/diffinery" --version) || die "the CLI was installed in $(tilde "$root") but doesn't start; run $bin/diffinery --version to see why"
105
106# 4. PATH: in startup files for new terminals, plus a link in a directory already on PATH (such as
107# ~/.local/bin) so the command works right away, in this terminal and in agents' shells too.
108added='' kept='' ready='' linked=''
109# As written in startup files: $HOME/.diffinery/bin rather than the expanded path, when it's in $HOME.
110case $bin in "$HOME"/*) rc_bin='$HOME'${bin#"$HOME"} ;; *) rc_bin=$bin ;; esac
111on_path() { case ":$PATH:" in *":$1:"*) return 0 ;; esac; return 1; }
112# Adds the line for this kind of file, once: sh syntax for bash, zsh and sh, fish's own for fish.
113add_to() {
114 local rc=$1 line
115 case $rc in
116 *.fish) line="fish_add_path \"$rc_bin\"" ;;
117 *) line="case \":\$PATH:\" in *\":$rc_bin:\"*) ;; *) export PATH=\"$rc_bin:\$PATH\" ;; esac" ;;
118 esac
119 if grep -qsF -e "$bin" -e "$rc_bin" "$rc"; then kept=${kept:+$kept, }$(tilde "$rc"); return; fi
120 mkdir -p "$(dirname "$rc")"
121 printf '\n# Diffinery CLI\n%s\n' "$line" >>"$rc"
122 added=${added:+$added, }$(tilde "$rc")
123}
124if on_path "$bin"; then
125 ready=1
126elif [ -z "${DIFFINERY_NO_MODIFY_PATH:-}" ]; then
127 # The startup files of the login shell, created if missing, and of any other shell already set
128 # up here. Bash reads only the first of ~/.bash_profile, ~/.bash_login and ~/.profile in a login
129 # shell (every macOS terminal), so the line goes in the one it reads, never a new one that hides it.
130 shell=$(basename "${SHELL:-sh}")
131 zrc=${ZDOTDIR:-$HOME}/.zshrc
132 bash_login=''
133 for f in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do
134 if [ -f "$f" ]; then bash_login=$f; break; fi
135 done
136 rcs=()
137 case $shell in
138 zsh) rcs+=("$zrc") ;;
139 bash) rcs+=("$HOME/.bashrc" "${bash_login:-$([ "$os" = darwin ] && echo "$HOME/.bash_profile" || echo "$HOME/.profile")}") ;;
140 fish) rcs+=("$HOME/.config/fish/conf.d/diffinery.fish") ;;
141 *) rcs+=("$HOME/.profile") ;;
142 esac
143 [ "$shell" = zsh ] || [ ! -f "$zrc" ] || rcs+=("$zrc")
144 [ "$shell" = bash ] || [ ! -f "$HOME/.bashrc" ] || rcs+=("$HOME/.bashrc")
145 [ "$shell" = fish ] || [ ! -d "$HOME/.config/fish" ] || rcs+=("$HOME/.config/fish/conf.d/diffinery.fish")
146 for rc in "${rcs[@]}"; do add_to "$rc"; done
147 # A link from a personal bin directory that's already on PATH, unless something else lives there.
148 for dir in "$HOME/.local/bin" "$HOME/bin"; do
149 if on_path "$dir" && [ -d "$dir" ] && [ -w "$dir" ]; then
150 if [ "$(readlink "$dir/diffinery" 2>/dev/null || true)" = "$bin/diffinery" ] || { [ ! -e "$dir/diffinery" ] && [ ! -L "$dir/diffinery" ]; }; then
151 ln -sfn "$bin/diffinery" "$dir/diffinery"
152 linked=$dir/diffinery ready=1
153 fi
154 break
155 fi
156 done
157 # In a workflow job, later steps get it on PATH too.
158 if [ -n "${GITHUB_PATH:-}" ] && [ -w "${GITHUB_PATH}" ]; then echo "$bin" >>"$GITHUB_PATH"; fi
159fi
160
161say "${green}✓${reset} ${version} installed to $(tilde "$bin/diffinery")${node:+ ${dim}(with its own Node.js)${reset}}"
162if [ -n "$added" ]; then
163 say "Added $(tilde "$bin") to PATH in $added."
164elif [ -n "$kept" ]; then
165 say "New terminals already get $(tilde "$bin") on PATH from $kept."
166fi
167[ -z "$linked" ] || say "Linked it from $(tilde "$linked"), so diffinery works in this terminal now."
168if [ -z "$ready" ]; then
169 if [ -n "${DIFFINERY_NO_MODIFY_PATH:-}" ]; then
170 say "Add $(tilde "$bin") to your PATH to run diffinery."
171 else
172 say "Open a new terminal, or run this to use diffinery in this one:"
173 say " ${bold}export PATH=\"$rc_bin:\$PATH\"${reset}"
174 fi
175fi
176other=$(command -v diffinery 2>/dev/null || true)
177if [ -n "$other" ] && [ "$other" != "$bin/diffinery" ] && [ "$(readlink "$other" 2>/dev/null || true)" != "$bin/diffinery" ]; then say "${dim}Another diffinery at $(tilde "$other") comes first on your PATH; remove it to use this one.${reset}"; fi
178setup="diffinery setup"
179[ "$server" = https://diffinery.com ] || setup="diffinery setup --url $server"
180say ""
181say "Next, sign in through your browser and set up git (and gh, if you have it):"
182say " ${bold}$setup${reset}"