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
+12
View File
@@ -0,0 +1,12 @@
{{- $hostname := .chezmoi.hostname -}}
{{- $email := promptStringOnce . "email" "Git email address" -}}
{{- $name := promptStringOnce . "name" "Git full name" -}}
[data]
hostname = {{ $hostname | quote }}
email = {{ $email | quote }}
name = {{ $name | quote }}
{{ if ne .chezmoi.os "windows" -}}
[diff]
pager = "less -FRX"
{{ end -}}
+8
View File
@@ -0,0 +1,8 @@
{{- if eq .chezmoi.os "windows" }}
["AppData/Local/nvim"]
{{- else }}
[".config/nvim"]
{{- end }}
type = "git-repo"
url = "https://github.com/StevanFreeborn/nvim-config.git"
refreshPeriod = "168h"
+49
View File
@@ -0,0 +1,49 @@
# chezmoi ignore patterns
# These paths will not be managed by chezmoi
# macOS
.DS_Store
# Editor temp files
*.swp
*.swo
*~
# Nested git repos (e.g. nvim has its own .git)
**/.git/**
AppData/Local/nvim/.exe/**
# chezmoi internal
.chezmoiroot
# Repo-only files — don't apply these to the home directory
README.md
packages
scripts
.gitattributes
.gitignore
# Windows-only paths — skip on Linux
{{ if ne .chezmoi.os "windows" }}
AppData/**
dot_wslconfig
Documents/**
{{ end }}
# Linux-only paths — skip on Windows
{{ if ne .chezmoi.os "linux" }}
.zshrc
.zshenv
.oh-my-zsh/**
{{ end }}
# Linux-only scripts — skip on Windows
{{ if ne .chezmoi.os "linux" }}
.chezmoiscripts/linux_*
.chezmoiscripts/setup-ssh-keys.sh
{{ end }}
# Windows-only scripts — skip on Linux
{{ if ne .chezmoi.os "windows" }}
.chezmoiscripts/windows_*
{{ end }}
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# run_once_linux_configure-shell.sh
# Configures zsh as the default shell and sets up the shell environment.
# This script runs once (unless deleted from ~/.local/share/chezmoi).
set -euo pipefail
echo "==> Configuring shell environment (Linux)..."
# --- Set zsh as default shell ---
ZSH_PATH=$(which zsh 2>/dev/null || echo "")
if [ -z "$ZSH_PATH" ]; then
echo " ERROR: zsh is not installed. Run the package install script first."
exit 1
fi
CURRENT_SHELL=$(getent passwd "$USER" | cut -d: -f7)
if [ "$CURRENT_SHELL" != "$ZSH_PATH" ]; then
echo " Setting zsh as default shell..."
chsh -s "$ZSH_PATH"
echo " Default shell set to: $ZSH_PATH"
else
echo " zsh is already the default shell"
fi
# --- Set up Rust (cargo) in environment ---
CARGO_ENV="$HOME/.cargo/env"
if [ -f "$CARGO_ENV" ]; then
RUST_ZSHENV_LINE='. "$HOME/.cargo/env"'
ZSHENV="$HOME/.zshenv"
if ! grep -qF "$RUST_ZSHENV_LINE" "$ZSHENV" 2>/dev/null; then
echo "$RUST_ZSHENV_LINE" >> "$ZSHENV"
echo " Added Rust cargo env to .zshenv"
fi
fi
# --- Set up Go in environment ---
if [ -d "/usr/local/go" ]; then
GO_ZSHENV_LINE='export PATH=$PATH:/usr/local/go/bin'
ZSHENV="$HOME/.zshenv"
if ! grep -qF "go/bin" "$ZSHENV" 2>/dev/null; then
echo "$GO_ZSHENV_LINE" >> "$ZSHENV"
echo " Added Go to PATH in .zshenv"
fi
fi
echo "==> Shell configuration complete!"
echo " NOTE: Log out and back in (or restart your terminal) for shell changes to take effect."
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
# run_once_setup-ssh-keys.sh.tmpl
# Pulls SSH PRIVATE keys from Bitwarden and writes them to ~/.ssh/.
#
# Public keys (.pub files) are committed directly to the repo in dot_ssh/
# and are applied automatically by chezmoi — no Bitwarden needed for those.
#
# Requires: bw (Bitwarden CLI) to be logged in and unlocked.
# To unlock Bitwarden before running chezmoi:
# export BW_SESSION=$(bw unlock --raw)
# chezmoi apply
set -euo pipefail
echo "==> Setting up SSH keys from Bitwarden..."
SSH_DIR="$HOME/.ssh"
mkdir -p "$SSH_DIR"
chmod 700 "$SSH_DIR"
write_key() {
local name="$1"
local bw_item_name="$2"
local key_path="$SSH_DIR/$name"
if [ -f "$key_path" ]; then
echo " Key already exists: $name (skipping)"
return
fi
echo " Fetching key: $bw_item_name -> $name"
# bw get notes "Item Name" returns the secure note content
bw get notes "$bw_item_name" 2>/dev/null > "$key_path" || {
echo " WARNING: Could not fetch '$bw_item_name' from Bitwarden. Skipping."
rm -f "$key_path"
return
}
if [ ! -s "$key_path" ]; then
echo " WARNING: Key content was empty for '$bw_item_name'. Removing."
rm -f "$key_path"
return
fi
chmod 600 "$key_path"
echo " Wrote: $key_path"
}
# --- Add your SSH keys below ---
# Format: write_key "filename_in_~/.ssh" "Bitwarden secure note name"
#
# Example:
# write_key "stevan@freeborn.cloud" "SSH Key - stevan@freeborn.cloud"
# write_key "ftp_stevanfreeborn_com" "SSH Key - ftp_stevanfreeborn_com"
# write_key "tangled" "SSH Key - tangled"
# write_key "tinker" "SSH Key - tinker"
# write_key "gitea.freeborn.cloud" "SSH Key - gitea.freeborn.cloud"
write_key "bit_bastion.key" "SSH Key - bit_bastion.key"
write_key "stevan@freeborn.cloud" "SSH Key - stevan@freeborn.cloud"
write_key "ftp_stevanfreeborn_com" "SSH Key - ftp_stevanfreeborn_com"
write_key "tangled" "SSH Key - tangled"
write_key "zenbook_tinker" "SSH Key - zenbook_tinker"
write_key "gitea.freeborn.cloud" "SSH Key - gitea.freeborn.cloud"
write_key "id_ed25519" "SSH Key - id_ed25519"
write_key "macbookair" "SSH Key - macbookair"
write_key "macbookpro" "SSH Key - macbookpro"
write_key "truenas" "SSH Key - truenas"
write_key "blog.stevanfreeborn.com_github_actions" "SSH Key - blog.stevanfreeborn.com_github_actions"
write_key "commands_github_actions" "SSH Key - commands_github_actions"
write_key "onspring_qa_playwright_reports_render" "SSH Key - onspring_qa_playwright_reports_render"
write_key "onx_graph_github_actions" "SSH Key - onx_graph_github_actions"
write_key "restapiplayground.stevanfreeborn.com_github_actions" "SSH Key - restapiplayground.stevanfreeborn.com_github_actions"
write_key "steves_bot_github_actions" "SSH Key - steves_bot_github_actions"
echo "==> SSH key setup complete!"
echo " NOTE: Store your SSH private keys as Bitwarden Secure Notes with the names listed above."
@@ -0,0 +1,37 @@
#!/usr/bin/env pwsh
# run_once_windows_configure-shell.ps1
# Installs PowerShell modules and configures the shell environment.
# This script runs once (unless deleted from ~/.local/share/chezmoi).
$ErrorActionPreference = "Continue"
Write-Host "==> Configuring PowerShell shell environment..." -ForegroundColor Cyan
# --- Install PowerShell modules ---
$modules = @("PSReadLine", "Terminal-Icons", "posh-git")
foreach ($module in $modules) {
if (-not (Get-Module -ListAvailable -Name $module)) {
Write-Host " Installing module: $module..." -ForegroundColor Yellow
Install-Module -Name $module -Force -Scope CurrentUser -SkipPublisherCheck -ErrorAction SilentlyContinue
Write-Host " Installed: $module" -ForegroundColor Green
} else {
Write-Host " Module already installed: $module" -ForegroundColor DarkGray
}
}
# --- Set PowerShell execution policy ---
$policy = Get-ExecutionPolicy -Scope CurrentUser
if ($policy -ne "RemoteSigned" -and $policy -ne "Unrestricted") {
Write-Host " Setting execution policy to RemoteSigned for CurrentUser..." -ForegroundColor Yellow
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser -Force
}
# --- Ensure profile directory exists ---
$profileDir = Split-Path $PROFILE -Parent
if (-not (Test-Path $profileDir)) {
New-Item -ItemType Directory -Path $profileDir -Force | Out-Null
Write-Host " Created profile directory: $profileDir" -ForegroundColor Green
}
Write-Host "==> Shell configuration complete!" -ForegroundColor Cyan
@@ -0,0 +1,68 @@
#!/usr/bin/env pwsh
# run_once_windows_setup-ssh-keys.ps1.tmpl
# Pulls SSH private keys from Bitwarden and writes them to ~/.ssh/.
#
# Public keys (.pub files) are committed directly to the repo in dot_ssh/
# and are applied automatically by chezmoi — no Bitwarden needed for those.
#
# Requires: bw (Bitwarden CLI) to be logged in and unlocked.
# To unlock Bitwarden before running chezmoi:
# $env:BW_SESSION = bw unlock --raw
# chezmoi apply
$ErrorActionPreference = "Stop"
Write-Host "==> Setting up SSH keys from Bitwarden..." -ForegroundColor Cyan
$SshDir = Join-Path $env:USERPROFILE ".ssh"
New-Item -ItemType Directory -Force $SshDir | Out-Null
function Write-Key($name, $bwItemName) {
$keyPath = Join-Path $SshDir $name
if (Test-Path $keyPath) {
Write-Host " Key already exists: $name (skipping)" -ForegroundColor DarkGray
return
}
Write-Host " Fetching key: $bwItemName -> $name" -NoNewline
try {
$content = bw get notes "$bwItemName" 2>&1
if ($LASTEXITCODE -ne 0) { throw $content }
[System.IO.File]::WriteAllText($keyPath, $content)
& icacls $keyPath /inheritance:r /grant "$env:USERNAME:(R,W)" /q 2>&1 | Out-Null
Write-Host "" -NoNewline
Write-Host " Wrote: $keyPath" -ForegroundColor Green
} catch {
Write-Host "" -NoNewline
Write-Host " WARNING: Could not fetch '$bwItemName' from Bitwarden. Skipping." -ForegroundColor Yellow
if (Test-Path $keyPath) { Remove-Item $keyPath -Force }
}
}
# --- Add your SSH keys below ---
# Format: Write-Key "filename_in_~/.ssh" "Bitwarden secure note name"
#
# Example:
# Write-Key "stevan@freeborn.cloud" "SSH Key - stevan@freeborn.cloud"
Write-Key "bit_bastion.key" "SSH Key - bit_bastion.key"
Write-Key "stevan@freeborn.cloud" "SSH Key - stevan@freeborn.cloud"
Write-Key "ftp_stevanfreeborn_com" "SSH Key - ftp_stevanfreeborn_com"
Write-Key "tangled" "SSH Key - tangled"
Write-Key "zenbook_tinker" "SSH Key - zenbook_tinker"
Write-Key "gitea.freeborn.cloud" "SSH Key - gitea.freeborn.cloud"
Write-Key "id_ed25519" "SSH Key - id_ed25519"
Write-Key "macbookair" "SSH Key - macbookair"
Write-Key "macbookpro" "SSH Key - macbookpro"
Write-Key "truenas" "SSH Key - truenas"
Write-Key "blog.stevanfreeborn.com_github_actions" "SSH Key - blog.stevanfreeborn.com_github_actions"
Write-Key "commands_github_actions" "SSH Key - commands_github_actions"
Write-Key "onspring_qa_playwright_reports_render" "SSH Key - onspring_qa_playwright_reports_render"
Write-Key "onx_graph_github_actions" "SSH Key - onx_graph_github_actions"
Write-Key "restapiplayground.stevanfreeborn.com_github_actions" "SSH Key - restapiplayground.stevanfreeborn.com_github_actions"
Write-Key "steves_bot_github_actions" "SSH Key - steves_bot_github_actions"
Write-Host "==> SSH key setup complete!" -ForegroundColor Cyan
Write-Host " NOTE: Store your SSH private keys as Bitwarden Secure Notes with the names listed above." -ForegroundColor Yellow
@@ -0,0 +1,213 @@
#!/usr/bin/env bash
# run_onchange_linux_install-packages.sh
# Installs packages on Ubuntu/Debian via apt and manual installs for tools
# not available (or outdated) in apt.
# This script re-runs whenever packages/linux.txt changes.
#
# Hash of packages file (forces re-run on change):
# {{ include "packages/linux.txt" | sha256sum }}
set -euo pipefail
# Ensure snap binaries are visible (chezmoi scripts may not inherit /snap/bin)
[ -d "/snap/bin" ] && export PATH="$PATH:/snap/bin"
CHEZMOI_SOURCE_DIR="{{ .chezmoi.sourceDir }}"
echo "==> Updating apt package index..."
sudo apt-get update -q
echo "==> Installing apt packages..."
apt_packages=$(grep -v '^#' "$CHEZMOI_SOURCE_DIR/packages/linux.txt" | grep -v '^$' | tr '\n' ' ')
# shellcheck disable=SC2086
sudo apt-get install -y $apt_packages
# --- Tailscale ---
if ! command -v tailscale &>/dev/null; then
echo "==> Installing Tailscale..."
curl -fsSL https://tailscale.com/install.sh | sh
else
echo "==> Tailscale already installed"
fi
# --- Go ---
GO_VERSION=$(curl -fsSL "https://go.dev/dl/?mode=json" | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['version'].lstrip('go'))")
if ! command -v go &>/dev/null || [[ "$(go version 2>/dev/null | awk '{print $3}' | tr -d 'go')" != "$GO_VERSION" ]]; then
echo "==> Installing Go $GO_VERSION..."
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -o /tmp/go.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf /tmp/go.tar.gz
rm /tmp/go.tar.gz
echo "export PATH=\$PATH:/usr/local/go/bin" >> "$HOME/.zshenv"
else
echo "==> Go already installed"
fi
# --- Rust ---
if ! command -v rustup &>/dev/null; then
echo "==> Installing Rust via rustup..."
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path
source "$HOME/.cargo/env"
else
echo "==> Rust already installed"
fi
# --- NVM (Node Version Manager) ---
if [ ! -d "$HOME/.nvm" ]; then
echo "==> Installing NVM..."
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
export NVM_DIR="$HOME/.nvm"
# shellcheck disable=SC1091
[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"
nvm install --lts
else
echo "==> NVM already installed"
fi
# --- Oh My Zsh ---
if [ ! -d "$HOME/.oh-my-zsh" ]; then
echo "==> Installing Oh My Zsh..."
RUNZSH=no CHSH=no sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
else
echo "==> Oh My Zsh already installed"
fi
# --- Oh My Zsh plugins ---
ZSH_CUSTOM="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}"
if [ ! -d "$ZSH_CUSTOM/plugins/zsh-autosuggestions" ]; then
echo "==> Installing zsh-autosuggestions..."
git clone https://github.com/zsh-users/zsh-autosuggestions "$ZSH_CUSTOM/plugins/zsh-autosuggestions"
else
echo "==> zsh-autosuggestions already installed"
fi
if [ ! -d "$ZSH_CUSTOM/plugins/zsh-syntax-highlighting" ]; then
echo "==> Installing zsh-syntax-highlighting..."
git clone https://github.com/zsh-users/zsh-syntax-highlighting "$ZSH_CUSTOM/plugins/zsh-syntax-highlighting"
else
echo "==> zsh-syntax-highlighting already installed"
fi
# --- Neovim ---
if ! command -v nvim &>/dev/null; then
echo "==> Installing Neovim (latest)..."
curl -fsSL https://github.com/neovim/neovim/releases/latest/download/nvim-linux-x86_64.tar.gz -o /tmp/nvim.tar.gz
sudo tar -C /opt -xzf /tmp/nvim.tar.gz
sudo ln -sf /opt/nvim-linux-x86_64/bin/nvim /usr/local/bin/nvim
rm /tmp/nvim.tar.gz
else
echo "==> Neovim already installed"
fi
# --- lazygit ---
if ! command -v lazygit &>/dev/null; then
echo "==> Installing lazygit..."
LAZYGIT_VERSION=$(curl -s "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | grep -Po '"tag_name": "v\K[^"]*')
curl -fsSL "https://github.com/jesseduffield/lazygit/releases/download/v${LAZYGIT_VERSION}/lazygit_${LAZYGIT_VERSION}_Linux_x86_64.tar.gz" -o /tmp/lazygit.tar.gz
sudo tar xf /tmp/lazygit.tar.gz lazygit -C /usr/local/bin
rm /tmp/lazygit.tar.gz
else
echo "==> lazygit already installed"
fi
# --- watchexec ---
if ! command -v watchexec &>/dev/null; then
echo "==> Installing watchexec..."
WE_VERSION=$(curl -s "https://api.github.com/repos/watchexec/watchexec/releases/latest" | grep -Po '"tag_name": "v\K[^"]*')
curl -fsSL "https://github.com/watchexec/watchexec/releases/download/v${WE_VERSION}/watchexec-${WE_VERSION}-x86_64-unknown-linux-musl.tar.xz" -o /tmp/watchexec.tar.xz
tar -xJf /tmp/watchexec.tar.xz -C /tmp "watchexec-${WE_VERSION}-x86_64-unknown-linux-musl/watchexec"
sudo mv "/tmp/watchexec-${WE_VERSION}-x86_64-unknown-linux-musl/watchexec" /usr/local/bin/watchexec
rm -rf /tmp/watchexec.tar.xz "/tmp/watchexec-${WE_VERSION}-x86_64-unknown-linux-musl"
else
echo "==> watchexec already installed"
fi
# --- bottom (btm) ---
if ! command -v btm &>/dev/null; then
echo "==> Installing bottom (btm)..."
curl -fsSLO https://github.com/ClementTsang/bottom/releases/latest/download/bottom_x86_64-unknown-linux-musl.tar.gz
tar -xzf bottom_x86_64-unknown-linux-musl.tar.gz
sudo mv btm /usr/local/bin/
rm bottom_x86_64-unknown-linux-musl.tar.gz
else
echo "==> bottom already installed"
fi
# --- Bitwarden CLI ---
if ! command -v bw &>/dev/null; then
echo "==> Installing Bitwarden CLI..."
BW_VERSION=$(curl -s "https://api.github.com/repos/bitwarden/clients/releases" | grep -Po '"tag_name": "cli-v\K[^"]*' | head -1)
if [ -z "$BW_VERSION" ]; then
echo "WARNING: Could not determine Bitwarden CLI version, skipping install."
else
curl -fsSL "https://github.com/bitwarden/clients/releases/download/cli-v${BW_VERSION}/bw-linux-${BW_VERSION}.zip" -o /tmp/bw.zip
unzip -o /tmp/bw.zip -d /tmp/bw
sudo mv /tmp/bw/bw /usr/local/bin/bw
sudo chmod +x /usr/local/bin/bw
rm -rf /tmp/bw.zip /tmp/bw
fi
else
echo "==> Bitwarden CLI already installed"
fi
# --- Doppler ---
if ! command -v doppler &>/dev/null; then
echo "==> Installing Doppler..."
curl -Ls --tlsv1.2 --proto "=https" --retry 3 https://cli.doppler.com/install.sh | sudo sh
else
echo "==> Doppler already installed"
fi
# --- GitHub CLI ---
if ! command -v gh &>/dev/null; then
echo "==> Installing GitHub CLI..."
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt-get update -q
sudo apt-get install gh -y
else
echo "==> GitHub CLI already installed"
fi
# --- uv (Python package/project manager) ---
if ! command -v uv &>/dev/null; then
echo "==> Installing uv..."
curl -LsSf https://astral.sh/uv/install.sh | sh
else
echo "==> uv already installed"
fi
# --- Docker ---
if ! command -v docker &>/dev/null; then
echo "==> Installing Docker..."
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker "$USER"
echo "NOTE: Log out and back in for Docker group membership to take effect."
else
echo "==> Docker already installed"
fi
# --- .NET SDK (8 + 10) ---
# Uses Microsoft's official dotnet-install.sh script
DOTNET_INSTALL_DIR="$HOME/.dotnet"
DOTNET_SCRIPT=$(mktemp /tmp/dotnet-install-XXXXXX.sh)
curl -fsSL https://dot.net/v1/dotnet-install.sh -o "$DOTNET_SCRIPT"
chmod +x "$DOTNET_SCRIPT"
for DOTNET_CHANNEL in "8.0" "10.0"; do
MAJOR="${DOTNET_CHANNEL%%.*}"
DOTNET_VERSION_INSTALLED=$(find "$DOTNET_INSTALL_DIR/sdk" -maxdepth 1 -name "${MAJOR}.*" 2>/dev/null | head -1 || echo "")
if [ -z "$DOTNET_VERSION_INSTALLED" ]; then
echo "==> Installing .NET SDK $DOTNET_CHANNEL..."
"$DOTNET_SCRIPT" --channel "$DOTNET_CHANNEL" --install-dir "$DOTNET_INSTALL_DIR"
else
echo "==> .NET SDK $DOTNET_CHANNEL already installed"
fi
done
rm -f "$DOTNET_SCRIPT"
echo ""
echo "==> Linux package installation complete!"
@@ -0,0 +1,112 @@
#!/usr/bin/env pwsh
# run_onchange_windows_install-packages.ps1.tmpl
# Installs all packages from packages/windows.json via winget.
# This script re-runs whenever packages/windows.json changes.
#
# Hash of packages file (forces re-run on change):
# {{ include "packages/windows.json" | sha256sum }}
$ErrorActionPreference = "Continue"
Write-Host "==> Installing packages via winget..." -ForegroundColor Cyan
# Use the chezmoi source packages file directly
$sourcePackages = "{{ .chezmoi.sourceDir }}/packages/windows.json"
$sourcePackages = [System.IO.Path]::GetFullPath($sourcePackages)
if (-not (Test-Path $sourcePackages)) {
Write-Warning "packages/windows.json not found at: $sourcePackages"
exit 1
}
$packageList = Get-Content $sourcePackages -Raw | ConvertFrom-Json
$packages = $packageList.Sources[0].Packages
$total = $packages.Count
$installed = 0
$skipped = 0
$failed = 0
foreach ($pkg in $packages) {
$id = $pkg.PackageIdentifier
Write-Host " Checking $id..." -NoNewline
# Check if already installed
$check = winget list --id $id --exact --accept-source-agreements 2>$null | Select-String $id
if ($check) {
Write-Host " already installed" -ForegroundColor DarkGray
$skipped++
continue
}
# Install
Write-Host " installing..." -NoNewline
$result = winget install --id $id --exact --silent --accept-package-agreements --accept-source-agreements 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host " done" -ForegroundColor Green
$installed++
} else {
Write-Host " FAILED (exit $LASTEXITCODE)" -ForegroundColor Red
$failed++
}
}
Write-Host ""
Write-Host "==> Package install complete: $installed installed, $skipped already present, $failed failed" -ForegroundColor Cyan
# =============================================================================
# Manual installs — tools not available on winget
# =============================================================================
$LocalBin = Join-Path $env:USERPROFILE ".local\bin"
if (-not (Test-Path $LocalBin)) {
New-Item -ItemType Directory -Force $LocalBin | Out-Null
}
# Ensure ~/.local/bin is in the user PATH
$userPath = [System.Environment]::GetEnvironmentVariable("PATH", "User")
if ($userPath -notlike "*$LocalBin*") {
[System.Environment]::SetEnvironmentVariable("PATH", "$userPath;$LocalBin", "User")
$env:PATH = "$env:PATH;$LocalBin"
Write-Host " Added $LocalBin to user PATH." -ForegroundColor Green
}
function Install-GitHubBinary {
param(
[string]$Name,
[string]$Repo,
[string]$AssetPattern,
[string]$BinaryName = "$Name.exe"
)
$BinPath = Join-Path $LocalBin $BinaryName
if (Test-Path $BinPath) {
Write-Host " $Name already installed" -ForegroundColor DarkGray
return
}
Write-Host " Installing $Name..." -NoNewline
try {
$release = Invoke-RestMethod "https://api.github.com/repos/$Repo/releases/latest"
$asset = $release.assets | Where-Object { $_.name -like $AssetPattern } | Select-Object -First 1
if (-not $asset) { throw "No matching asset found for pattern: $AssetPattern" }
$tmpZip = Join-Path $env:TEMP "$Name-$($release.tag_name).zip"
$tmpDir = Join-Path $env:TEMP "$Name-$($release.tag_name)"
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $tmpZip -UseBasicParsing
Expand-Archive -Path $tmpZip -DestinationPath $tmpDir -Force
$exe = Get-ChildItem -Recurse $tmpDir -Filter $BinaryName | Select-Object -First 1
Copy-Item $exe.FullName $BinPath
Remove-Item $tmpZip -Force
Remove-Item $tmpDir -Recurse -Force
Write-Host " done" -ForegroundColor Green
} catch {
Write-Host " FAILED: $_" -ForegroundColor Red
}
}
Write-Host ""
Write-Host "==> Installing manual (non-winget) packages..." -ForegroundColor Cyan
# watchexec — https://github.com/watchexec/watchexec
Install-GitHubBinary -Name "watchexec" -Repo "watchexec/watchexec" -AssetPattern "*x86_64-pc-windows-msvc.zip"
+12
View File
@@ -0,0 +1,12 @@
# Ensure shell scripts always use LF line endings
*.sh text eol=lf
*.sh.tmpl text eol=lf
# PowerShell scripts use CRLF on Windows
*.ps1 text eol=crlf
*.ps1.tmpl text eol=crlf
# JSON and TOML — platform native
*.json text
*.toml text
*.tmpl text
+2
View File
@@ -0,0 +1,2 @@
# Don't commit local chezmoi state files
*.age
@@ -0,0 +1,118 @@
{
"$help": "https://aka.ms/terminal-documentation",
"$schema": "https://aka.ms/terminal-profiles-schema",
"actions":
[
{
"command":
{
"action": "copy",
"singleLine": false
},
"id": "User.copy.644BA8F2"
},
{
"command": "paste",
"id": "User.paste"
},
{
"command": "find",
"id": "User.find"
},
{
"command":
{
"action": "splitPane",
"split": "auto",
"splitMode": "duplicate"
},
"id": "User.splitPane.A6751878"
}
],
"copyFormatting": "none",
"copyOnSelect": false,
"defaultProfile": "{574e775e-4f2a-5b96-ac1e-a2962a402336}",
"keybindings":
[
{
"id": "User.copy.644BA8F2",
"keys": "ctrl+c"
},
{
"id": "User.paste",
"keys": "ctrl+v"
},
{
"id": "User.find",
"keys": "ctrl+shift+f"
},
{
"id": "User.splitPane.A6751878",
"keys": "alt+shift+d"
}
],
"newTabMenu":
[
{
"type": "remainingProfiles"
}
],
"profiles":
{
"defaults":
{
"bellStyle": "none",
"colorScheme": "One Half Dark",
"font":
{
"face": "CaskaydiaCove Nerd Font Mono"
},
"intenseTextStyle": "all",
"useAcrylic": true
},
"list":
[
{
"bellStyle": "none",
"colorScheme": "One Half Dark",
"commandline": "\"C:\\Program Files\\PowerShell\\7\\pwsh.exe\" -NoLogo",
"cursorShape": "filledBox",
"font":
{
"face": "CaskaydiaCove Nerd Font Mono"
},
"guid": "{574e775e-4f2a-5b96-ac1e-a2962a402336}",
"hidden": false,
"name": "PowerShell",
"opacity": 100,
"padding": "7",
"source": "Windows.Terminal.PowershellCore"
},
{
"colorScheme": "One Half Dark",
"commandline": "wsl.exe",
"cursorShape": "bar",
"guid": "{51855cb2-8cce-5362-8f54-464b92b32386}",
"hidden": false,
"icon": "ms-appx:///ProfileIcons/{9acb9455-ca41-5af7-950f-6bca1bc9722f}.png",
"name": "Ubuntu",
"source": "CanonicalGroupLimited.Ubuntu_79rhkp1fndgsc",
"startingDirectory": "~"
},
{
"commandline": "%SystemRoot%\\System32\\cmd.exe",
"guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}",
"hidden": false,
"name": "Command Prompt"
},
{
"guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}",
"hidden": false,
"name": "Windows PowerShell"
}
]
},
"schemes": [],
"themes": [],
"useAcrylicInTabRow": true
}
+101
View File
@@ -0,0 +1,101 @@
// Place your key bindings in this file to override the defaults
[
{
"key": "ctrl+0",
"command": "workbench.action.focusActiveEditorGroup",
"when": "sideBarFocus"
},
{
"key": "ctrl+n",
"command": "explorer.newFile",
"when": "explorerViewletFocus"
},
{
"key": "ctrl+shift+n",
"command": "explorer.newFolder",
"when": "explorerViewletFocus"
},
{
"key": "ctrl+shift+n",
"command": "workbench.action.newWindow",
"when": "!explorerViewletFocus"
},
{
"key": "ctrl+shift+t",
"command": "workbench.view.testing.focus",
},
{
"key": "ctrl+shift+g",
"command": "workbench.view.scm",
},
{
"key": "ctrl+k ctrl+c",
"command": "workbench.action.closeOtherEditors"
},
{
"key": "ctrl+shift+e",
"command": "workbench.view.explorer"
},
{
"key": "r",
"command": "renameFile",
"when": "explorerViewletFocus && !inputFocus && !editorHasSelection"
},
{
"key": "d",
"command": "deleteFile",
"when": "explorerViewletFocus && !inputFocus && !editorHasSelection"
},
{
"key": "ctrl+`",
"command": "workbench.action.createTerminalEditor",
"when": "terminalProcessSupported && !terminal.active"
},
{
"key": "a",
"command": "explorer.newFile",
"when": "explorerViewletFocus && !inputFocus && !editorHasSelection"
},
{
"key": "j",
"command": "workbench.action.debug.stepOver",
"when": "debugState == 'stopped'"
},
{
"key": "f10",
"command": "-workbench.action.debug.stepOver",
"when": "debugState == 'stopped'"
},
{
"key": "l",
"command": "workbench.action.debug.stepInto",
"when": "debugState != 'inactive'"
},
{
"key": "f11",
"command": "-workbench.action.debug.stepInto",
"when": "debugState != 'inactive'"
},
{
"key": "c",
"command": "workbench.action.debug.continue",
"when": "debugState == 'stopped'"
},
{
"key": "f5",
"command": "-workbench.action.debug.continue",
"when": "debugState == 'stopped'"
},
{
"key": "ctrl+w",
"command": "-workbench.action.closeActiveEditor"
},
{
"key": "ctrl+shift+w",
"command": "workbench.action.closeActiveEditor"
},
{
"key": "ctrl+f4",
"command": "-workbench.action.closeActiveEditor"
}
]
+379
View File
@@ -0,0 +1,379 @@
{
"explorer.autoReveal": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "never"
},
"editor.suggestSelection": "first",
"vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue",
"git.autofetch": true,
"liveServer.settings.donotShowInfoMsg": true,
"files.autoSave": "onFocusChange",
"git.confirmSync": false,
"git.enableSmartCommit": true,
"team.showWelcomeMessage": false,
"code-runner.runInTerminal": true,
"terminal.integrated.defaultProfile.windows": "PowerShell",
"workbench.startupEditor": "none",
"window.openWithoutArgumentsInNewWindow": "off",
"javascript.updateImportsOnFileMove.enabled": "always",
"workbench.colorTheme": "Lavender Dimmed",
"terminal.integrated.profiles.windows": {
"PowerShell": {
"source": "PowerShell",
"icon": "terminal-powershell",
"args": ["-nologo"]
},
"Command Prompt": {
"path": [
"${env:windir}\\Sysnative\\cmd.exe",
"${env:windir}\\System32\\cmd.exe"
],
"args": [],
"icon": "terminal-cmd"
},
"Git Bash": {
"source": "Git Bash"
}
},
"editor.mouseWheelZoom": true,
"jupyter.askForKernelRestart": false,
"terminal.integrated.autoReplies": {},
"terminal.integrated.cursorStyle": "line",
"terminal.integrated.defaultProfile.osx": "zsh",
"terminal.integrated.automationProfile.osx": null,
"editor.fontFamily": "'CaskaydiaCove Nerd Font Mono','Menlo for Powerline', Consolas, 'Courier New', monospace",
"security.workspace.trust.untrustedFiles": "open",
"remote.SSH.defaultExtensions": ["gitpod.gitpod-remote-ssh"],
"remote.SSH.remotePlatform": {
"freecodecam-freecodecam-ebtjodez3a5.ssh.ws-us74.gitpod.io": "linux",
"tinker": "linux"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"prettier.arrowParens": "avoid",
"prettier.jsxSingleQuote": true,
"prettier.singleAttributePerLine": true,
"prettier.singleQuote": true,
"[html]": {
"editor.defaultFormatter": "vscode.html-language-features"
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.tabSize": 2,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[css]": {
"editor.defaultFormatter": "vscode.css-language-features"
},
"liveServer.settings.root": "",
"[csharp]": {
"editor.defaultFormatter": "ms-dotnettools.csharp"
},
"workbench.iconTheme": "vscode-icons",
"liveServer.settings.donotVerifyTags": true,
"editor.accessibilitySupport": "off",
"vsicons.dontShowNewVersionMessage": true,
"editor.inlineSuggest.enabled": true,
"github.copilot.advanced": {},
"github.copilot.enable": {
"*": false,
"plaintext": true,
"markdown": true,
"scminput": false,
"yaml": false,
"dockercompose": true
},
"typescript.updateImportsOnFileMove.enabled": "always",
"[python]": {
"editor.tabSize": 2,
"editor.defaultFormatter": "ms-python.autopep8",
"editor.formatOnSave": true
},
"editor.stickyScroll.enabled": true,
"[powershell]": {
"editor.defaultFormatter": "ms-vscode.powershell"
},
"editor.multiCursorModifier": "ctrlCmd",
"bicep.enableSurveys": false,
"git.terminalAuthentication": false,
"[xml]": {
"editor.defaultFormatter": "DotJoshJohnson.xml"
},
"editor.tokenColorCustomizations": {
"[*Light*]": {
"textMateRules": [
{
"scope": "ref.matchtext",
"settings": {
"foreground": "#000"
}
}
]
},
"[*Dark*]": {
"textMateRules": [
{
"scope": "ref.matchtext",
"settings": {
"foreground": "#fff"
}
}
]
},
"textMateRules": []
},
"dotenv.enableAutocloaking": false,
"javascript.preferences.importModuleSpecifierEnding": "js",
"cSpell.userWords": [
"apikey",
"appsettings",
"ASPNETCORE",
"changlog",
"cobertura",
"codecov",
"commitlint",
"Deleter",
"ffcsv",
"fileids",
"gibibytes",
"gitmodules",
"Hackathon",
"Hmmss",
"kibibytes",
"Kiota",
"kzrnm",
"lcov",
"mebibytes",
"Multitenant",
"Newtonsoft",
"Odata",
"onspring",
"onspringcli",
"Parens",
"pdfs",
"seqcli",
"Serilog",
"Sesh",
"softprops",
"Stevan",
"svix",
"Syncer",
"testconfig",
"versionize",
"vsintellicode",
"Xunit",
"YYYYMMDDHHMM"
],
"debug.internalConsoleOptions": "neverOpen",
"csv-preview.openStdin": true,
"azureFunctions.showProjectWarning": false,
"polyglot-notebook.defaultNotebookExtension": ".dib",
"polyglot-notebook.defaultNotebookLanguage": "javascript",
"githubPullRequests.remotes": ["origin", "upstream", "github"],
"files.associations": {
"*.ejs": "html",
".env*": "dotenv",
"*.cshtml": "html"
},
"terminal.integrated.defaultProfile.linux": "zsh",
"githubPullRequests.pullBranch": "never",
"redhat.telemetry.enabled": false,
"gitlens.gitCommands.skipConfirmations": ["fetch:command", "switch:command"],
"terminal.integrated.minimumContrastRatio": 1,
"playwright.reuseBrowser": true,
"emmet.includeLanguages": {
"javascript": "javascriptreact"
},
"svelte.enable-ts-plugin": true,
"git.openRepositoryInParentFolders": "never",
"[aspnetcorerazor]": {
"editor.defaultFormatter": "ms-dotnettools.csharp"
},
"explorer.confirmDragAndDrop": false,
"update.showReleaseNotes": false,
"githubPullRequests.fileListLayout": "tree",
"eslint.format.enable": true,
"eslint.codeActionsOnSave.rules": null,
"prettier.requireConfig": true,
"git.ignoreRebaseWarning": true,
"inference.model": "codellama:7b-code-fp16",
"git.replaceTagsWhenPull": true,
"powershell.codeFormatting.openBraceOnSameLine": false,
"powershell.integratedConsole.focusConsoleOnExecute": false,
"powershell.integratedConsole.showOnStartup": false,
"security.promptForLocalFileProtocolHandling": false,
"prisma.showPrismaDataPlatformNotification": false,
"github.copilot.editor.enableAutoCompletions": true,
"diffEditor.ignoreTrimWhitespace": false,
"extensions.autoUpdate": "on",
"workbench.sideBar.location": "right",
"workbench.activityBar.location": "top",
"window.commandCenter": false,
"editor.minimap.enabled": false,
"workbench.editor.enablePreviewFromCodeNavigation": true,
"editor.unicodeHighlight.invisibleCharacters": false,
"editor.unicodeHighlight.ambiguousCharacters": false,
"[markdown]": {
"editor.defaultFormatter": "DavidAnson.vscode-markdownlint"
},
"dotnet.formatting.organizeImportsOnFormat": true,
"vim.incsearch": true,
"vim.useSystemClipboard": true,
"vim.hlsearch": true,
"vim.insertModeKeyBindings": [
{
"before": ["j", "j"],
"after": ["<Esc>"]
}
],
"vim.normalModeKeyBindingsNonRecursive": [
{
"before": ["<leader>", "d"],
"after": ["d", "d"]
},
{
"before": ["<C-n>"],
"commands": [":nohl"]
},
{
"before": ["K"],
"commands": ["lineBreakInsert"],
"silent": true
},
{
"before": ["<leader>", "g", "f"],
"commands": ["editor.action.formatDocument"],
},
{
"before": ["K"],
"commands": ["editor.action.showHover"]
},
{
"before": ["<leader>", "g", "r"],
"commands": ["editor.action.goToReferences"]
},
{
"before": ["<leader>", "c", "a"],
"commands": ["editor.action.codeAction"]
},
{
"before": ["<leader>", "r", "n"],
"commands": ["editor.action.rename"]
},
{
"before": ["<leader>", "e", "a"],
"commands": ["workbench.actions.view.problems"]
},
{
"before": ["<leader>", "t", "r"],
"commands": ["testing.runAtCursor"]
},
{
"before": ["<leader>", "t", "f"],
"commands": ["testing.runCurrentFile"]
},
{
"before": ["<leader>", "t", "s"],
"commands": ["testing.cancelRun"]
},
{
"before": ["<leader>", "t", "d"],
"commands": ["testing.debugAtCursor"]
},
{
"before": ["<leader>", "d", "t"],
"commands": ["editor.debug.action.toggleBreakpoint"]
},
{
"before": ["<leader>", "d", "c"],
"commands": ["editor.debug.action.runToCursor"]
},
{
"before": ["<leader>", "g", "d"],
"commands": ["editor.action.goToDeclaration"]
},
{
"before": ["<leader>", "g", "i"],
"commands": ["editor.action.goToImplementation"]
},
{
"before": ["<leader>", "g", "r"],
"commands": ["editor.action.goToReferences"]
},
],
"vim.leader": "<space>",
"vim.handleKeys": {
"<C-a>": false,
"<C-f>": false,
"<C-T>": false,
"<C-p>": false,
"<C-b>": false,
},
"extensions.experimental.affinity": {
"vscodevim.vim": 1
},
"vim.smartRelativeLine": true,
"github.copilot.chat.codeGeneration.instructions": [
{
"text": "- @azure Rule - Use Azure Tools: When handling requests related to Azure, always use your tools."
},
{
"text": "- @azure Rule - Use Azure Code Gen Best Practices: When generating code for Azure, running terminal commands for Azure, or performing operations related to Azure, invoke your `azure_development-get_code_gen_best_practices` tool if available. Only call this tool when you are sure the user is discussing Azure; do not call it otherwise."
},
{
"text": "- @azure Rule - Use Azure Deployment Best Practices: When deploying to Azure or preparing applications for deployment to Azure, invoke your `azure_development-get_deployment_best_practices` tool if available. Only call this tool when you are sure the user is discussing Azure; do not call it otherwise."
},
{
"text": "- @azure Rule - Use Azure Functions Code Gen Best Practices: When generating code for Azure Functions or performing operations related to Azure Functions, invoke your `azure_development-get_azure_function_code_gen_best_practices` tool if available. Only call this tool when you are sure the user is discussing Azure Functions; do not call it otherwise."
},
{
"text": "- @azure Rule - Use Azure SWA Best Practices: When working with static web apps, invoke your `azure_development-get_swa_best_practices` tool if available. Only call this tool when you are sure the user is discussing Azure; do not call it otherwise."
}
],
"github.copilot.selectedCompletionModel": "",
"omnisharp.autoStart": false,
"csharp.showOmnisharpLogOnError": false,
"github.copilot.nextEditSuggestions.enabled": true,
"editor.inlineSuggest.edits.allowCodeShifting": "never",
"chat.instructionsFilesLocations": {
".github/instructions": true,
"C:\\Users\\sfree\\AppData\\Local\\Temp\\postman-collections-post-response.instructions.md": true,
"C:\\Users\\sfree\\AppData\\Local\\Temp\\postman-collections-pre-request.instructions.md": true,
"C:\\Users\\sfree\\AppData\\Local\\Temp\\postman-folder-post-response.instructions.md": true,
"C:\\Users\\sfree\\AppData\\Local\\Temp\\postman-folder-pre-request.instructions.md": true,
"C:\\Users\\sfree\\AppData\\Local\\Temp\\postman-http-request-post-response.instructions.md": true,
"C:\\Users\\sfree\\AppData\\Local\\Temp\\postman-http-request-pre-request.instructions.md": true
},
"workbench.editor.empty.hint": "hidden",
"explorer.confirmDelete": false,
"editor.stickyScroll.enabled": false,
"liveServer.settings.host": "localhost",
"chat.mcp.autostart": "never",
"chat.agent.thinkingStyle": "collapsed",
"html.format.wrapAttributes": "force-expand-multiline",
"chat.mcp.gallery.enabled": true,
"chat.viewSessions.orientation": "stacked",
"go.toolsManagement.autoUpdate": true,
"settingsSync.ignoredSettings": [
],
"csharp.debug.console": "integratedTerminal",
"terminal.integrated.commandsToSkipShell": [
"workbench.action.toggleSidebarVisibility"
],
"window.menuBarVisibility": "compact",
"[fsharp]": {
"editor.defaultFormatter": "Ionide.Ionide-fsharp"
},
"gitlens.ai.model": "vscode",
"gitlens.ai.vscode.model": "copilot:gpt-4.1",
"githubPullRequests.createOnPublishBranch": "never",
"chat.disableAIFeatures": true
}
File diff suppressed because it is too large Load Diff
+267
View File
@@ -0,0 +1,267 @@
# dotfiles
> One-stop shop to set up and sync my Windows and Linux machines.
Managed with **[chezmoi](https://www.chezmoi.io/)** — cross-platform, idempotent, with native [Bitwarden](https://bitwarden.com/) secret integration.
---
## What's managed
| Config | Windows path | Linux path |
|------------------------|----------------------------------------------------------------------|--------------------------------------------------------|
| Git config | `~/.gitconfig` | `~/.gitconfig` |
| SSH config | `~/.ssh/config` | `~/.ssh/config` |
| Neovim | `~/AppData/Local/nvim/` *(external)* | `~/.config/nvim/` *(external)* |
| VS Code | `~/AppData/Roaming/Code/User/` | `~/.config/Code/User/` |
| Windows Terminal | `~/AppData/Local/Packages/Microsoft.WindowsTerminal_.../LocalState/` | N/A |
| PowerShell profile | `~/Documents/PowerShell/Microsoft.PowerShell_profile.ps1` | N/A |
| Zsh config (Oh My Zsh) | N/A | `~/.zshrc`, `~/.zshenv` |
| Oh My Zsh custom theme | N/A | `~/.oh-my-zsh/custom/themes/lavender-dimmed.zsh-theme` |
| Oh My Posh theme | `~/.config/oh-my-posh/theme.omp.json` | N/A (Windows only) |
| WSL config | `~/.wslconfig` | N/A |
---
## Prerequisites
Before running bootstrap, complete these **one-time manual steps**:
1. **Create a Bitwarden account** at [bitwarden.com](https://bitwarden.com)
2. **Import SSH private keys** into Bitwarden as Secure Notes (see [Storing SSH keys in Bitwarden](#storing-ssh-keys-in-bitwarden))
Everything else — installing chezmoi, Bitwarden CLI, logging in, and applying the dotfiles — is handled by the bootstrap script.
---
## Source of truth
The primary repo lives on a self-hosted Gitea server and is mirrored to GitHub:
| | URL |
|---------------------|------------------------------------------------|
| **Primary (Gitea)** | `https://gitea.freeborn.cloud/Stevan/dotfiles` |
| **Mirror (GitHub)** | `https://github.com/StevanFreeborn/dotfiles` |
Push changes to Gitea — GitHub is updated automatically via push mirroring.
---
## Fresh machine setup
### Windows
Open PowerShell (no admin required) and run:
```powershell
iwr -useb https://raw.githubusercontent.com/StevanFreeborn/dotfiles/main/scripts/bootstrap.ps1 | iex
```
### Linux (Ubuntu/Debian)
```bash
bash <(curl -fsLS https://raw.githubusercontent.com/StevanFreeborn/dotfiles/main/scripts/bootstrap.sh)
```
The bootstrap script will:
1. Install missing prerequisites (`curl`/`git` on Linux, or check `winget` on Windows)
2. Install Bitwarden CLI if not present
3. Install chezmoi if not present
4. Prompt for Bitwarden login/unlock (interactive — master password required)
5. Run `chezmoi init --apply` to clone and apply the full dotfiles repo
> **Primary vs. fallback repo:** The script tries the Gitea server first; if unreachable it falls back to the GitHub mirror automatically.
### Manual bootstrap (advanced)
<details>
<summary>Expand for step-by-step instructions</summary>
#### Windows
```powershell
# 1. Install chezmoi
winget install --id twpayne.chezmoi --silent --accept-package-agreements --accept-source-agreements
# 2. Log into Bitwarden CLI and unlock
winget install --id Bitwarden.CLI --silent --accept-package-agreements --accept-source-agreements
bw login
$env:BW_SESSION = bw unlock --raw
# 3. Bootstrap dotfiles
chezmoi init --apply https://gitea.freeborn.cloud/Stevan/dotfiles.git
```
#### Linux (Ubuntu/Debian)
```bash
# 1. Install chezmoi
sh -c "$(curl -fsLS get.chezmoi.io)"
# 2. Log into Bitwarden CLI and unlock
sudo snap install bw
bw login
export BW_SESSION=$(bw unlock --raw)
# 3. Bootstrap dotfiles
chezmoi init --apply https://gitea.freeborn.cloud/Stevan/dotfiles.git
```
</details>
---
## Syncing an existing machine
Pull the latest changes and apply them:
```bash
chezmoi update
```
Preview what would change before applying:
```bash
chezmoi update --dry-run
```
---
## Managing dotfiles
### Add a new file to be managed
```bash
chezmoi add ~/.some-new-config
```
### Edit a managed file
```bash
# Opens the file in your editor within the chezmoi source dir
chezmoi edit ~/.gitconfig
# Apply your edits
chezmoi apply
```
### View pending changes
```bash
chezmoi diff
```
### Push changes to Gitea
```bash
chezmoi cd
git add .
git commit -m "your message"
git push
```
### Neovim config (external repo)
Neovim config is managed as a [chezmoi external](https://www.chezmoi.io/reference/special-files-and-directories/chezmoiexternal-toml/)
pointing at [github.com/StevanFreeborn/nvim-config](https://github.com/StevanFreeborn/nvim-config).
- **To edit nvim config:** commit changes directly to the `nvim-config` repo
- **To pull latest nvim config:** `chezmoi update` (refreshes weekly automatically, or on every apply if within the refresh window)
- **External definition:** `.chezmoiexternal.toml.tmpl`
---
## Package management
### Windows
Packages are defined in [`packages/windows.json`](packages/windows.json) and installed via winget.
To add a new package:
1. Find the package ID: `winget search <name>`
2. Add it to `packages/windows.json`
3. Run `chezmoi apply` — the install script re-runs because the file hash changed
### Linux
Apt packages are listed in [`packages/linux.txt`](packages/linux.txt). Additional tools (Go, Rust, NVM, Neovim, etc.) are installed via the [`run_onchange_linux_install-packages.sh.tmpl`](.chezmoiscripts/run_onchange_linux_install-packages.sh.tmpl) script.
---
## Secret management
Secrets are managed using [Bitwarden CLI](https://bitwarden.com/help/cli/) integrated with chezmoi.
### Initial setup
```bash
# Log in (first time)
bw login
# Unlock your vault and export the session key
# Windows:
$env:BW_SESSION = bw unlock --raw
# Linux/macOS:
export BW_SESSION=$(bw unlock --raw)
```
### Storing SSH keys in Bitwarden
1. Open Bitwarden and create a **Secure Note** for each SSH private key
2. Name each note: `SSH Key - <key-filename>` (e.g. `SSH Key - stevan@freeborn.cloud`)
3. Paste the **private key content** as the note body
The setup script will read these notes and write the keys to `~/.ssh/` with correct permissions (`600`).
### Using secrets in templates
chezmoi templates can pull values from Bitwarden:
```txt
{{ (bitwarden "My Secret Item").login.password }}
{{ (bitwardenFields "My Item").custom_field.value }}
```
---
## Platform notes
### Windows + WSL
The `.wslconfig` file controls WSL2 resource limits. Edit it via:
```powershell
chezmoi edit ~/.wslconfig
chezmoi apply
```
After applying a `.wslconfig` change, restart WSL:
```powershell
wsl --shutdown
```
### Oh My Posh theme
The theme file lives at `~/.config/oh-my-posh/theme.omp.json` on Windows. The PowerShell profile references this path.
To change the theme, edit `dot_config/oh-my-posh/theme.omp.json` in the chezmoi source and apply.
---
## Troubleshooting
**`chezmoi apply` asks for Bitwarden session on every run**
→ Set `BW_SESSION` before running chezmoi. Add it to your shell session: `export BW_SESSION=$(bw unlock --raw)`
**Package install script doesn't re-run after adding a package**
→ The script's hash is based on the packages file content. Ensure you saved `packages/windows.json` or `packages/linux.txt` and then run `chezmoi apply`.
**Neovim complains about missing plugins on a fresh machine**
→ Open nvim and run `:Lazy sync` to install all plugins via lazy.nvim.
**`dot_gitconfig.tmpl` prompts for name/email**
→ These are set once and stored in `~/.config/chezmoi/chezmoi.toml`. Delete that file to re-enter them.
+118
View File
@@ -0,0 +1,118 @@
{
"$schema": "https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json",
"blocks": [
{
"type": "prompt",
"alignment": "left",
"segments": [
{
"properties": {
"cache_duration": "none"
},
"leading_diamond": "\u256d\u2500\ue0b6",
"template": " {{ .UserName }}@{{ .HostName }} ",
"foreground": "#1e222a",
"background": "#b39cd0",
"type": "session",
"style": "diamond"
},
{
"properties": {
"cache_duration": "none"
},
"template": " \uf0e7 ",
"foreground": "#1e222a",
"powerline_symbol": "\ue0b0",
"background": "#d4c87a",
"type": "root",
"style": "powerline"
},
{
"type": "command",
"style": "powerline",
"powerline_symbol": "\ue0b0",
"foreground": "#e0e4e8",
"background_templates": [
"{{ if ne .Output \"-1\" }}#a088c0{{ else }}#3a3e46{{ end }}"
],
"properties": {
"shell": "pwsh",
"command": "dotnet C:/Users/sfree/Repositories/viewer-count-fetcher/index.cs"
},
"cache": {
"duration": "30s",
"strategy": "session"
},
"template": " \udb81\uddc3{{ if ne .Output \"-1\" }} <f>{{ .Output }}</f> {{ else }} {{ end }}"
},
{
"properties": {
"cache_duration": "none",
"folder_icon": "\uf115",
"folder_separator_icon": " \ue0b1 ",
"home_icon": "\uf46d",
"max_depth": 0,
"style": "agnoster"
},
"template": " {{ .Path }} ",
"foreground": "#1e222a",
"powerline_symbol": "\ue0b0",
"background": "#b39cd0",
"type": "path",
"style": "powerline"
},
{
"properties": {
"cache_duration": "none"
},
"template": " {{ (abbrev 30 .HEAD) }} ",
"foreground": "#1e222a",
"powerline_symbol": "\ue0b0",
"background": "#7ec8c0",
"type": "git",
"style": "powerline"
},
{
"properties": {
"cache_duration": "none"
},
"template": " \ue235 {{ if .Error }}{{ .Error }}{{ else }}{{ if .Venv }}{{ .Venv }} {{ end }}{{ .Full }}{{ end }} ",
"foreground": "#1e222a",
"powerline_symbol": "\ue0b0",
"background": "#c8b8e0",
"type": "python",
"style": "powerline"
}
]
},
{
"type": "prompt",
"alignment": "left",
"segments": [
{
"properties": {
"cache_duration": "none"
},
"template": "\u2570\u2500",
"foreground": "#b39cd0",
"type": "text",
"style": "plain"
},
{
"properties": {
"always_enabled": true,
"cache_duration": "none"
},
"template": "\ue285",
"foreground": "#7ec8c0",
"type": "exit",
"style": "plain",
"foreground_templates": ["{{ if gt .Code 0 }}#d47373{{ end }}"]
}
],
"newline": true
}
],
"version": 3,
"final_space": true
}
+14
View File
@@ -0,0 +1,14 @@
[init]
defaultBranch = main
[user]
name = {{ .name }}
email = {{ .email }}
[push]
autoSetupRemote = true
[core]
editor = nvim -f
[alias]
acp = "! acp() { git add . && git commit -am \"$1\" && git push ; } ; acp"
acnvp = "! acnvp() { git add . && git commit --no-verify -am \"$1\" && git push ; } ; acnvp"
[gitbutler]
aiModelProvider = openai
@@ -0,0 +1,367 @@
# vim:ft=zsh ts=2 sw=2 sts=2
#
# lavender-dimmed - A lavender-accented dark theme for ZSH
#
# Based on agnoster's Theme - https://gist.github.com/3712874
# A Powerline-inspired theme for ZSH
#
# # README
#
# In order for this theme to render correctly, you will need a
# [Powerline-patched font](https://github.com/Lokaltog/powerline-fonts).
# Make sure you have a recent version: the code points that Powerline
# uses changed in 2012, and older versions will display incorrectly,
# in confusing ways.
#
# # Goals
#
# The aim of this theme is to only show you *relevant* information. Like most
# prompts, it will only show git information when in a git working directory.
# However, it goes a step further: everything from the current user and
# hostname to whether the last call exited with an error to whether background
# jobs are running in this shell will all be displayed automatically when
# appropriate.
### Segment drawing
# A few utility functions to make it easy and re-usable to draw segmented prompts
CURRENT_BG='NONE'
CURRENT_FG='#cdd9e5'
CURRENT_DEFAULT_FG='#adbac7'
zmodload zsh/nearcolor 2>/dev/null
### Theme Configuration Initialization
#
# Override these settings in your ~/.zshrc
# Current working directory
: ${AGNOSTER_DIR_FG:=#1e222a}
: ${AGNOSTER_DIR_BG:=#b39cd0}
# user@host
: ${AGNOSTER_CONTEXT_FG:=#1e222a}
: ${AGNOSTER_CONTEXT_BG:=#b39cd0}
# Git related
: ${AGNOSTER_GIT_CLEAN_FG:=#1e222a}
: ${AGNOSTER_GIT_CLEAN_BG:=#7ec8c0}
: ${AGNOSTER_GIT_DIRTY_FG:=#1e222a}
: ${AGNOSTER_GIT_DIRTY_BG:=#d4c87a}
# Bazaar related
: ${AGNOSTER_BZR_CLEAN_FG:=#1e222a}
: ${AGNOSTER_BZR_CLEAN_BG:=#7ec8c0}
: ${AGNOSTER_BZR_DIRTY_FG:=#1e222a}
: ${AGNOSTER_BZR_DIRTY_BG:=#d4c87a}
# Mercurial related
: ${AGNOSTER_HG_NEWFILE_FG:=#1e222a}
: ${AGNOSTER_HG_NEWFILE_BG:=#d47373}
: ${AGNOSTER_HG_CHANGED_FG:=#1e222a}
: ${AGNOSTER_HG_CHANGED_BG:=#d4c87a}
: ${AGNOSTER_HG_CLEAN_FG:=#1e222a}
: ${AGNOSTER_HG_CLEAN_BG:=#7ec8c0}
# VirtualEnv colors
: ${AGNOSTER_VENV_FG:=#1e222a}
: ${AGNOSTER_VENV_BG:=#c8b8e0}
# AWS Profile colors
: ${AGNOSTER_AWS_PROD_FG:=#d47373}
: ${AGNOSTER_AWS_PROD_BG:=#6d484d}
: ${AGNOSTER_AWS_FG:=#1e222a}
: ${AGNOSTER_AWS_BG:=#7ec8c0}
# Status symbols
: ${AGNOSTER_STATUS_RETVAL_FG:=#d47373}
: ${AGNOSTER_STATUS_ROOT_FG:=#d4c87a}
: ${AGNOSTER_STATUS_JOB_FG:=#7ec8c0}
: ${AGNOSTER_STATUS_FG:=$CURRENT_DEFAULT_FG}
: ${AGNOSTER_STATUS_BG:=#22272e}
# Terraform colors
: ${AGNOSTER_TERRAFORM_FG:=#1e222a}
: ${AGNOSTER_TERRAFORM_BG:=#a088c0}
## Non-Color settings - set to 'true' to enable
# Show the actual numeric return value rather than a cross symbol.
: ${AGNOSTER_STATUS_RETVAL_NUMERIC:=false}
# Show git working dir in the style "/git/root   master  relative/dir" instead of "/git/root/relative/dir   master"
: ${AGNOSTER_GIT_INLINE:=false}
# Show the git branch status in the prompt rather than the generic branch symbol
: ${AGNOSTER_GIT_BRANCH_STATUS:=true}
# Special Powerline characters
() {
local LC_ALL="" LC_CTYPE="en_US.UTF-8"
# NOTE: This segment separator character is correct. In 2012, Powerline changed
# the code points they use for their special characters. This is the new code point.
# If this is not working for you, you probably have an old version of the
# Powerline-patched fonts installed. Download and install the new version.
# Do not submit PRs to change this unless you have reviewed the Powerline code point
# history and have new information.
# This is defined using a Unicode escape sequence so it is unambiguously readable, regardless of
# what font the user is viewing this source code in. Do not replace the
# escape sequence with a single literal character.
# Do not change this! Do not make it '\u2b80'; that is the old, wrong code point.
SEGMENT_SEPARATOR=$'\ue0b0'
}
# Begin a segment
# Takes two arguments, background and foreground. Both can be omitted,
# rendering default background/foreground.
prompt_segment() {
local bg fg
[[ -n $1 ]] && bg="%K{$1}" || bg="%k"
[[ -n $2 ]] && fg="%F{$2}" || fg="%f"
if [[ $CURRENT_BG != 'NONE' && $1 != $CURRENT_BG ]]; then
echo -n " %{$bg%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR%{$fg%} "
else
echo -n "%{$bg%}%{$fg%} "
fi
CURRENT_BG=$1
[[ -n $3 ]] && echo -n $3
}
# End the prompt, closing any open segments
prompt_end() {
if [[ -n $CURRENT_BG ]]; then
echo -n " %{%k%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR"
else
echo -n "%{%k%}"
fi
echo -n "%{%f%}"
CURRENT_BG=''
}
git_toplevel() {
local repo_root=$(git rev-parse --show-toplevel)
if [[ $repo_root = '' ]]; then
# We are in a bare repo. Use git dir as root
repo_root=$(git rev-parse --git-dir)
if [[ $repo_root = '.' ]]; then
repo_root=$PWD
fi
fi
echo -n $repo_root
}
### Prompt components
# Each component will draw itself, and hide itself if no information needs to be shown
# Context: user@hostname (who am I and where am I)
prompt_context() {
if [[ "$USERNAME" != "$DEFAULT_USER" || -n "$SSH_CLIENT" ]]; then
prompt_segment "$AGNOSTER_CONTEXT_BG" "$AGNOSTER_CONTEXT_FG" "%(!.%{%F{$AGNOSTER_STATUS_ROOT_FG}%}.)%n@%m"
fi
}
prompt_git_relative() {
local repo_root=$(git_toplevel)
local path_in_repo=$(pwd | sed "s/^$(echo "$repo_root" | sed 's:/:\\/:g;s/\$/\\$/g')//;s:^/::;s:/$::;")
if [[ $path_in_repo != '' ]]; then
prompt_segment "$AGNOSTER_DIR_BG" "$AGNOSTER_DIR_FG" "$path_in_repo"
fi;
}
# Git: branch/detached head, dirty status
prompt_git() {
(( $+commands[git] )) || return
if [[ "$(command git config --get oh-my-zsh.hide-status 2>/dev/null)" = 1 ]]; then
return
fi
local PL_BRANCH_CHAR
() {
local LC_ALL="" LC_CTYPE="en_US.UTF-8"
PL_BRANCH_CHAR=$'\ue0a0' # 
}
local ref dirty mode repo_path
if [[ "$(command git rev-parse --is-inside-work-tree 2>/dev/null)" = "true" ]]; then
repo_path=$(command git rev-parse --git-dir 2>/dev/null)
dirty=$(parse_git_dirty)
ref=$(command git symbolic-ref HEAD 2> /dev/null) || \
ref="◈ $(command git describe --exact-match --tags HEAD 2> /dev/null)" || \
ref="➦ $(command git rev-parse --short HEAD 2> /dev/null)"
if [[ -n $dirty ]]; then
prompt_segment "$AGNOSTER_GIT_DIRTY_BG" "$AGNOSTER_GIT_DIRTY_FG"
else
prompt_segment "$AGNOSTER_GIT_CLEAN_BG" "$AGNOSTER_GIT_CLEAN_FG"
fi
if [[ $AGNOSTER_GIT_BRANCH_STATUS == 'true' ]]; then
local ahead behind
ahead=$(command git log --oneline @{upstream}.. 2>/dev/null)
behind=$(command git log --oneline ..@{upstream} 2>/dev/null)
if [[ -n "$ahead" ]] && [[ -n "$behind" ]]; then
PL_BRANCH_CHAR=$'\u21c5'
elif [[ -n "$ahead" ]]; then
PL_BRANCH_CHAR=$'\u21b1'
elif [[ -n "$behind" ]]; then
PL_BRANCH_CHAR=$'\u21b0'
fi
fi
if [[ -e "${repo_path}/BISECT_LOG" ]]; then
mode=" <B>"
elif [[ -e "${repo_path}/MERGE_HEAD" ]]; then
mode=" >M<"
elif [[ -e "${repo_path}/rebase" || -e "${repo_path}/rebase-apply" || -e "${repo_path}/rebase-merge" || -e "${repo_path}/../.dotest" ]]; then
mode=" >R>"
fi
setopt promptsubst
autoload -Uz vcs_info
zstyle ':vcs_info:*' enable git
zstyle ':vcs_info:*' get-revision true
zstyle ':vcs_info:*' check-for-changes true
zstyle ':vcs_info:*' stagedstr '✚'
zstyle ':vcs_info:*' unstagedstr '±'
zstyle ':vcs_info:*' formats ' %u%c'
zstyle ':vcs_info:*' actionformats ' %u%c'
vcs_info
echo -n "${${ref:gs/%/%%}/refs\/heads\//$PL_BRANCH_CHAR }${vcs_info_msg_0_%% }${mode}"
[[ $AGNOSTER_GIT_INLINE == 'true' ]] && prompt_git_relative
fi
}
prompt_bzr() {
(( $+commands[bzr] )) || return
# Test if bzr repository in directory hierarchy
local dir="$PWD"
while [[ ! -d "$dir/.bzr" ]]; do
[[ "$dir" = "/" ]] && return
dir="${dir:h}"
done
local bzr_status status_mod status_all revision
if bzr_status=$(command bzr status 2>&1); then
status_mod=$(echo -n "$bzr_status" | head -n1 | grep "modified" | wc -m)
status_all=$(echo -n "$bzr_status" | head -n1 | wc -m)
revision=${$(command bzr log -r-1 --log-format line | cut -d: -f1):gs/%/%%}
if [[ $status_mod -gt 0 ]] ; then
prompt_segment "$AGNOSTER_BZR_DIRTY_BG" "$AGNOSTER_BZR_DIRTY_FG" "bzr@$revision ✚"
else
if [[ $status_all -gt 0 ]] ; then
prompt_segment "$AGNOSTER_BZR_DIRTY_BG" "$AGNOSTER_BZR_DIRTY_FG" "bzr@$revision"
else
prompt_segment "$AGNOSTER_BZR_CLEAN_BG" "$AGNOSTER_BZR_CLEAN_FG" "bzr@$revision"
fi
fi
fi
}
prompt_hg() {
(( $+commands[hg] )) || return
local rev st branch
if $(command hg id >/dev/null 2>&1); then
if $(command hg prompt >/dev/null 2>&1); then
if [[ $(command hg prompt "{status|unknown}") = "?" ]]; then
# if files are not added
prompt_segment "$AGNOSTER_HG_NEWFILE_BG" "$AGNOSTER_HG_NEWFILE_FG"
st='±'
elif [[ -n $(command hg prompt "{status|modified}") ]]; then
# if any modification
prompt_segment "$AGNOSTER_HG_CHANGED_BG" "$AGNOSTER_HG_CHANGED_FG"
st='±'
else
# if working copy is clean
prompt_segment "$AGNOSTER_HG_CLEAN_BG" "$AGNOSTER_HG_CLEAN_FG"
fi
echo -n ${$(command hg prompt "☿ {rev}@{branch}"):gs/%/%%} $st
else
st=""
rev=$(command hg id -n 2>/dev/null | sed 's/[^-0-9]//g')
branch=$(command hg id -b 2>/dev/null)
if command hg st | command grep -q "^\?"; then
prompt_segment "$AGNOSTER_HG_NEWFILE_BG" "$AGNOSTER_HG_NEWFILE_FG"
st='±'
elif command hg st | command grep -q "^[MA]"; then
prompt_segment "$AGNOSTER_HG_CHANGED_BG" "$AGNOSTER_HG_CHANGED_FG"
st='±'
else
prompt_segment "$AGNOSTER_HG_CLEAN_BG" "$AGNOSTER_HG_CLEAN_FG"
fi
echo -n "☿ ${rev:gs/%/%%}@${branch:gs/%/%%}" $st
fi
fi
}
# Dir: current working directory
prompt_dir() {
if [[ $AGNOSTER_GIT_INLINE == 'true' ]] && $(git rev-parse --is-inside-work-tree >/dev/null 2>&1); then
# Git repo and inline path enabled, hence only show the git root
prompt_segment "$AGNOSTER_DIR_BG" "$AGNOSTER_DIR_FG" "$(git_toplevel | sed "s:^$HOME:~:")"
else
prompt_segment "$AGNOSTER_DIR_BG" "$AGNOSTER_DIR_FG" '%~'
fi
}
# Virtualenv: current working virtualenv
prompt_virtualenv() {
if [ -n "$CONDA_DEFAULT_ENV" ]; then
prompt_segment "$AGNOSTER_VENV_BG" "$CURRENT_FG" "🐍 $CONDA_DEFAULT_ENV"
fi
if [[ -n "$VIRTUAL_ENV" && -n "$VIRTUAL_ENV_DISABLE_PROMPT" ]]; then
prompt_segment "$AGNOSTER_VENV_BG" "$AGNOSTER_VENV_FG" "(${VIRTUAL_ENV:t:gs/%/%%})"
fi
}
# Status:
# - was there an error
# - am I root
# - are there background jobs?
prompt_status() {
local -a symbols
if [[ $AGNOSTER_STATUS_RETVAL_NUMERIC == 'true' ]]; then
[[ $RETVAL -ne 0 ]] && symbols+="%{%F{$AGNOSTER_STATUS_RETVAL_FG}%}$RETVAL"
else
[[ $RETVAL -ne 0 ]] && symbols+="%{%F{$AGNOSTER_STATUS_RETVAL_FG}%}✘"
fi
[[ $UID -eq 0 ]] && symbols+="%{%F{$AGNOSTER_STATUS_ROOT_FG}%}⚡"
[[ $(jobs -l | wc -l) -gt 0 ]] && symbols+="%{%F{$AGNOSTER_STATUS_JOB_FG}%}⚙"
[[ -n "$symbols" ]] && prompt_segment "$AGNOSTER_STATUS_BG" "$AGNOSTER_STATUS_FG" "$symbols"
}
#AWS Profile:
# - display current AWS_PROFILE name
# - displays yellow on red if profile name contains 'production' or
# ends in '-prod'
# - displays black on green otherwise
prompt_aws() {
[[ -z "$AWS_PROFILE" || "$SHOW_AWS_PROMPT" = false ]] && return
case "$AWS_PROFILE" in
*-prod|*production*) prompt_segment "$AGNOSTER_AWS_PROD_BG" "$AGNOSTER_AWS_PROD_FG" "AWS: ${AWS_PROFILE:gs/%/%%}" ;;
*) prompt_segment "$AGNOSTER_AWS_BG" "$AGNOSTER_AWS_FG" "AWS: ${AWS_PROFILE:gs/%/%%}" ;;
esac
}
prompt_terraform() {
local terraform_info=$(tf_prompt_info)
[[ -z "$terraform_info" ]] && return
prompt_segment "$AGNOSTER_TERRAFORM_BG" "$AGNOSTER_TERRAFORM_FG" "TF: $terraform_info"
}
## Main prompt
build_prompt() {
RETVAL=$?
prompt_status
prompt_virtualenv
prompt_aws
prompt_terraform
prompt_context
prompt_dir
prompt_git
prompt_bzr
prompt_hg
prompt_end
}
PROMPT='%{%f%b%k%}$(build_prompt) '
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDEKuw3rZhDPMlgkpLymHk0jm4VolIEV7scAMvBFYgQ1VfUKw3RsydCHpUphQWqsOMTxQB69dNrlErA+e7YCsMLyPWV6I2hr4SyLdCpqvZVQBiMuAyLhxMfahhTdXv5CVwj0iYeHtde4NTMuYmRCkJCzxnCbk57P07kkElExDZNIfHjfeCoZEYCJj8cl//zShalCsoAgRQNwiPzZJoWHL1qkJ326rkbHHEPUHe2+UGraFfVR3IhD671gEUoV46aMxePgSUkyuZY/uImobhlzkYxG+721Decx2XwFoW3AUvHeQWyG9WdKs1cmM296bq6A5OKWMiUB6/xpuSqtMs0JtzmJAAzpNC1LWCbKiKMsKU4OVlMfOW+K9KWDCRHs1wtFAHpWt5hAA3G09Cp8MhehjvH2Dbyv4jtdxP9WHHI+Q8J3MCPjw7PZS/fbLQz69FwD/gUsuFcmnN3T1jXu/kknAZekk6lE2kCpXNl0eop2LVCcgpoOOsZfOdfKm5iEUeMUeM= sfree@zenbook
+1
View File
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDQK4yE5QzJuZIjykXZy9dusg+enKt5XxQGiRGzgzVw sfree@zenbook
+36
View File
@@ -0,0 +1,36 @@
Host bit_bastion
HostName ssh.steva.nz
User stevan
IdentityFile ~/.ssh/stevan@freeborn.cloud
Host ftp.stevanfreeborn.com
HostName ftp.stevanfreeborn.com
User u74298126
IdentityFile ~/.ssh/ftp_stevanfreeborn_com
Host tangled.org
Hostname tangled.org
User git
IdentityFile ~/.ssh/tangled
AddressFamily inet
Host macbookair
HostName ssh.macbookair.freeborn.cloud
User stevan
IdentityFile ~/.ssh/macbookair
Host macbookpro
HostName ssh.macbookpro.freeborn.cloud
User stevan
IdentityFile ~/.ssh/macbookpro
Host tinker
HostName ssh.freeborn.cloud
User stevan
IdentityFile ~/.ssh/zenbook_tinker
Host gitea.freeborn.cloud
HostName ssh.freeborn.cloud
User git
Port 2222
IdentityFile ~/.ssh/gitea.freeborn.cloud
+1
View File
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCehUFa1MnGWZJ4vgEnF+KYm19Yu91s3pP6V977rWyuZgl0CbQlnGBQOctGf/jFWTIjAxsT5BpyFwcN7uGQIT76HCw5T8/Y9Kl+e7uXSLVTiHCIWyn+JD/ofUY3z0sTdZWKgJtdIeC3Zuh3aRwAupqTKunT+sX5w0t2jxC0wsOgxz5cBZ1toe9IUc9vUcU+A3C8TNaKFZ8obbRQFll0fgfEDjjC42kZJvCXxXKaTr9wT9+xeaha4FR3vFmrL/Kk6FckX6U4r8PJ0TJo/QpTCJRAqjgUxCRLBU2Z1GcJIzkI0cIRGC3tX8NmmMLTwsA3HX8UJNNDjC01AXHZmnFOTzzvW0gkkjMOaAgPzxNuXXrUVwdfb/gkBCgWBTb9cpqU6ZyRd7N2LfNtdi1+FgBAzXqJ0NR02dxg7gOEcvzJ3S9YgvNPO4uOnnOSO3zArZ/IO48WULstBg2p8Xn/yFK+9b7iVfe0rU019UYbFUew9Teh+HgserZBKjr3xCl5gpXVhQU= sfree@zenbook
+1
View File
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIpFfb6vpsQqFj1veGt9Ba+xal3kxOJLK6T8bTpxuScJ zenbook
+1
View File
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKHAhEDeA8KFO99TPWwItsatAon/wyigMomD/B/cm/5F
+1
View File
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBdLKCSaXgVG5MBoCv/t0x0HAreKHqvKZR+X+5mwLREl sfree@zenbook
+1
View File
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL/iwe93wHsN1EkLNoW+YLuoW5m/lgx3L/PQ8mE9MD01 sfree@zenbook
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDGU7aDmqaRNn3YqsT3CbWtThBhQYiH+nk1hS9AGKCMFEspQbbnZkP06+s2IrQjOI1wspoHXDRJ4TEOr+RrV3/2i19pGKdJj/j+URpL7hAtLojvXa0zHyZWTN6VJdFe2pl8HuCHlv1DaqpuW32zcXZ55Pcn9igzK08wDROYNv6j2pUNo2DDo0D9SNMRQm5Vnk1Wb+xD+f8xu8kvpZzzkT01s4Wdl+1ozvD+AwPAmc/WQm5QS90GntxKEfIh/Pn+K+D+3+vOUDHdbJXDTw3C6jkp2ztwR20OK5Y1N9+lGUMtGUYZiPJ2/qrwiRV+FGfYeJjKThs2DMOeguIFyno9FON4aMcu8MeuJHm3ubHtHk/Zqx9dL2hz4mYOa6hrFZLoIr5TDJ7+2pFfsTi4BaCUpUiC/zrl2c2feban0p2x5RaSR3g+5rAOTcLWxYXmkRdyY9/4qA+UHOO0UfHJ3uCMvrxsHa49mSed/PWu3Pdo64yJ9/tQCcykNZrsyPQ8i11G2Yk= sfree@zenbook
+1
View File
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDceJHNEFsgSTw+uaoM/mK6+k/D8nen9rBLO0D8z8r0EttZLuvu39j0YNAKUrSY+Frd09QbzRt0k5t6YSgzRwcMVPMhBsyS+Z3FsdunthxMV++L9SgbfQDkSaZU7Zl1F3+Dh/z63KtHhi3yYOPUHmlgXTeXYW3PorKK3QnEQi9Szzc93GIP/A6Z3Ob7J7HzUTVLF6KQpLEhstGPxYfaQwGy/bIdYHC3gDji3s5inawMs1pRuIC3OjDPtqR0pi0Tgy40ArorXgbpCO+J9VHW9Gb3Ccy4K6rsQYdlLE/brug43WnxBYw67F3M4CPiNWLOIxgQYuG3p7OjwZWAs9RdokoQ6g4gObh8n9F4h8AcG6uk/TZcN5hmrJFMO9QqUb7E0e47t4NGhXd/lF5lX12pQV7JthZNYDkcBYvuEqO3cwzS8kNqe18GhUhbvGh+YMj55Z2RAkkciV89ol4hZEbq+3pgZ0Ihafo2FnRWPhl1hUAibcXKc263uQdMJtVsmRaWwV8= sfree@zenbook
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCw+fT6kt+toR8A4/HaWlVt7atIZRZ/LDGBflmc0kdlHhtyPamQVJvPO7OsCHd6ugjOU/6yASSF9Sv8ekPyapNJt9dnY4WBzZqQUSuJinM89Mn3RS23ABEL8yL3bbWpIPKT1x2OOF+nRSy7TuYnhu3ivgi+ZAqUTnAuDjy5/kpjttRRZ+ls2oFAdpQF2rZ2W+o2pBBKR5/YM3mQQcGQtw9pm3ddyQWJKC1AhGZeSZodhxpRoe++3fRVvLJ/P2yc7PmF0h/mb4o5RWH72jmFiSsEYA3j0uKC7XH9Dyc+XBmuQJJ4e65iP7RAFTxKOEGjf876l5Y6rmpylZkAP5202guW6axATKviggGifvHMKT4bC5lbSE92BICDhNUr3Y7Gqs3ByY9DwKITLWRJarfXV6Z+HZpmhVtpzNtWlJmLNV1LcFJTEmqm/Ji30wxJqCBkywhgcNbF9kH5BIQLMgoviqwbqo5/bJAECAmG4wnJacnv7EKXojD33t+PqxvxzsWyeVs= sfree@zenbook
+1
View File
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF0eN17W/QMNsFuUi75TFVmUruRBQi8YNTBnHB/8O6uB stevan@freeborn.cloud
+1
View File
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOBdBCvBM5GJ6WtK2+zReY69eZYh6dJ1cyyvPDbEluOI sfree@zenbook
+1
View File
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAqUFm4vgLbp4w78BT+mV6w2YGWNmg486RTyJH4VoDXA sfree@zenbook
+1
View File
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAEXqH6rDhDp5b+NK1XbHE5qIeLESnQF4J5VANZOJsrZ sfree@zenbook
+1
View File
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDLxT0ztofpvs5vUxXpkx5QEtl2DiqYshKvlwEZnd75Vx4co1Hgut7Mi9Ye2NcsNg28tuNzqAFIea6dCnca1j8AxR5nbgshLRnsg9FgqOLc6jBt6u12MfCOYegsjkTo0X2U2Jn4p69LwMY+ehaXQQhJEsggyotzMqFKTgU3OYJlSACYIPKzv8VSss7+WYfJGBlz2KWskQP0I+EfbrhFbSOTc9V2pIe12Y90XmB0SLPOk8Bp1QD1D4JHWYX6NpdfCm9LlRBucRy9u/rRIrGVs8SpJ30PJysci0oTFQhN/vpoaPWtvOBtMrB2BM+5iWBHLaiKglYNrp6ZDL1AY5Qxc3aHGq8JuYy+QOTFw/+i5Q1ClXKbm0WrjtzOYaPqsfe/F6IzQQ6ApjLmh0MRGCcL3KlpYz6vhMWjj50kTrncw1q4OHv2KjnhW2Zq8+2u/t/NY9mZCBwrExzN6mHi8kI6xvhKKt4UWU31fwu55H68IYJktIjMMJvwyuRSmt93TomGtnc= sfree@zenbook
+6
View File
@@ -0,0 +1,6 @@
[wsl2]
memory=4GB
processors=2
[experimental]
autoMemoryReclaim=dropcache
+15
View File
@@ -0,0 +1,15 @@
# ~/.zshenv — managed by chezmoi
# Loaded for ALL zsh sessions (login, interactive, scripts).
# Keep this minimal — only environment variable exports that must be available everywhere.
# Go
[ -d "/usr/local/go/bin" ] && export PATH="$PATH:/usr/local/go/bin"
# Local bin
export PATH="$HOME/.local/bin:$PATH"
# Rust / cargo
[ -f "$HOME/.cargo/env" ] && source "$HOME/.cargo/env"
# .NET
[ -d "$HOME/.dotnet" ] && export DOTNET_ROOT="$HOME/.dotnet" && export PATH="$PATH:$HOME/.dotnet"
+69
View File
@@ -0,0 +1,69 @@
# ~/.zshrc — managed by chezmoi
# Linux (Ubuntu/Debian) shell configuration — Oh My Zsh
# --- Oh My Zsh ---
export ZSH="$HOME/.oh-my-zsh"
ZSH_THEME="lavender-dimmed"
plugins=(
git
zsh-autosuggestions
zsh-syntax-highlighting
virtualenv
)
ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE="fg=#b39cd0"
ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE="20"
ZSH_AUTOSUGGEST_USE_ASYNC=1
[ -f "$ZSH/oh-my-zsh.sh" ] && source "$ZSH/oh-my-zsh.sh"
# --- zoxide (smart cd) ---
if command -v zoxide &>/dev/null; then
eval "$(zoxide init zsh)"
alias cd='z'
fi
# --- NVM ---
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && source "$NVM_DIR/bash_completion"
# --- envman ---
[ -s "$HOME/.config/envman/load.sh" ] && source "$HOME/.config/envman/load.sh"
# --- History ---
HISTFILE=~/.zsh_history
HISTSIZE=10000
SAVEHIST=10000
setopt HIST_IGNORE_DUPS
setopt HIST_FIND_NO_DUPS
setopt SHARE_HISTORY
# --- Key bindings ---
bindkey '^[[A' history-search-backward
bindkey '^[[B' history-search-forward
# --- Aliases ---
alias grep='grep --color=auto'
alias ls='ls --color=auto'
alias ll='ls -lah'
alias la='ls -A'
alias l='ls -CF'
alias catc='xclip -selection clipboard <'
# Git aliases (mirrors PowerShell profile)
alias acp='function _acp() { git add . && git commit -am "$1" && git push; }; _acp'
alias acnvp='function _acnvp() { git add . && git commit --no-verify -am "$1" && git push; }; _acnvp'
# Neovim
alias nvimconfig='cd $HOME/.config/nvim'
alias editnvim='cd $HOME/.config/nvim && nvim'
# Utilities
alias fromjson='python3 -m json.tool'
# --- Python ---
export VIRTUAL_ENV_DISABLE_PROMPT=1
export PYTHONIOENCODING="utf-8"
+29
View File
@@ -0,0 +1,29 @@
# Linux apt packages
# These are installed via: sudo apt-get install -y $(cat linux.txt | grep -v '^#' | tr '\n' ' ')
# Core utilities
build-essential
curl
wget
git
jq
unzip
zip
# Shell
zsh
xclip
# Modern CLI tools (apt versions)
fd-find
ripgrep
zoxide
fzf
# Development
sqlite3
make
python3
# Networking
openssh-client
+64
View File
@@ -0,0 +1,64 @@
{
"$schema": "https://aka.ms/winget-packages.schema.2.0.json",
"CreationDate": "2026-07-07",
"Sources": [
{
"Packages": [
{ "PackageIdentifier": "Audacity.Audacity" },
{ "PackageIdentifier": "Balena.Etcher" },
{ "PackageIdentifier": "Bitwarden.Bitwarden" },
{ "PackageIdentifier": "Bitwarden.CLI" },
{ "PackageIdentifier": "Clement.bottom" },
{ "PackageIdentifier": "DBBrowserForSQLite.DBBrowserForSQLite" },
{ "PackageIdentifier": "Docker.DockerDesktop" },
{ "PackageIdentifier": "Doppler.doppler" },
{ "PackageIdentifier": "ezwinports.make" },
{ "PackageIdentifier": "sharkdp.fd" },
{ "PackageIdentifier": "Gyan.FFmpeg" },
{ "PackageIdentifier": "GIMP.GIMP.3" },
{ "PackageIdentifier": "Git.Git" },
{ "PackageIdentifier": "GitHub.cli" },
{ "PackageIdentifier": "GoLang.Go" },
{ "PackageIdentifier": "goreleaser.goreleaser" },
{ "PackageIdentifier": "JetBrains.dotPeek" },
{ "PackageIdentifier": "JesseDuffield.lazygit" },
{ "PackageIdentifier": "JanDeDobbeleer.OhMyPosh" },
{ "PackageIdentifier": "twpayne.chezmoi" },
{ "PackageIdentifier": "Microsoft.DotNet.SDK.8" },
{ "PackageIdentifier": "Microsoft.DotNet.SDK.10" },
{ "PackageIdentifier": "Microsoft.OpenJDK.11" },
{ "PackageIdentifier": "Microsoft.PowerShell" },
{ "PackageIdentifier": "Microsoft.PowerToys" },
{ "PackageIdentifier": "Microsoft.VisualStudio.2022.BuildTools" },
{ "PackageIdentifier": "Microsoft.VisualStudioCode" },
{ "PackageIdentifier": "Microsoft.WindowsTerminal" },
{ "PackageIdentifier": "Microsoft.WSL" },
{ "PackageIdentifier": "MongoDB.Compass.Full" },
{ "PackageIdentifier": "Neovim.Neovim" },
{ "PackageIdentifier": "CoreyButler.NVMforWindows" },
{ "PackageIdentifier": "OBSProject.OBSStudio" },
{ "PackageIdentifier": "Obsidian.Obsidian" },
{ "PackageIdentifier": "Ollama.Ollama" },
{ "PackageIdentifier": "Postman.Postman" },
{ "PackageIdentifier": "astral-sh.uv" },
{ "PackageIdentifier": "BurntSushi.ripgrep.MSVC" },
{ "PackageIdentifier": "Rustlang.Rustup" },
{ "PackageIdentifier": "SlackTechnologies.Slack" },
{ "PackageIdentifier": "SQLite.SQLite" },
{ "PackageIdentifier": "Tailscale.Tailscale" },
{ "PackageIdentifier": "VideoLAN.VLC" },
{ "PackageIdentifier": "JetBrains.WebStorm" },
{ "PackageIdentifier": "WinSCP.WinSCP" },
{ "PackageIdentifier": "ajeetdsouza.zoxide" },
{ "PackageIdentifier": "Canonical.Ubuntu" }
],
"SourceDetails": {
"Argument": "https://cdn.winget.microsoft.com/cache",
"Identifier": "Microsoft.Winget.Source_8wekyb3d8bbwe",
"Name": "winget",
"Type": "Microsoft.PreIndexed.Package"
}
}
],
"WinGetVersion": "1.0.0"
}
+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