Skip to content

Instantly share code, notes, and snippets.

@pythoninthegrass
Last active April 28, 2025 20:06
Show Gist options
  • Save pythoninthegrass/883d0554915579788e4f81c2ff378c34 to your computer and use it in GitHub Desktop.
Save pythoninthegrass/883d0554915579788e4f81c2ff378c34 to your computer and use it in GitHub Desktop.
Bash shell config (~/.bashrc) for Intel and ARM Macs
# shellcheck disable=all
# PATH
# export CODE="${VSCODE_GIT_ASKPASS_NODE%/*}/bin/remote-cli"
# export PATH="$CODE:$HOME/.asdf/shims:$PATH"
# check if binary exists
check_bin() { command -v "$1" >/dev/null 2>&1; }
# aliases
alias ll='ls -FGlAhp'
alias ..='cd ../'
alias ...='cd ../../'
alias bat='bat --paging=never'
alias dd='gdd'
alias diff='diff -W $(( $(tput cols) - 2 ))'
alias flushdns='sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder'
alias mkdir='mkdir -pv'
alias open-ports='sudo lsof -i | grep LISTEN'
alias rsync='rsync -arvW --progress --stats --ignore-existing'
alias show-blocked='sudo ipfw list'
alias sudocode='SUDO_EDITOR="$(which code) --wait" sudoedit'
alias tree='tree -a -I "__*__*|*.pyc|*.bak|.DS_*|.devbox|.git|.pytest_*|.ruff_cache|.venv|facts|node_modules"'
alias vim='lvim'
# k8s
alias k="kubectl"
alias kc="kubectl config use-context"
alias kns='kubectl config set-context --current --namespace'
alias kgns="kubectl config view --minify --output 'jsonpath={..namespace}' | xargs printf '%s\n'"
alias mk='minikube'
# argo
alias argo="argo -n argo"
# terraform
alias tf='terraform'
alias tfi='terraform init'
alias tfa='terraform apply'
alias tfp='terraform plan'
alias tfpn='terraform plan -refresh=false'
# multipass
alias m='multipass'
# limactl
alias lc='limactl'
# windmill
alias wm='wmill'
# lazydocker
# https://github.com/jesseduffield/lazydocker#installation
alias lzd="lazydocker"
# asdf
# https://asdf-vm.com/guide/getting-started.html
export ASDF_DIR="$HOME/.asdf"
[[ -f "$ASDF_DIR/asdf.sh" ]] && . "$ASDF_DIR/asdf.sh"
# fzf
# https://github.com/junegunn/fzf#using-git
check_bin fzf && eval "$(fzf --bash)"
# nix
if [[ -d "/nix" ]]; then
. "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh"
export PATH="/nix/var/nix/profiles/default/bin:$PATH"
fi
# orbstack
check_bin orb && source ~/.orbstack/shell/init.bash 2>/dev/null || :
# localstask
check_bin localstack && export LOCALSTACK_ACTIVATE_PRO=1
# process-compose
# https://f1bonacc1.github.io/process-compose/cli/process-compose_completion_bash/
check_bin process-compose && source <(process-compose completion bash)
# unikraft
check_bin kraft && source <(kraft completion bash)
# linuxbrew (i.e., homebrew)
# https://docs.brew.sh/Homebrew-on-Linux
[[ -f "/home/linuxbrew/.linuxbrew/bin/brew" ]] && eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
# FUNCTIONS
cd() { builtin cd "$@"; ll; }
iface() { route get 0.0.0.0 2>/dev/null | awk '/interface: / {print $2}'; }
mkpasswd() { docker run -it --rm alpine mkpasswd -m sha-512 "$@"; }
inspect-cert() { openssl crl2pkcs7 -nocrl -certfile $1 | openssl pkcs7 -print_certs -text -noout; }
lsof() {
if [[ $# -ne 1 ]]; then
command lsof
elif [[ $1 =~ ^[0-9]+$ ]]; then
command sudo lsof -i TCP:"$1" -n -P
elif [[ $# -gt 2 ]]; then
command lsof "$@"
fi
}
# lazygit
# https://github.com/jesseduffield/lazygit?tab=readme-ov-file#installation
lg() {
export LAZYGIT_NEW_DIR_FILE=~/.lazygit/newdir
lazygit "$@"
if [ -f $LAZYGIT_NEW_DIR_FILE ]; then
cd "$(cat $LAZYGIT_NEW_DIR_FILE)"
rm -f $LAZYGIT_NEW_DIR_FILE > /dev/null
fi
}
# recursive git status
git_status() {
export GREEN='\033[0;32m'
export YELLOW='\033[0;33m'
export RED='\033[0;31m'
export BLUE='\033[0;34m'
export NC='\033[0m'
git_dir=$(find . -mindepth 1 -maxdepth 1 -type d ! -name ".git")
for dir in $git_dir; do
cd $dir && printf "${GREEN}${dir}${NC}\n"
branch=$(git branch --show-current)
status=$(git status --porcelain)
[[ -n $branch ]] && printf "${BLUE}Branch${NC}: $branch\n"
[[ -n $status ]] && printf "${YELLOW}$status${NC}\n"
[[ -z $status ]] && printf "${GREEN}Clean${NC}\n" || printf "${RED}Dirty${NC}\n"
cd ..
done
}
rgrep() {
local exclude_file="$HOME/exclude_patterns.txt"
local args=(
--color never
--max-depth 3
--no-line-number
--files-with-matches
)
[[ -f "$exclude_file" ]] && args+=(--ignore-file "$exclude_file")
if ! command -v rg &>/dev/null; then
echo "ripgrep not found. Please install ripgrep."
echo "https://github.com/BurntSushi/ripgrep?tab=readme-ov-file#installation"
return
fi
rg "${args[@]}" "$@"
}
# Check listening TCP and UDP ports
# USAGE: find_available_ports [start] [end] [max]
# EXAMPLE: find_available_ports 8000 9000 15
find_available_ports() {
local start=${1:-8000} end=${2:-9000} max=${3:-10}
local count=0 tcp_count=0 udp_count=0
echo "Searching for up to $max available port-protocol combinations in range $start-$end..."
local used_tcp_ports=$(sudo lsof -iTCP -sTCP:LISTEN -n -P 2>/dev/null \
| awk '{print $9}' \
| sed -E 's/.*:([0-9]+)$/\1/' \
| sort -nu \
| grep -E "^($start|[1-9][0-9]{4,})$" \
| awk -v end=$end '$1 <= end' \
| tr '\n' ' '
)
local used_udp_ports=$(sudo lsof -iUDP -n -P 2>/dev/null \
| awk '{print $9}' \
| sed -E 's/.*:([0-9]+)$/\1/' \
| sort -nu \
| grep -E "^($start|[1-9][0-9]{4,})$" \
| awk -v end=$end '$1 <= end' \
| tr '\n' ' '
)
for port in $(seq $start $end); do
local available=""
if ! echo "$used_tcp_ports" | grep -qw "$port"; then
available+="TCP"
((tcp_count++))
((count++))
fi
if ! echo "$used_udp_ports" | grep -qw "$port"; then
[[ -n $available ]] && available+=","
available+="UDP"
((udp_count++))
((count++))
fi
if [[ -n $available ]]; then
echo "Port $port is available ($available)"
[[ $count -eq $max ]] && break
fi
done
echo "Found $count total available ports ($tcp_count TCP, $udp_count UDP)."
}
[[ -s ~/.bashrc ]] && source ~/.bashrc
# Verify that shell is interactive
if [[ $- != *i* ]]; then return; fi
# shellcheck disable=all
# # .bashrc profiling start
# export BASH_XTRACEFD=5
# PS4=":${BASH_SOURCE[0]##*/}:$LINENO+"
# set -x
if [[ $(uname) = "Darwin" ]]; then
# arch
[[ $(sysctl -n machdep.cpu.brand_string) =~ "Apple M" ]] && arch='arm64' || arch='i386'
# homebrew
[[ $arch = 'arm64' ]] && BREW_PREFIX="/opt/homebrew" || BREW_PREFIX="/usr/local/bin"
export HOMEBREW_NO_INSTALL_CLEANUP=1
export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1
# bash location
arm() { arch -arm64 /opt/homebrew/bin/bash; }
intel() { arch -x86_64 /usr/local/bin/bash; }
# rust aarch64 target
[[ $arch = 'arm64' ]] && export RUSTFLAGS="-C link-arg=-fuse-ld=ld"
# make
export MAKEFLAGS="--jobs $(sysctl -n hw.ncpu)"
# set imagemagick version
export MAGICK_HOME="${BREW_PREFIX}/opt/imagemagick@6"
# set libssh flags (brew install libssh)
export CFLAGS="-I${BREW_PREFIX}/include"
export LDFLAGS="-L${BREW_PREFIX}/lib"
# aliases
alias ip="ipconfig getifaddr $(networksetup -listallhardwareports | awk '/Hardware Port: Wi-Fi/{getline; print $2}')"
alias tailscale="/Applications/Tailscale.app/Contents/MacOS/Tailscale"
# functions
# TODO: Delete after https://github.com/microsoft/vscode/issues/204085 is fixed
code() {
command /usr/local/bin/code "$@" 2>/dev/null
}
else
# homebrew
BREW_PREFIX="/home/linuxbrew/.linuxbrew/bin"
# make
export MAKEFLAGS="--jobs $(nproc)"
# disable bracketed paste
bind 'set enable-bracketed-paste off'
# aliases
alias open='xdg-open'
alias pbcopy='xclip -selection clipboard'
alias pbpaste='xclip -selection clipboard -o'
# functions
code() {
command /usr/bin/code-insiders "$@" 2>/dev/null
}
fi
# PATH
## BAK (macOS): export PATH="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" || source /etc/profile
export ASDF_DATA_DIR="$HOME/.asdf"
export BUN="$HOME/.bun/bin"
export BUN_INSTALL="$HOME/.bun"
export GEM_HOME="$HOME/gems"
export KREW_ROOT="$HOME/.krew"
export MODULAR_HOME="$HOME/.modular"
export MODULAR_BIN="$MODULAR_HOME/bin"
export N_PREFIX="$HOME/.n"
export NPM_GLOBAL="$HOME/.npm-global/bin"
export RUST_WITHOUT="rust-docs"
export PATH="$ASDF_DATA_DIR/shims:$HOME/.cargo/bin:$MODULAR_BIN:$BUN:$BUN_INSTALL/bin:$HOME/.local/bin:$BREW_PREFIX/bin:$BREW_PREFIX/sbin:$HOME/.rd/bin:$KREW_ROOT/bin:$GEM_HOME/bin:$N_PREFIX/bin:$NPM_GLOBAL:/usr/local/opt/gnu-getopt/bin:/usr/local/bin:/usr/local:/usr/local/sbin:$HOME/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
export PATH=$(echo -n $PATH | awk -v RS=: -v ORS=: '!x[$0]++' | sed "s/\(.*\).\{1\}/\1/")
# shell completions
[[ -f "$ASDF_DATA_DIR/asdf.sh" ]] && . "$ASDF_DATA_DIR/asdf.sh"
[[ -f "$HOME/.cargo/env" ]] && . "$HOME/.cargo/env"
[[ -f "$HOME/.fzf.bash" ]] && . "$HOME/.fzf.bash"
[[ -f "$HOME/.gitleaks.bash" ]] && . "$HOME/.gitleaks.bash"
[[ -f "$HOME/.hugo.bash" ]] && . "$HOME/.hugo.bash"
[[ -f "$HOME/.kind-completion" ]] && . "$HOME/.kind-completion"
# Source shell aliases
[[ -f "$HOME/.bash_aliases" ]] && . "$HOME/.bash_aliases"
# golang
export GOPATH="$ASDF_DATA_DIR/shims"
export GOCACHE="$HOME/go/cache"
export GOPROXY="https://proxy.golang.org,direct"
# rbenv aliases
# eval "$(rbenv init - bash)"
# Vim Default Editor
export EDITOR="lvim"
# Tame ansible-lint
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
# GLOBAL BASH HISTORY
# Avoid duplicates
HISTCONTROL=ignoredups:erasedups
# When the shell exits, append to the history file instead of overwriting it
shopt -s histappend
# After each command, append to the history file and reread it
PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND$'\n'}history -a; history -c; history -r"
# Forward search in bash (step forward via ctrl-s)
stty -ixon
# Set default blocksize for ls, df, du
export BLOCKSIZE=1k
# python
# macOS: verify brew symlinks via `brew link --overwrite python`
export PYTHONSTARTUP="$HOME/.config/startup.py"
pip_install() { python -m pip install --user "$@"; }
pip_uninstall() { python -m pip uninstall -y "$@"; }
check_bin register-python-argcomplete && eval "$(register-python-argcomplete pipx)"
export PIPX_DEFAULT_PYTHON="$ASDF_DATA_DIR/shims/python"
export UV_HTTP_TIMEOUT=60
# TODO: adds literal seconds to prompt startup time
# magic (mojo/modular)
# # check_bin magic && eval "$(magic completion --shell bash)"
# biome
check_bin biome && \
biome() {
command biome format \
--use-editorconfig=true \
--config-path="$HOME/.config/biome.jsonc" \
--write "$@"
}
# docker
# * used with `act` for GitHub Actions
# export DOCKER_HOST=$(docker context inspect --format '{{.Endpoints.docker.Host}}')
# aliases (not in ~/.bash_aliases)
alias ex-ip='curl icanhazip.com'
alias youtube-dl='yt-dlp'
# playwright (firewall prompt)
sign_playwright() {
sudo codesign --force --deep --sign - "$HOME/Library/Caches/ms-playwright/chromium-*/chrome-mac/Chromium.app"
}
# thefuck
check_bin thefuck && eval "$(thefuck --alias)"
alias f='fuck'
export THEFUCK_RULES='sudo:no_command'
export THEFUCK_REQUIRE_CONFIRMATION='false'
# starship
check_bin colorscript && colorscript -e random # crunch, random
check_bin starship && eval "$(starship init bash)"
# k8s
export KUBECONFIG="$HOME/.kube/config:$HOME/.kube/k3s.yml:$HOME/.kube/minikube.yml"
if check_bin kubectl; then
source <(kubectl completion bash)
complete -o default -F __start_kubectl k
fi
# man pager
export MANPAGER="sh -c 'col -bx | bat -l man -p'"
# aws
export AWS_VAULT_PROMPT=terminal
export AWS_DEFAULT_PROFILE="dev.use1"
# silence zsh shenanigans on 10.15+
export BASH_SILENCE_DEPRECATION_WARNING=1
# zoxide (`cd` killer)
check_bin zoxide && eval "$(zoxide init --cmd cd bash)"
if [[ "$TERM_PROGRAM" == "ghostty" ]]; then
export TERM=xterm-256color
fi
# .bashrc profiling end
# set +x

intel & arm setup

env arch -x86_64 /bin/bash
/usr/sbin/softwareupdate --install-rosetta --agree-to-license
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
rm /usr/local/bin/bash  # if alias was previously installed
brew install bash
ln -s /usr/local/Cellar/bash/5.1.16/bin/bash /usr/local/bin/bash
arm
arch; which bash
intel
arch; which bash
git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install
asdf plugin add zoxide https://github.com/nyrst/asdf-zoxide.git
asdf install zoxide latest
asdf global zoxide latest

python

pipx

brew install pipx

ipython repl w/rich

# install ipython, rich et al
python -m pip install -r ~/.config/requirements.txt

# create ~/.config/startup.py

thefuck

pipx install thefuck

argcomplete

pipx install argcomplete
activate-global-python-argcomplete

kind

# install
brew install kind

# shell completion
kind completion bash > ~/.kind-completion

rust

# install
asdf plugin-add rust https://github.com/asdf-community/asdf-rust.git
asdf install rust latest

# rust cargo env
mkdir -p ~/.cargo/env && cp env $_
[filter "lfs"]
clean = git-lfs clean -- %f
smudge = git-lfs smudge -- %f
process = git-lfs filter-process
required = true
[user]
name = pythoninthegrass
email = [email protected]
[alias]
switch = !legit switch
sync = !legit sync
publish = !legit publish
unpublish = !legit unpublish
undo = !legit undo
branches = !legit branches
parent = "!git show-branch | grep '*' | grep -v \"$(git rev-parse --abbrev-ref HEAD)\" | head -n1 | sed 's/.*\\[\\(.*\\)\\].*/\\1/' | sed 's/[\\^~].*//' #"
amend = commit --amend --no-edit
[credential]
helper = cache --timeout=28800
[maintenance]
repo = ~/git/automate_boring_stuff
repo = ~/git/meetup_bot
repo = ~/git/mvp
repo = ~/git/pre-commit-hooks
repo = ~/git/python_template
[http]
postBuffer = 524288000
[core]
pager = delta
excludesfile = /Users/lance/.gitignore
[interactive]
diffFilter = delta --color-only
[delta]
navigate = true
[init]
defaultBranch = main
[merge]
conflictstyle = diff3
tool = meld
[diff]
tool = meld
colorMoved = default
[pull]
rebase = false
[fetch]
prune = true
[push]
autoSetupRemote = true
client_secret.json
.env
scratch.*
*.pem
# MACOS
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# SOURCES:
# https://gist.github.com/spicycode/1229612
# https://github.com/olalonde/dotfiles/blob/master/tmux.conf
# https://github.com/samoshkin/tmux-config/blob/master/tmux/tmux.conf
# https://unix.stackexchange.com/a/255343
# General
set -g default-terminal "screen-256color"
set -g history-limit 20000
set -g buffer-limit 20
set -sg escape-time 0
set -g display-time 1500
set -g remain-on-exit off
set -g repeat-time 300
set -g status off
setw -g allow-rename on
setw -g automatic-rename on
setw -g aggressive-resize on
setw -g mode-mouse on
setw -g monitor-activity on
# Set parent terminal title to reflect current window in tmux session
set -g set-titles on
set -g set-titles-string "#I:#W"
# Start index of window/pane with 1, because we're humans, not computers
set -g base-index 1
setw -g pane-base-index 1
# Enable mouse support
set -g mouse on
# Copy mode, scroll and clipboard
set -g @copy_use_osc52_fallback on
set -g status-keys vi
setw -g mode-keys vi
# Unbind default key bindings, we're going to override
# unbind "\$" # rename-session
# unbind , # rename-window
# unbind % # split-window -h
# unbind '"' # split-window
# unbind } # swap-pane -D
# unbind { # swap-pane -U
# unbind [ # paste-buffer
# unbind ]
# unbind "'" # select-window
# unbind n # next-window
# unbind p # previous-window
# unbind l # last-window
# unbind M-n # next window with alert
# unbind M-p # next window with alert
# unbind o # focus thru panes
# unbind & # kill-window
# unbind "#" # list-buffer
# unbind = # choose-buffer
# unbind z # zoom-pane
# unbind M-Up # resize 5 rows up
# unbind M-Down # resize 5 rows down
# unbind M-Right # resize 5 rows right
# unbind M-Left # resize 5 rows left
# unbind all keys
unbind-key -a
source-file ~/.tmux.reset.conf
# rebind prefix (C-b) to Control-Space
set -g prefix C-Space
bind-key C-Space select-pane -t :.+
# Edit configuration and reload
bind C-e new-window -n 'tmux.conf' "sh -c '\${EDITOR:-vim} ~/.tmux.conf && tmux source ~/.tmux.conf && tmux display \"Config reloaded\"'"
# Reload tmux configuration
bind C-r source-file ~/.tmux.conf \; display "Config reloaded"
# new window and retain cwd
bind c new-window -c "#{pane_current_path}"
# Prompt to rename window right after it's created
set-hook -g after-new-window 'command-prompt -I "#{window_name}" "rename-window '%%'"'
# Rename session and window
bind r command-prompt -I "#{window_name}" "rename-window '%%'"
bind R command-prompt -I "#{session_name}" "rename-session '%%'"
# Split panes
bind | split-window -h -c "#{pane_current_path}"
bind _ split-window -v -c "#{pane_current_path}"
# Select pane and windows
bind -r C-[ previous-window
bind -r C-] next-window
bind -r [ select-pane -t :.-
bind -r ] select-pane -t :.+
bind -r Tab last-window # cycle thru MRU tabs
bind -r C-o swap-pane -D
# Zoom pane
bind + resize-pane -Z
# Link window
bind L command-prompt -p "Link window from (session:window): " "link-window -s %% -a"
# Swap panes back and forth with 1st pane
# When in main-(horizontal|vertical) layouts, the biggest/widest panel is always @1
bind \ if '[ #{pane_index} -eq 1 ]' \
'swap-pane -s "!"' \
'select-pane -t:.1 ; swap-pane -d -t 1 -s "!"'
# Kill pane/window/session shortcuts
bind x kill-pane
bind X kill-window
bind C-x confirm-before -p "kill other windows? (y/n)" "kill-window -a"
bind Q confirm-before -p "kill-session #S? (y/n)" kill-session
# Merge session with another one (e.g. move all windows)
# If you use adhoc 1-window sessions, and you want to preserve session upon exit
# but don't want to create a lot of small unnamed 1-window sessions around
# move all windows from current session to main named one (dev, work, etc)
bind C-u command-prompt -p "Session to merge with: " \
"run-shell 'yes | head -n #{session_windows} | xargs -I {} -n 1 tmux movew -t %%'"
# Detach from session
bind d detach
bind D if -F '#{session_many_attached}' \
'confirm-before -p "Detach other clients? (y/n)" "detach -a"' \
'display "Session has only 1 client attached"'
# Hide status bar on demand
bind C-s if -F '#{s/off//:status}' 'set status off' 'set status on'
#!/usr/bin/env bash
tmux -f /dev/null -L temp start-server \; list-keys | \
sed -r \
-e "s/bind-key(\s+)([\"#~\$])(\s+)/bind-key\1\'\2\'\3/g" \
-e "s/bind-key(\s+)([\'])(\s+)/bind-key\1\"\2\"\3/g" \
-e "s/bind-key(\s+)([;])(\s+)/bind-key\1\\\\\2\3/g" \
-e "s/command-prompt -I #([SW])/command-prompt -I \"#\1\"/g" \
> ~/.tmux.reset.conf
#!/bin/sh
# rustup shell setup
# affix colons on either side of $PATH to simplify matching
case ":${PATH}:" in
*:"$HOME/.cargo/bin":*)
;;
*)
# Prepending path in case a system-installed rustc needs to be overridden
export PATH="$HOME/.cargo/bin:$PATH"
;;
esac
appnope
asttokens
backcall
colorama
commonmark
cryptography
filelock
decorator
executing
icecream
ipython
jedi
jinja2
matplotlib-inline
numpy
pandas
parso
pathvalidate
pexpect
pickleshare
prompt-toolkit
ptyprocess
pure-eval
pygments
python-dateutil
pytz
pyyaml
rich
six
stack-data
traitlets
wcwidth
yamllint
#!/usr/bin/env python3
# https://ipython.readthedocs.io/en/stable/interactive/reference.html#ipython-as-your-default-python-environment
# https://github.com/Textualize/rich#rich-repl
import os, IPython
from rich.jupyter import print
os.environ['PYTHONSTARTUP'] = '' # Prevent running this again
IPython.start_ipython()
raise SystemExit
@pythoninthegrass
Copy link
Author

pythoninthegrass commented Sep 10, 2024

git clone [email protected]:883d0554915579788e4f81c2ff378c34.git bashrc && cd $_
ln -s $(pwd)/.bashrc ~/.bashrc
ln -s $(pwd)/.bash_aliases ~/.bash_aliases
ln -s $(pwd)/.bash_profile ~/.bash_profile
ln -s $(pwd)/.gitignore ~/.gitignore
ln -s $(pwd)/.gitconfig ~/.gitconfig

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment