Safe to re-run: updates SSH key and config if user exists, creates new user if not. Matches NixOS declarative model. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
239 lines
7 KiB
Bash
Executable file
239 lines
7 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# dev-add.sh - Add or update a dev user account
|
|
# Usage: dev-add.sh <username> <ssh-pubkey>
|
|
#
|
|
# Idempotent: safe to re-run. Updates SSH key and config if user exists.
|
|
#
|
|
# Creates/updates:
|
|
# - Unix user account with SSH key
|
|
# - Adds to devs group (Slack token access)
|
|
# - Outputs onboarding instructions
|
|
|
|
set -euo pipefail
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m' # No Color
|
|
|
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
|
|
|
|
usage() {
|
|
echo "Usage: $0 <username> <ssh-pubkey>"
|
|
echo ""
|
|
echo "Arguments:"
|
|
echo " username - Dev's username (alphanumeric, 3-16 chars)"
|
|
echo " ssh-pubkey - SSH public key (starts with ssh-ed25519, ssh-rsa, etc.)"
|
|
echo ""
|
|
echo "Example:"
|
|
echo " $0 alice 'ssh-ed25519 AAAA... alice@laptop'"
|
|
exit 1
|
|
}
|
|
|
|
validate_username() {
|
|
local username="$1"
|
|
if [[ ! "$username" =~ ^[a-z][a-z0-9_-]{2,15}$ ]]; then
|
|
log_error "Invalid username: must be 3-16 chars, start with letter, alphanumeric/underscore/dash only"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
validate_ssh_key() {
|
|
local key="$1"
|
|
if [[ ! "$key" =~ ^ssh-(ed25519|rsa|ecdsa) ]]; then
|
|
log_error "Invalid SSH key: must start with ssh-ed25519, ssh-rsa, or ssh-ecdsa"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
create_user() {
|
|
local username="$1"
|
|
local ssh_key="$2"
|
|
local user_exists=false
|
|
|
|
# Check if user already exists
|
|
if id "$username" &>/dev/null; then
|
|
user_exists=true
|
|
log_info "Updating existing user '$username'..."
|
|
else
|
|
log_info "Creating user '$username'..."
|
|
# Create user with home directory
|
|
# NixOS: don't specify shell (uses default), group is 'users'
|
|
useradd -m -g users "$username"
|
|
fi
|
|
|
|
# Make home directory private (not world-readable)
|
|
chmod 700 "/home/$username"
|
|
|
|
# Add to devs group for Slack token access
|
|
if ! groups "$username" | grep -q '\bdevs\b'; then
|
|
usermod -aG devs "$username"
|
|
log_info "Added to devs group"
|
|
fi
|
|
|
|
# Set up SSH key
|
|
local ssh_dir="/home/$username/.ssh"
|
|
mkdir -p "$ssh_dir"
|
|
echo "$ssh_key" > "$ssh_dir/authorized_keys"
|
|
chmod 700 "$ssh_dir"
|
|
chmod 600 "$ssh_dir/authorized_keys"
|
|
chown -R "$username:users" "$ssh_dir"
|
|
if [[ "$user_exists" == true ]]; then
|
|
log_info "Updated SSH key"
|
|
fi
|
|
|
|
# Set up user's shell config (may not exist on NixOS)
|
|
# .profile = login shells (SSH), .bashrc = interactive non-login
|
|
local profile="/home/$username/.profile"
|
|
{
|
|
echo '# bun global packages (preferred - faster installs)'
|
|
echo "export PATH=\"\$HOME/.bun/bin:\$PATH\""
|
|
echo ''
|
|
echo '# npm global packages (fallback)'
|
|
echo "export PATH=\"\$HOME/.npm-global/bin:\$PATH\""
|
|
echo ''
|
|
echo '# Slack bot development tokens (if readable)'
|
|
echo '[ -r /etc/slack-dev.env ] && source /etc/slack-dev.env'
|
|
} > "$profile"
|
|
chown "$username:users" "$profile"
|
|
|
|
# Pre-create npm global directory and configure npm prefix
|
|
local npm_global="/home/$username/.npm-global"
|
|
local npmrc="/home/$username/.npmrc"
|
|
mkdir -p "$npm_global"
|
|
echo "prefix=$npm_global" > "$npmrc"
|
|
chown "$username:users" "$npm_global" "$npmrc"
|
|
|
|
# Create AGENTS.md for AI coding assistants
|
|
cat > "/home/$username/AGENTS.md" << 'AGENTS_EOF'
|
|
# AGENTS.md - Dev Server Guide
|
|
|
|
Shared NixOS dev server. This guide helps AI coding agents work effectively here.
|
|
|
|
## Possible Right Now
|
|
|
|
**Install packages:**
|
|
```bash
|
|
bun install -g @google/gemini-cli # JS tools (fast)
|
|
nix profile install nixpkgs#go # System tools (go, rust, etc.)
|
|
uv venv && uv pip install <pkg> # Python packages
|
|
```
|
|
|
|
**Run services:**
|
|
```bash
|
|
python -m http.server 8080 # Dev servers on high ports
|
|
bun run app.js # Run JS directly
|
|
tmux / screen # Persistent sessions
|
|
```
|
|
|
|
**Available tools:** python3, uv, bun, node, npm, zig, git, vim, curl, tmux, opencode, bd
|
|
|
|
## Not Possible Right Now
|
|
|
|
| Want | Why | Workaround |
|
|
|------|-----|------------|
|
|
| sudo / root | Shared server security | Use nix profile or npm install -g |
|
|
| apt / yum | NixOS uses nix | `nix profile install nixpkgs#<pkg>` |
|
|
| Port 80/443 | Needs root | Use high port + SSH tunnel |
|
|
| Docker | Security isolation | Use nix for dependencies |
|
|
| systemd system services | Needs root | Use pm2, screen, or tmux |
|
|
|
|
## Resource Limits
|
|
|
|
Per-user limits to keep the server stable:
|
|
- **Memory**: ~1GB (50% of system)
|
|
- **Processes**: 200 max
|
|
- **Network**: 30 new connections/min
|
|
|
|
Heavy processes may be killed automatically.
|
|
|
|
## Environment
|
|
|
|
- **OS**: NixOS (not Debian/Ubuntu)
|
|
- **Shell**: bash
|
|
- **Home**: ~/ (private, 700)
|
|
- **Temp**: /tmp (fast, cleared on reboot)
|
|
AGENTS_EOF
|
|
chown "$username:users" "/home/$username/AGENTS.md"
|
|
|
|
if [[ "$user_exists" == true ]]; then
|
|
log_info "User updated"
|
|
else
|
|
log_info "User created with SSH access"
|
|
fi
|
|
}
|
|
|
|
print_onboarding() {
|
|
local username="$1"
|
|
local server_ip
|
|
# NixOS: use ip command instead of hostname -I
|
|
server_ip=$(ip -4 addr show scope global | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1)
|
|
|
|
echo ""
|
|
echo "=========================================="
|
|
echo " Dev Environment Ready: $username"
|
|
echo "=========================================="
|
|
echo ""
|
|
echo "## SSH Config (~/.ssh/config on your laptop)"
|
|
echo ""
|
|
echo " Host dev-server"
|
|
echo " HostName ${server_ip:-<server-ip>}"
|
|
echo " User $username"
|
|
echo " LocalForward 8080 127.0.0.1:8080"
|
|
echo ""
|
|
echo "## Quick Start"
|
|
echo ""
|
|
echo "1. SSH in:"
|
|
echo " ssh dev-server"
|
|
echo ""
|
|
echo "2. Install AI tools (pick one or more):"
|
|
echo " bun install -g @anthropic-ai/claude-code # Claude"
|
|
echo " bun install -g @google/gemini-cli # Gemini"
|
|
echo ""
|
|
echo "3. Authenticate and start coding:"
|
|
echo " claude # or: gemini"
|
|
echo " # Follow prompts to authenticate"
|
|
echo ""
|
|
echo "## Tools Available"
|
|
echo " System: python3, uv, git, bun, node, opencode, bd"
|
|
echo " Install more: bun install -g <package>"
|
|
echo " nix profile install nixpkgs#<package>"
|
|
echo ""
|
|
echo "=========================================="
|
|
}
|
|
|
|
main() {
|
|
if [[ $# -lt 2 ]]; then
|
|
usage
|
|
fi
|
|
|
|
local username="$1"
|
|
local ssh_key="$2"
|
|
|
|
# Must run as root
|
|
if [[ $EUID -ne 0 ]]; then
|
|
log_error "This script must be run as root"
|
|
exit 1
|
|
fi
|
|
|
|
validate_username "$username"
|
|
validate_ssh_key "$ssh_key"
|
|
|
|
# Check devs group exists (created by NixOS config)
|
|
if ! getent group devs >/dev/null; then
|
|
log_error "Required group 'devs' does not exist"
|
|
log_error "Ensure users.groups.devs = {} is in NixOS config and deployed"
|
|
exit 1
|
|
fi
|
|
|
|
create_user "$username" "$ssh_key"
|
|
print_onboarding "$username"
|
|
|
|
log_info "Dev user '$username' setup complete!"
|
|
}
|
|
|
|
main "$@"
|