chore: initial commit

This commit is contained in:
Stevan Freeborn
2026-07-07 18:28:35 -05:00
commit 914afa469f
44 changed files with 3917 additions and 0 deletions
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env pwsh
# bootstrap.ps1
# One-shot bootstrap for a fresh Windows machine.
# Installs chezmoi and Bitwarden CLI, authenticates with Bitwarden,
# then runs chezmoi init --apply to pull and apply this dotfiles repo.
#
# Usage (run from an elevated or standard PowerShell prompt):
# iwr -useb https://raw.githubusercontent.com/StevanFreeborn/dotfiles/main/scripts/bootstrap.ps1 | iex
$ErrorActionPreference = "Stop"
$DOTFILES_REPO = "https://gitea.freeborn.cloud/Stevan/dotfiles.git"
$DOTFILES_REPO_FALLBACK = "StevanFreeborn/dotfiles"
function Write-Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan }
function Write-Ok($msg) { Write-Host " $msg" -ForegroundColor Green }
function Write-Skip($msg) { Write-Host " $msg" -ForegroundColor DarkGray }
function Write-Err($msg) { Write-Host " ERROR: $msg" -ForegroundColor Red }
# --- Check winget ---
Write-Step "Checking winget..."
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Write-Err "winget not found. Install App Installer from the Microsoft Store and re-run."
exit 1
}
Write-Ok "winget is available."
# --- Helper: winget install if not present ---
function Install-WingetPackage($id, $name) {
if (Get-Command $name -ErrorAction SilentlyContinue) {
Write-Skip "$name already installed, skipping."
return
}
Write-Step "Installing $name ($id) via winget..."
winget install --id $id --silent --accept-package-agreements --accept-source-agreements
# Refresh PATH so the new binary is usable in this session
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH", "Machine") + ";" +
[System.Environment]::GetEnvironmentVariable("PATH", "User")
Write-Ok "$name installed."
}
# --- Install Bitwarden CLI ---
Install-WingetPackage "Bitwarden.CLI" "bw"
# --- Install chezmoi ---
Install-WingetPackage "twpayne.chezmoi" "chezmoi"
# --- Bitwarden login ---
Write-Step "Checking Bitwarden login status..."
$bwStatus = (bw status 2>$null | ConvertFrom-Json -ErrorAction SilentlyContinue).status
if ($bwStatus -ne "unlocked") {
if ($bwStatus -ne "unauthenticated" -and $null -ne $bwStatus) {
Write-Skip "Already logged in, skipping login."
} else {
Write-Step "Logging into Bitwarden (enter your email and master password)..."
bw login
if ($LASTEXITCODE -ne 0) {
Write-Err "Bitwarden login failed. Please check your credentials and re-run."
exit 1
}
}
Write-Step "Unlocking Bitwarden vault..."
$env:BW_SESSION = bw unlock --raw
if (-not $env:BW_SESSION) {
Write-Err "Failed to unlock Bitwarden vault. Re-run the script and try again."
exit 1
}
Write-Ok "Vault unlocked. BW_SESSION is set."
} else {
Write-Skip "Bitwarden vault already unlocked."
if (-not $env:BW_SESSION) {
Write-Step "Refreshing BW_SESSION..."
$env:BW_SESSION = bw unlock --raw
}
}
# --- Bootstrap dotfiles ---
Write-Step "Bootstrapping dotfiles with chezmoi..."
$chezmoiSourceDir = Join-Path $env:USERPROFILE ".local\share\chezmoi"
if (Test-Path (Join-Path $chezmoiSourceDir ".git")) {
# Already initialized — pull latest and apply
Write-Step "chezmoi already initialized, pulling latest changes..."
chezmoi update
if ($LASTEXITCODE -ne 0) {
Write-Err "chezmoi update failed."
exit 1
}
Write-Ok "Dotfiles updated and applied."
} else {
# Fresh init — try Gitea first, fall back to GitHub mirror
$applied = $false
try {
chezmoi init --apply $DOTFILES_REPO
$applied = $LASTEXITCODE -eq 0
} catch {
$applied = $false
}
if (-not $applied) {
Write-Host " Primary Gitea repo unreachable, trying GitHub mirror..." -ForegroundColor Yellow
chezmoi init --apply $DOTFILES_REPO_FALLBACK
if ($LASTEXITCODE -ne 0) {
Write-Err "chezmoi init failed. Check the repo URL and your network connection."
exit 1
}
}
}
Write-Host ""
Write-Host "Bootstrap complete!" -ForegroundColor Green
Write-Host ""
Write-Host "Next steps:" -ForegroundColor Yellow
Write-Host " - Restart your terminal to pick up the new PowerShell profile."
Write-Host " - Run 'chezmoi update' at any time to sync the latest changes."
Write-Host " - To add a new machine later, re-run this script."
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env bash
# bootstrap.sh
# One-shot bootstrap for a fresh Linux (Ubuntu/Debian) machine.
# Installs curl, git, chezmoi, and Bitwarden CLI, authenticates with Bitwarden,
# then runs chezmoi init --apply to pull and apply this dotfiles repo.
#
# Usage:
# bash <(curl -fsLS https://raw.githubusercontent.com/StevanFreeborn/dotfiles/main/scripts/bootstrap.sh)
set -euo pipefail
DOTFILES_REPO="https://gitea.freeborn.cloud/Stevan/dotfiles.git"
DOTFILES_REPO_FALLBACK="StevanFreeborn/dotfiles"
step() { echo -e "\n\033[0;36m==> $*\033[0m"; }
ok() { echo -e " \033[0;32m$*\033[0m"; }
skip() { echo -e " \033[0;90m$*\033[0m"; }
err() { echo -e " \033[0;31mERROR: $*\033[0m" >&2; exit 1; }
# --- Ensure running on a supported distro ---
if ! command -v apt-get &>/dev/null; then
err "This script requires apt-get (Ubuntu/Debian). Adjust for your distro."
fi
# --- Install curl and git ---
step "Ensuring curl and git are installed..."
missing=()
command -v curl &>/dev/null || missing+=("curl")
command -v git &>/dev/null || missing+=("git")
if [ ${#missing[@]} -gt 0 ]; then
sudo apt-get update -qq
sudo apt-get install -y "${missing[@]}"
ok "Installed: ${missing[*]}"
else
skip "curl and git already present."
fi
# --- Install Bitwarden CLI ---
# Ensure /snap/bin is in PATH — snap binaries aren't visible in the current
# shell session without this when running via bash <(curl ...)
command -v snap &>/dev/null && export PATH="$PATH:/snap/bin"
step "Checking Bitwarden CLI..."
if command -v bw &>/dev/null; then
skip "bw already installed ($(bw --version))."
else
if command -v snap &>/dev/null; then
step "Installing Bitwarden CLI via snap..."
sudo snap install bw
ok "bw installed via snap."
else
# Fallback: download the official Linux binary from GitHub releases
step "snap not available; downloading Bitwarden CLI binary..."
BW_VERSION=$(curl -fsLS "https://api.github.com/repos/bitwarden/clients/releases/latest" \
| grep '"tag_name"' | grep cli | head -1 | sed 's/.*"cli-v\([^"]*\)".*/\1/' || echo "")
if [ -z "$BW_VERSION" ]; then
err "Could not determine latest Bitwarden CLI version. Install bw manually and re-run."
fi
TMP_ZIP=$(mktemp /tmp/bw-XXXXXX.zip)
curl -fsLS "https://github.com/bitwarden/clients/releases/download/cli-v${BW_VERSION}/bw-linux-${BW_VERSION}.zip" \
-o "$TMP_ZIP"
sudo unzip -o "$TMP_ZIP" -d /usr/local/bin/ bw
sudo chmod +x /usr/local/bin/bw
rm -f "$TMP_ZIP"
ok "bw ${BW_VERSION} installed to /usr/local/bin/bw."
fi
fi
# --- Install chezmoi ---
step "Checking chezmoi..."
if command -v chezmoi &>/dev/null; then
skip "chezmoi already installed ($(chezmoi --version))."
else
step "Installing chezmoi..."
sh -c "$(curl -fsLS get.chezmoi.io)" -- -b "$HOME/.local/bin"
export PATH="$HOME/.local/bin:$PATH"
ok "chezmoi installed."
fi
# --- Bitwarden login ---
step "Checking Bitwarden login status..."
BW_STATUS=$(bw status 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','unauthenticated'))" 2>/dev/null || echo "unauthenticated")
if [ "$BW_STATUS" != "unlocked" ]; then
if [ "$BW_STATUS" = "unauthenticated" ]; then
step "Logging into Bitwarden (enter your email and master password)..."
bw login || err "Bitwarden login failed. Check your credentials and re-run."
else
skip "Already logged in."
fi
step "Unlocking Bitwarden vault..."
export BW_SESSION
BW_SESSION=$(bw unlock --raw) || err "Failed to unlock Bitwarden vault."
ok "Vault unlocked. BW_SESSION is set."
else
skip "Bitwarden vault already unlocked."
if [ -z "${BW_SESSION:-}" ]; then
step "Refreshing BW_SESSION..."
export BW_SESSION
BW_SESSION=$(bw unlock --raw)
fi
fi
# --- Bootstrap dotfiles ---
step "Bootstrapping dotfiles with chezmoi..."
CHEZMOI_SOURCE_DIR="$HOME/.local/share/chezmoi"
if [ -d "$CHEZMOI_SOURCE_DIR/.git" ]; then
# Already initialized — pull latest and apply
step "chezmoi already initialized, pulling latest changes..."
chezmoi update || err "chezmoi update failed."
ok "Dotfiles updated and applied."
else
# Fresh init — try Gitea first, fall back to GitHub mirror
if chezmoi init --apply "$DOTFILES_REPO" 2>/dev/null; then
ok "Dotfiles applied from Gitea."
else
echo " Primary Gitea repo unreachable, trying GitHub mirror..."
chezmoi init --apply "$DOTFILES_REPO_FALLBACK" \
|| err "chezmoi init failed. Check the repo URL and your network connection."
ok "Dotfiles applied from GitHub mirror."
fi
fi
echo ""
echo -e "\033[0;32mBootstrap complete!\033[0m"
echo ""
echo "IMPORTANT: Open a new terminal (or run: exec zsh) before continuing."
echo " ~/.local/bin is now in your PATH via ~/.zshenv, but your current"
echo " shell session won't see it until you start a new one."
echo ""
echo " chezmoi is at: $HOME/.local/bin/chezmoi"
echo ""
echo "Next steps:"
echo " 1. Open a new terminal"
echo " 2. Run: chezmoi update at any time to sync the latest changes"
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Imports SSH private keys from ~/.ssh into Bitwarden as Secure Notes.
.DESCRIPTION
Creates a Bitwarden Secure Note for each private SSH key using the naming
convention expected by the dotfiles setup script:
"SSH Key - <filename>"
e.g. "SSH Key - zenbook_tinker", "SSH Key - stevan@freeborn.cloud"
Public keys (.pub files) and non-key files (config, known_hosts) are skipped.
Requires the Bitwarden CLI (bw) to be installed and your vault to be unlocked:
bw login # first time
$env:BW_SESSION = bw unlock --raw # each session
.EXAMPLE
# Unlock Bitwarden, then run the script
$env:BW_SESSION = bw unlock --raw
.\import-ssh-keys-to-bitwarden.ps1
.EXAMPLE
# Dry-run: see what would be imported without creating anything
.\import-ssh-keys-to-bitwarden.ps1 -WhatIf
#>
[CmdletBinding(SupportsShouldProcess)]
param (
[string]$SshDir = (Join-Path $env:USERPROFILE ".ssh")
)
$ErrorActionPreference = "Stop"
# --- Preflight checks ---
if (-not (Get-Command bw -ErrorAction SilentlyContinue)) {
Write-Error "Bitwarden CLI (bw) not found. Install it with: winget install Bitwarden.CLI"
exit 1
}
if (-not $env:BW_SESSION) {
Write-Error "BW_SESSION is not set. Unlock your vault first:`n `$env:BW_SESSION = bw unlock --raw"
exit 1
}
$vaultStatus = bw status 2>$null | ConvertFrom-Json
if ($vaultStatus.status -ne "unlocked") {
Write-Error "Bitwarden vault is not unlocked. Run: `$env:BW_SESSION = bw unlock --raw"
exit 1
}
if (-not (Test-Path $SshDir)) {
Write-Error "SSH directory not found: $SshDir"
exit 1
}
# --- Identify private keys ---
$skipNames = @("config", "known_hosts", "known_hosts.old", "authorized_keys")
$privateKeys = Get-ChildItem $SshDir -File | Where-Object {
$_.Extension -ne ".pub" -and $_.Name -notin $skipNames
}
if ($privateKeys.Count -eq 0) {
Write-Host "No private keys found in $SshDir" -ForegroundColor Yellow
exit 0
}
Write-Host "Found $($privateKeys.Count) private key(s) in $SshDir`n" -ForegroundColor Cyan
# --- Get existing Bitwarden items to avoid duplicates ---
Write-Host "Fetching existing Bitwarden items..." -ForegroundColor DarkGray
$existingItems = bw list items 2>$null | ConvertFrom-Json
$existingNames = $existingItems | ForEach-Object { $_.name }
# --- Import each key ---
$imported = 0
$skipped = 0
$failed = 0
foreach ($keyFile in $privateKeys) {
$itemName = "SSH Key - $($keyFile.Name)"
Write-Host " $($keyFile.Name)" -NoNewline
# Check for duplicate
if ($existingNames -contains $itemName) {
Write-Host " — already in Bitwarden, skipping" -ForegroundColor DarkGray
$skipped++
continue
}
if ($PSCmdlet.ShouldProcess($itemName, "Create Bitwarden Secure Note")) {
try {
$keyContent = Get-Content $keyFile.FullName -Raw -ErrorAction Stop
# Build Bitwarden Secure Note JSON
$item = [ordered]@{
organizationId = $null
collectionIds = @()
folderId = $null
type = 2 # 2 = Secure Note
name = $itemName
notes = $keyContent
favorite = $false
secureNote = @{ type = 0 }
reprompt = 0
}
$json = $item | ConvertTo-Json -Depth 5 -Compress
$encoded = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($json))
$result = bw create item $encoded 2>&1
if ($LASTEXITCODE -ne 0) {
throw $result
}
Write-Host " — imported" -ForegroundColor Green
$imported++
}
catch {
Write-Host " — FAILED: $_" -ForegroundColor Red
$failed++
}
}
}
# --- Summary ---
Write-Host ""
Write-Host "Done: $imported imported, $skipped already existed, $failed failed" -ForegroundColor Cyan
if ($imported -gt 0) {
Write-Host ""
Write-Host "Syncing vault..." -ForegroundColor DarkGray
bw sync 2>$null | Out-Null
Write-Host "Vault synced." -ForegroundColor DarkGray
}
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env bash
# import-ssh-keys-to-bitwarden.sh
# Imports SSH private keys from ~/.ssh into Bitwarden as Secure Notes.
#
# Creates a Bitwarden Secure Note for each private SSH key using the naming
# convention "SSH Key - <filename>", e.g. "SSH Key - id_ed25519".
# Public keys (.pub files) and non-key files (config, known_hosts) are skipped.
#
# Requires:
# - Bitwarden CLI (bw) — install via: sudo snap install bw
# - jq — install via: sudo apt install jq
# - BW_SESSION environment variable set
#
# Usage:
# export BW_SESSION=$(bw unlock --raw)
# ./import-ssh-keys-to-bitwarden.sh
#
# Dry-run (no changes):
# ./import-ssh-keys-to-bitwarden.sh --dry-run
set -euo pipefail
SSH_DIR="${SSH_DIR:-$HOME/.ssh}"
DRY_RUN=false
for arg in "$@"; do
case "$arg" in
--dry-run|-n) DRY_RUN=true ;;
*) echo "Unknown option: $arg" >&2; exit 1 ;;
esac
done
# --- Preflight checks ---
if ! command -v bw &>/dev/null; then
echo "Bitwarden CLI (bw) not found. Install it with: sudo snap install bw" >&2
exit 1
fi
if ! command -v jq &>/dev/null; then
echo "jq not found. Install it with: sudo apt install jq" >&2
exit 1
fi
if [ -z "${BW_SESSION:-}" ]; then
echo "BW_SESSION is not set. Unlock your vault first:" >&2
echo " export BW_SESSION=\$(bw unlock --raw)" >&2
exit 1
fi
vault_status=$(bw status 2>/dev/null | jq -r '.status')
if [ "$vault_status" != "unlocked" ]; then
echo "Bitwarden vault is not unlocked. Run: export BW_SESSION=\$(bw unlock --raw)" >&2
exit 1
fi
if [ ! -d "$SSH_DIR" ]; then
echo "SSH directory not found: $SSH_DIR" >&2
exit 1
fi
# --- Identify private keys ---
skip_names=("config" "known_hosts" "known_hosts.old" "authorized_keys" "authorized_keys2")
private_keys=()
while IFS= read -r -d '' file; do
filename=$(basename "$file")
ext="${filename##*.}"
[[ "$ext" == "pub" ]] && continue
skip=false
for skip_name in "${skip_names[@]}"; do
[[ "$filename" == "$skip_name" ]] && { skip=true; break; }
done
[[ "$skip" == false ]] && private_keys+=("$file")
done < <(find "$SSH_DIR" -maxdepth 1 -type f -print0)
if [ ${#private_keys[@]} -eq 0 ]; then
echo "No private keys found in $SSH_DIR"
exit 0
fi
echo "Found ${#private_keys[@]} private key(s) in $SSH_DIR"
echo ""
# --- Get existing Bitwarden items to avoid duplicates ---
echo "Fetching existing Bitwarden items..."
existing_names=$(bw list items 2>/dev/null | jq -r '.[].name' | sort -u)
# --- Import each key ---
imported=0
skipped=0
failed=0
for key_file in "${private_keys[@]}"; do
key_name=$(basename "$key_file")
item_name="SSH Key - $key_name"
printf " %s" "$key_name"
if echo "$existing_names" | grep -Fxq "$item_name"; then
echo " — already in Bitwarden, skipping"
((skipped++))
continue
fi
if [ "$DRY_RUN" = true ]; then
echo " — would import"
((imported++))
continue
fi
item_json=$(jq -n \
--rawfile notes "$key_file" \
--arg name "$item_name" \
'{
organizationId: null,
collectionIds: [],
folderId: null,
type: 2,
name: $name,
notes: $notes,
favorite: false,
secureNote: { type: 0 },
reprompt: 0
}')
encoded=$(echo -n "$item_json" | base64 -w0)
result=$(echo "$encoded" | bw create item 2>&1)
exit_code=$?
if [ $exit_code -eq 0 ]; then
echo " — imported"
((imported++))
else
echo " — FAILED: $result"
((failed++))
fi
done
echo ""
echo "Done: $imported imported, $skipped already existed, $failed failed"
if [ "$imported" -gt 0 ]; then
echo ""
echo "Syncing vault..."
bw sync 2>/dev/null || true
echo "Vault synced."
fi