Compare commits

...

19 Commits

Author SHA1 Message Date
04206a5471 [shell-common] Support fzf on awslogin 2025-12-12 22:27:11 +00:00
Andrew Williams
4115692e51 [macos] Add Homebrew installs 2025-11-14 09:25:08 +00:00
Andrew Williams
d27de08a5c [shell-common] Rename check function 2025-11-14 09:21:44 +00:00
Andrew Williams
7b493cbfde [shell-common] Add AWS credentials check 2025-11-14 09:20:41 +00:00
b66a25ef26 [shell-common] Fix TZ_LIST, reorder to after homebrew 2025-08-09 14:54:54 +01:00
8ca8ea5a41 [shell-common] Fix homebrew init 2025-07-31 08:27:14 +01:00
08dde00b3a [macos] Add docs for functions 2025-07-31 08:26:53 +01:00
633203a69c [gpg] Move gitconfig to private 2025-07-31 08:26:39 +01:00
Andrew Williams
3620c5cf4d [shell-common] Add Dublin to TZ 2025-07-30 13:56:58 +01:00
Andrew Williams
8838fe8f42 [git] Fix includes 2025-07-30 13:50:41 +01:00
Andrew Williams
9e8e39db66 [shell-common] Break out AWS commands 2025-07-30 13:40:59 +01:00
Andrew Williams
2b3f64646c [macos] Combine macOS specific functions 2025-07-30 13:40:43 +01:00
Andrew Williams
fcc7918f7f [zsh] Show AWS profile on the prompt if active 2025-07-30 12:33:31 +01:00
Andrew Williams
ce9bfd8224 [shell-common] Add profile display name to awslogin/logout command 2025-07-30 12:33:14 +01:00
b475182c1e [shell-common] Add awslogin command 2025-07-29 23:03:47 +01:00
1c279bede4 [macos] Add apps 2025-07-28 23:55:08 +01:00
949daf1fd7 [bin] Ruff stowage 2025-07-28 23:42:46 +01:00
Andrew Williams
5fa8925daa [shell-common] Update instances command 2025-07-28 23:38:40 +01:00
e048cc4a06 [ssh] Don't put ghostty override in the config 2025-07-28 23:36:27 +01:00
12 changed files with 338 additions and 168 deletions

View File

@@ -39,10 +39,9 @@ def add(args):
file_path = path.realpath(args.file)
package = path.realpath(path.join(args.repository, args.packages[0]))
if path.commonprefix([target, file_path]) != target:
print(f"error: '{args.add}' not under '{args.target}'",
file=sys.stderr)
print(f"error: '{args.add}' not under '{args.target}'", file=sys.stderr)
sys.exit(1)
rest = file_path[len(target) + 1:]
rest = file_path[len(target) + 1 :]
dest_path = path.join(package, rest)
dest = path.dirname(dest_path)
if not path.exists(dest):
@@ -69,7 +68,7 @@ def install(args, is_excluded):
files = [filename for filename in files if not is_excluded(filename)]
if len(files) == 0:
continue
rest = root[len(package_dir) + 1:]
rest = root[len(package_dir) + 1 :]
dest = path.join(args.target, rest)
# Create the directory path
@@ -120,11 +119,10 @@ def uninstall(args, is_excluded):
print(f"no such package: {package}; skipping", file=sys.stderr)
continue
for root, _, files in os.walk(package_dir, followlinks=True):
files = [
filename for filename in files if not is_excluded(filename)]
files = [filename for filename in files if not is_excluded(filename)]
if len(files) == 0:
continue
rest = root[len(package_dir) + 1:]
rest = root[len(package_dir) + 1 :]
dest = path.join(args.target, rest)
if rest != "":
dirs.append(dest)
@@ -155,20 +153,18 @@ def uninstall(args, is_excluded):
def make_argparser():
parser = argparse.ArgumentParser(description="A dotfile package manager.")
parser.add_argument("--verbose", "-v",
action="store_true", help="Verbose output")
parser.add_argument("--dry-run", "-n",
action="store_true", help="Dry run.")
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
parser.add_argument("--dry-run", "-n", action="store_true", help="Dry run.")
parser.add_argument(
"--target",
"-t",
default=path.expanduser('~'),
default=path.expanduser("~"),
help="Target directory in which to place symlinks",
)
parser.add_argument(
"--repository",
"-r",
default=path.expanduser('~/.dotfiles'),
default=path.expanduser("~/.dotfiles"),
help="The location of the dotfile repository",
)
parser.add_argument(
@@ -185,25 +181,29 @@ def make_argparser():
help="Replace files even if they exist.",
)
subparsers = parser.add_subparsers(dest='command', help='sub-command help')
subparsers = parser.add_subparsers(dest="command", help="sub-command help")
# List
parser_add = subparsers.add_parser('list', help='List packages in the repository')
parser_add = subparsers.add_parser("list", help="List packages in the repository")
# Add
parser_add = subparsers.add_parser('add', help='Add a file to a package')
parser_add = subparsers.add_parser("add", help="Add a file to a package")
parser_add.add_argument("file", metavar="FILE", help="File to stow")
parser_add.add_argument(
"file", metavar="FILE", help="File to stow"
"packages", metavar="PACKAGE", nargs="+", help="Packages to install"
)
parser_add.add_argument("packages", metavar="PACKAGE", nargs="+", help="Packages to install")
# Uninstall
parser_uninstall = subparsers.add_parser('uninstall', help='Remove a package')
parser_uninstall.add_argument("packages", metavar="PACKAGE", nargs="+", help="Packages to uninstall")
parser_uninstall = subparsers.add_parser("uninstall", help="Remove a package")
parser_uninstall.add_argument(
"packages", metavar="PACKAGE", nargs="+", help="Packages to uninstall"
)
# Install
parser_install = subparsers.add_parser('install', help='Install packages')
parser_install.add_argument("packages", metavar="PACKAGE", nargs="+", help="Packages to install")
parser_install = subparsers.add_parser("install", help="Install packages")
parser_install.add_argument(
"packages", metavar="PACKAGE", nargs="+", help="Packages to install"
)
return parser
@@ -213,26 +213,25 @@ def main():
args = parser.parse_args()
if args.dry_run:
args.verbose = True
exclude = [re.compile(fnmatch.translate(pattern))
for pattern in args.exclude]
exclude = [re.compile(fnmatch.translate(pattern)) for pattern in args.exclude]
def is_excluded(filename):
return any(pattern.match(filename) for pattern in exclude)
if args.command == 'list':
if args.command == "list":
for dir in os.listdir(args.repository):
if path.isdir(path.join(args.repository, dir)) and dir[0] != '.':
if path.isdir(path.join(args.repository, dir)) and dir[0] != ".":
print(dir)
elif args.command == 'add':
elif args.command == "add":
if len(args.packages) > 1:
parser.error("--add only works with a single package")
args.file = path.normpath(path.join(args.target, args.file))
if not path.isfile(args.file):
parser.error(f"no such file: {args.file}")
add(args)
elif args.command == 'install':
elif args.command == "install":
install(args, is_excluded)
elif args.command == 'uninstall':
elif args.command == "uninstall":
uninstall(args, is_excluded)

View File

@@ -11,10 +11,6 @@
rh = reset --hard
dist = "!git archive --format=tar -v --prefix=$(git describe HEAD)/ $(git describe HEAD) | gzip -9 > ../$(git describe HEAD).tar.gz"
[include]
path = ~/.gitconfig-gpg
path = ~/.gitconfig-local
[init]
defaultBranch = main
@@ -52,3 +48,8 @@
[url "ssh://git@github.com/2315-Media"]
insteadOf = "https://github.com/2315-Media"
[include]
path = ~/.gitconfig-gpg
path = ~/.gitconfig-work
path = ~/.gitconfig-local

View File

@@ -1,5 +0,0 @@
[user]
signingkey = 86DAB9F71FF20A3A
[commit]
gpgsign = true

View File

@@ -5,6 +5,7 @@ tap "homebrew/bundle"
tap "sass/sass"
brew "awscli"
brew "awslogs"
brew "dockutil"
brew "fluxcd/tap/flux"
brew "go"
@@ -18,10 +19,13 @@ brew "mas"
brew "poetry"
brew "sass/sass/sass"
brew "smartmontools"
brew "terraform"
brew "hashicorp/tap/terraform"
brew "terraform-local"
brew "tz"
brew "uv"
brew "yazi"
brew "jq"
brew "fzf"
cask "appcleaner"
cask "balenaetcher"

View File

@@ -1,4 +0,0 @@
# Updates Homebrew installation from the Brewfile
function update-brewfile() {
brew bundle check --file "$HOME/.config/Brewfile" || brew bundle --cleanup -f --file "$HOME/.config/Brewfile"
}

View File

@@ -1,15 +0,0 @@
# Updates the dock
function update-dock() {
idx=1
while read entry; do
app_name=$(echo "$entry" | cut -d $'\t' -f 1)
app_path=$(echo "$entry" | cut -d $'\t' -f 2)
app_type=$(echo "$entry" | cut -d $'\t' -f 3)
idx=$((idx+1))
dockutil --no-restart -a "$app_path" > /dev/null 2>&1
if [ "$app_type" = "persisentApps" ]; then
dockutil --move "$app_name" -p $idx
fi
done < ~/.dotfiles/macos/.config/dockConfig.txt
killall Dock
}

View File

@@ -0,0 +1,36 @@
# macOS tags downloads with "com.apple.quarantine" attribute to prevent execution of downloaded files until the user explicitly allows it.
# This function removes that attribute, allowing the file to be opened without warning.
# Usage: itsok <file_path>
function itsok() {
if [[ $(uname) == "Darwin" ]]; then
xattr -d com.apple.quarantine $1
else
echo 'This only works on macOS...'
fi
}
# Runs a brew bundle check and installs missing packages
# Usage: update-brewfile
function update-brewfile() {
brew bundle check --file "$HOME/.config/Brewfile" || brew bundle --cleanup -f --file "$HOME/.config/Brewfile"
}
# Updates the macOS Dock based on a configuration file
# The configuration file should be in the format:
# app_name<TAB>app_path<TAB>app_type
# where app_type can be "persisentApps" or "other"
# Usage: update-dock
function update-dock() {
idx=1
while read entry; do
app_name=$(echo "$entry" | cut -d $'\t' -f 1)
app_path=$(echo "$entry" | cut -d $'\t' -f 2)
app_type=$(echo "$entry" | cut -d $'\t' -f 3)
idx=$((idx+1))
dockutil --no-restart -a "$app_path" > /dev/null 2>&1
if [ "$app_type" = "persisentApps" ]; then
dockutil --move "$app_name" -p $idx
fi
done < ~/.dotfiles/macos/.config/dockConfig.txt
killall Dock
}

View File

@@ -0,0 +1,246 @@
# Get the list of AWS profiles
function awsprofiles() {
profiles=$(aws --no-cli-pager configure list-profiles 2> /dev/null)
if [[ -z "$profiles" ]]; then
echo "No AWS profiles found in '$HOME/.aws/config, check if ~/.aws/config exists and properly configured.'"
return 1
else
echo $profiles
fi
}
# login via SSO to AWS
function awslogin() {
local profile=""
local region=""
# Parse optional arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--profile)
profile="$2"
shift 2
;;
--region)
region="$2"
shift 2
;;
*)
echo "Unknown option: $1"
echo "Usage: awslogin [--profile prof] [--region region]"
return 1
;;
esac
done
# Get available profiles
local available_profiles
available_profiles=$(aws --no-cli-pager configure list-profiles 2> /dev/null)
if [[ -z "$available_profiles" ]]; then
echo "No AWS profiles found in ~/.aws/config"
return 1
fi
# If no profile provided, use fzf to select one
if [[ -z "$profile" ]]; then
profile=$(echo "$available_profiles" | fzf --header "Select AWS profile" --height 40%)
if [[ -z "$profile" ]]; then
echo "No profile selected."
return 1
fi
else
# Check if provided profile exists
if ! echo "$available_profiles" | grep -qx "$profile"; then
echo "Profile '$profile' not found. Searching for matches..."
local matched_profiles
matched_profiles=$(echo "$available_profiles" | grep -i "$profile")
if [[ -z "$matched_profiles" ]]; then
echo "No matching profiles found."
return 1
fi
profile=$(echo "$matched_profiles" | fzf --header "Select AWS profile" --height 40%)
if [[ -z "$profile" ]]; then
echo "No profile selected."
return 1
fi
fi
fi
# Build AWS CLI options
local aws_opts=()
[[ -n "$profile" ]] && aws_opts+=(--profile "$profile")
[[ -n "$region" ]] && aws_opts+=(--region "$region")
# Login via SSO
aws sso login "${aws_opts[@]}"
# Export AWS credentials
while IFS= read -r line; do
[[ -n "$line" ]] && eval "export $line"
done < <(aws configure export-credentials --format env "${aws_opts[@]}")
if [[ $? -ne 0 ]]; then
echo "Failed to export AWS credentials."
return 2
fi
echo "AWS login successful. Credentials exported."
export AWS_PROFILE_ACTIVE="$profile"
if [[ -n "$profile" ]]; then
export AWS_PROFILE_DISPLAY="[aws: $profile]"
else
export AWS_PROFILE_DISPLAY=""
fi
}
function awslogout() {
unset AWS_PROFILE_ACTIVE
unset AWS_ACCESS_KEY_ID
unset AWS_SECRET_ACCESS_KEY
unset AWS_SESSION_TOKEN
unset AWS_CREDENTIAL_EXPIRATION
export AWS_PROFILE_DISPLAY=""
echo "AWS profile and credentials cleared."
}
function _aws_creds_expiration_check() {
if [[ -n "$AWS_CREDENTIAL_EXPIRATION" ]]; then
local expiration_epoch
local current_epoch
# Convert expiration time to epoch (handles ISO 8601 format)
if command -v gdate &> /dev/null; then
# macOS with GNU coreutils installed
expiration_epoch=$(gdate -d "$AWS_CREDENTIAL_EXPIRATION" +%s 2>/dev/null)
current_epoch=$(gdate +%s)
else
# macOS with BSD date
expiration_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "$AWS_CREDENTIAL_EXPIRATION" +%s 2>/dev/null)
current_epoch=$(date +%s)
fi
if [[ $? -eq 0 && -n "$expiration_epoch" ]]; then
if [[ $current_epoch -ge $expiration_epoch ]]; then
echo "AWS credentials have expired. Logging out..."
awslogout
fi
fi
fi
}
# easy access to SSH
function awsssh() {
local profile=""
local region=""
local username="ansible"
local search=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--profile)
profile="$2"
shift 2
;;
--region)
region="$2"
shift 2
;;
*)
search="$1"
shift
;;
esac
done
if [[ -z "$search" ]]; then
echo "Usage: awsssh [--profile prof] [--region reg] [user@]search-term"
return 1
fi
# Extract username if provided as user@search
if [[ "$search" == *@* ]]; then
username="${search%@*}"
search="${search#*@}"
fi
# Build AWS CLI options
local aws_opts=()
[[ -n "$profile" ]] && aws_opts+=(--profile "$profile")
[[ -n "$region" ]] && aws_opts+=(--region "$region")
# Get matching instances
local instances
instances=$(aws ec2 describe-instances \
--filters "Name=tag:Name,Values=*$search*" \
--query 'Reservations[].Instances[].{
Name: Tags[?Key==`Name`].Value | [0],
IP: PublicIpAddress,
InstanceId: InstanceId
}' \
--output json \
"${aws_opts[@]}")
if [[ $? -ne 0 || -z "$instances" || "$instances" == "[]" ]]; then
echo "Failed to retrieve instances or no match found."
return 2
fi
# Select instance using fzf
local selection
selection=$(echo "$instances" | jq -r '.[] | "\(.Name): \(.IP // "no-ip") (\(.InstanceId))"' |
fzf -1 -0 --header "Select an instance")
if [[ -z "$selection" ]]; then
echo "No valid instance selected."
return 3
fi
# Extract IP and InstanceId from selection
local ip instance_id
ip=$(echo "$selection" | sed -E 's/.*: (.*) \(.*/\1/')
instance_id=$(echo "$selection" | sed -E 's/.*\((i-[a-z0-9]+)\).*/\1/')
if [[ "$ip" != "no-ip" ]]; then
echo "Connecting to $username@$ip via SSH..."
ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 "${username}@${ip}"
else
echo "No public IP found. Falling back to AWS Session Manager..."
aws ssm start-session --target "$instance_id" "${aws_opts[@]}"
fi
}
function instances() {
local profile=""
local region=""
# Parse optional arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--profile)
profile="$2"
shift 2
;;
--region)
region="$2"
shift 2
;;
*)
echo "Unknown option: $1"
echo "Usage: list_ec2_instances [--profile prof] [--region region]"
return 1
;;
esac
done
# Build AWS CLI options
local aws_opts=()
[[ -n "$profile" ]] && aws_opts+=(--profile "$profile")
[[ -n "$region" ]] && aws_opts+=(--region "$region")
# Query EC2 for names and instance IDs
aws ec2 describe-instances \
--query 'Reservations[].Instances[].{Name: Tags[?Key==`Name`].Value | [0], InstanceId: InstanceId}' \
--output table \
"${aws_opts[@]}"
}

View File

@@ -32,14 +32,14 @@ if [ -f $HOME/.cargo/env ]; then
source $HOME/.cargo/env
fi
# https://github.com/oz/tz
if [ -x "$(command -v tz)" ]; then
export TZ_LIST="America/New_York,WDW;America/Los_Angeles,DLR;Europe/Paris,DLP"
fi
# macOS Specific envs
if [[ $(uname) == "Darwin" ]]; then
# Homebrew
export HOMEBREW_NO_ENV_HINTS=1
[ -d /opt/homebrew ] && eval $(/opt/homebrew/bin/brew shellenv)
[ -d /opt/homebrew ] && eval "$(/opt/homebrew/bin/brew shellenv)"
fi
# https://github.com/oz/tz
if [ -x "$(command -v tz)" ]; then
export TZ_LIST="Europe/Dublin,Portwest HQ;America/New_York,WDW;America/Los_Angeles,DLR;Europe/Paris,DLP"
fi

View File

@@ -1,18 +1,17 @@
# Git pulls latest dotfiles
function update-dotfiles() {
prevdir=$PWD
if [ -d "${HOME}/.dotfiles" ]; then
cd $HOME/.dotfiles
git pull --rebase --autostash
fi
if [ -d "${HOME}/.dotfiles-private" ]; then
cd $HOME/.dotfiles-private
git pull --rebase --autostash
fi
cd $prevdir
for dir in "${HOME}/.dotfiles" "${HOME}/.dotfiles-private" "${HOME}/.dotfiles-work"; do
if [ -d "$dir" ]; then
cd "$dir"
git pull --rebase --autostash
fi
done
cd "$prevdir"
}
# Wrapper around ssh-add to ease usage and also ensure basic timeouts
# Wrapper around ssh-add to easily add SSH keys with a timeout
# Usage: add-sshkey [key_name]
function add-sshkey() {
TIMEOUT="2h"
NAME=$1
@@ -40,95 +39,3 @@ function demoprompt() {
clear
fi
}
# Tag the file as OK to run
function itsok() {
if [[ $(uname) == "Darwin" ]]; then
xattr -d com.apple.quarantine $1
else
echo 'This only works on macOS...'
fi
}
# easy access to SSH
function awsssh() {
local profile=""
local region=""
local username="ec2-user"
local search=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--profile)
profile="$2"
shift 2
;;
--region)
region="$2"
shift 2
;;
*)
search="$1"
shift
;;
esac
done
if [[ -z "$search" ]]; then
echo "Usage: awsssh [--profile prof] [--region reg] [user@]search-term"
return 1
fi
# Extract username if provided as user@search
if [[ "$search" == *@* ]]; then
username="${search%@*}"
search="${search#*@}"
fi
# Build AWS CLI options
local aws_opts=()
[[ -n "$profile" ]] && aws_opts+=(--profile "$profile")
[[ -n "$region" ]] && aws_opts+=(--region "$region")
# Get matching instances
local instances
instances=$(aws ec2 describe-instances \
--filters "Name=tag:Name,Values=*$search*" \
--query 'Reservations[].Instances[].{
Name: Tags[?Key==`Name`].Value | [0],
IP: PublicIpAddress,
InstanceId: InstanceId
}' \
--output json \
"${aws_opts[@]}")
if [[ $? -ne 0 || -z "$instances" || "$instances" == "[]" ]]; then
echo "Failed to retrieve instances or no match found."
return 2
fi
# Select instance using fzf
local selection
selection=$(echo "$instances" | jq -r '.[] | "\(.Name): \(.IP // "no-ip") (\(.InstanceId))"' |
fzf -1 -0 --header "Select an instance")
if [[ -z "$selection" ]]; then
echo "No valid instance selected."
return 3
fi
# Extract IP and InstanceId from selection
local ip instance_id
ip=$(echo "$selection" | sed -E 's/.*: (.*) \(.*/\1/')
instance_id=$(echo "$selection" | sed -E 's/.*\((i-[a-z0-9]+)\).*/\1/')
if [[ "$ip" != "no-ip" ]]; then
echo "Connecting to $username@$ip via SSH..."
ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 "${username}@${ip}"
else
echo "No public IP found. Falling back to AWS Session Manager..."
aws ssm start-session --target "$instance_id" "${aws_opts[@]}"
fi
}

View File

@@ -13,4 +13,3 @@ Host *
ServerAliveInterval 60
ServerAliveCountMax 30
VerifyHostKeyDNS ask
SetEnv TERM=xterm-256color

View File

@@ -4,4 +4,6 @@ HISTSIZE=2000
SAVEHIST=1000
# Prompt
export PS1="%F{8}[%F{white}%n@%m%F{8}] (%F{white}%1~%F{8}) %F{white}%#%f "
setopt PROMPT_SUBST
export AWS_PROFILE_DISPLAY=''
export PS1='%F{8}[%F{white}%n@%m%F{8}] (%F{white}%1~%F{8}) %F{yellow}$AWS_PROFILE_DISPLAY%F{white} %#%f '