We have made an emacs conf with profiles. And refactored lab tool to use deploy-rs

This commit is contained in:
Geir Okkenhaug Jerstad 2025-07-03 15:09:33 +02:00
parent 24b01ae4f0
commit bff56e4ffc
22 changed files with 1448 additions and 176 deletions

View file

@ -0,0 +1,123 @@
# SSH Deployment Strategy - Unified sma User Approach
## Overview
This document outlines the updated SSH deployment strategy for the home lab, standardizing on the `sma` user for all administrative operations and deployments.
## User Strategy
### sma User (System Administrator)
- **Purpose**: System administration, deployment, maintenance
- **SSH Key**: `id_ed25519_admin`
- **Privileges**: sudo NOPASSWD, wheel group
- **Usage**: All lab tool deployments, system maintenance
### geir User (Developer)
- **Purpose**: Development work, daily usage, git operations
- **SSH Key**: `id_ed25519_dev`
- **Privileges**: Standard user with development tools
- **Usage**: Development workflows, git operations
## Deployment Workflow
### From Any Machine (Workstation or Laptop)
1. **Both machines have sma user configured** with admin SSH key
2. **Lab tool uses sma user consistently** for all remote operations
3. **Deploy-rs uses sma user** for automated deployments with rollback
### SSH Configuration
The SSH configuration supports both direct access patterns:
```bash
# Direct Tailscale access with sma user
ssh sma@sleeper-service.tail807ea.ts.net
ssh sma@grey-area.tail807ea.ts.net
ssh sma@reverse-proxy.tail807ea.ts.net
ssh sma@little-rascal.tail807ea.ts.net
# Local sma user (for deployment from laptop to workstation)
ssh sma@localhost
```
## Lab Tool Commands
All lab commands now work consistently from both machines:
```bash
# Status checking
lab status # Works from both workstation and laptop
# Deployment (using sma user automatically)
lab deploy sleeper-service # Works from both machines
lab deploy grey-area # Works from both machines
lab deploy little-rascal # Deploy TO laptop FROM workstation
lab deploy congenital-optimist # Deploy TO workstation FROM laptop
# Deploy-rs (with automatic rollback)
lab deploy-rs sleeper-service
lab hybrid-update all
```
## Security Benefits
1. **Principle of Least Privilege**: sma user only for admin tasks
2. **Key Separation**: Admin and development keys are separate
3. **Consistent Access**: Same user across all machines for deployment
4. **Audit Trail**: Clear separation between admin and development activities
## Machine-Specific Notes
### congenital-optimist (Workstation)
- **Type**: Local deployment
- **SSH**: Uses localhost with sma user for consistency
- **Primary Use**: Development and deployment hub
### little-rascal (Laptop)
- **Type**: Remote deployment
- **SSH**: Tailscale hostname with sma user
- **Primary Use**: Mobile development and deployment
### Remote Servers (sleeper-service, grey-area, reverse-proxy)
- **Type**: Remote deployment
- **SSH**: Tailscale hostnames with sma user
- **Access**: Both workstation and laptop can deploy
## Migration Benefits
1. **Simplified Workflow**: Same commands work from both machines
2. **Better Security**: Dedicated admin user for all system operations
3. **Consistency**: All deployments use the same SSH user pattern
4. **Flexibility**: Can deploy from either workstation or laptop seamlessly
## Testing the Setup
```bash
# Test SSH connectivity with sma user
ssh sma@sleeper-service.tail807ea.ts.net echo "Connection OK"
ssh sma@grey-area.tail807ea.ts.net echo "Connection OK"
ssh sma@little-rascal.tail807ea.ts.net echo "Connection OK"
# Test lab tool
lab status # Should show all machines
lab deploy sleeper-service # Should work with sma user
# Test deploy-rs
lab deploy-rs sleeper-service --dry-run
```
## Implementation Status
- ✅ SSH keys configured for sma user on all machines
- ✅ Lab tool updated to use sma user for all operations
- ✅ Deploy-rs configuration updated to use sma user
- ✅ SSH client configuration updated with proper host patterns
- 📋 Ready for testing and validation
## Next Steps
1. Test SSH connectivity from both machines to all targets
2. Validate lab tool deployment commands
3. Test deploy-rs functionality with sma user
4. Update any remaining scripts that might use old SSH patterns

View file

@ -0,0 +1,201 @@
;;; init.el --- Nix-integrated modular Emacs configuration -*- lexical-binding: t; -*-
;;; Commentary:
;; A Nix-integrated, modular Emacs configuration that leverages Nix-provided tools
;; and packages where possible, falling back to Emacs package manager only when needed.
;; Core setup: UI, Nix integration, modular loading
;;; Code:
;; Performance optimizations
(setq gc-cons-threshold (* 50 1000 1000))
(add-hook 'emacs-startup-hook
(lambda ()
(setq gc-cons-threshold (* 2 1000 1000))
(let ((profile (getenv "EMACS_PROFILE")))
(message "Emacs loaded in %s with %d garbage collections (Profile: %s)."
(format "%.2f seconds"
(float-time
(time-subtract after-init-time before-init-time)))
gcs-done
(or profile "unknown")))))
;; Basic UI setup - minimal but pleasant
(setq inhibit-startup-screen t)
(menu-bar-mode -1)
(when (fboundp 'tool-bar-mode) (tool-bar-mode -1))
(when (fboundp 'scroll-bar-mode) (scroll-bar-mode -1))
(set-face-attribute 'default nil :height 140)
(setq-default cursor-type 'bar)
;; Nix Integration Setup
;; Configure Emacs to use Nix-provided tools when available
(defun nix-tool-path (tool-name)
"Get the path to TOOL-NAME from Nix environment variables."
(let ((env-var (concat (upcase tool-name) "_PATH")))
(getenv env-var)))
;; Configure external tools to use Nix-provided binaries
(when-let ((rg-path (nix-tool-path "rg")))
(setq consult-ripgrep-command rg-path))
(when-let ((ag-path (nix-tool-path "ag")))
(setq ag-executable ag-path))
(when-let ((fd-path (nix-tool-path "fd")))
(setq find-program fd-path))
(when-let ((sqlite-path (nix-tool-path "sqlite")))
(setq org-roam-database-connector 'sqlite3)
(setq org-roam-db-executable sqlite-path))
;; Language Server Configuration (for Nix-provided LSP servers)
(defun configure-nix-lsp-servers ()
"Configure LSP to use Nix-provided language servers."
(when (featurep 'lsp-mode)
;; Nix LSP server
(when-let ((nil-path (nix-tool-path "nil_lsp")))
(setq lsp-nix-nil-server-path nil-path))
;; Bash LSP server
(when-let ((bash-lsp-path (nix-tool-path "bash_lsp")))
(setq lsp-bash-language-server-path bash-lsp-path))
;; YAML LSP server
(when-let ((yaml-lsp-path (nix-tool-path "yaml_lsp")))
(setq lsp-yaml-language-server-path yaml-lsp-path))))
;; Configure format-all to use Nix-provided formatters
(defun configure-nix-formatters ()
"Configure format-all to use Nix-provided formatters."
(when (featurep 'format-all)
;; Shellcheck for shell scripts
(when-let ((shellcheck-path (nix-tool-path "shellcheck")))
(setq format-all-formatters
(cons `(sh (shellcheck ,shellcheck-path))
format-all-formatters)))))
;; Package management setup
;; Note: With Nix integration, we rely less on package.el
;; Most packages come pre-installed via the flake
(require 'package)
(setq package-archives
'(("melpa" . "https://melpa.org/packages/")
("gnu" . "https://elpa.gnu.org/packages/")))
;; Only initialize package.el if we're not in a Nix environment
;; In Nix environments, packages are pre-installed
(unless (getenv "EMACS_PROFILE")
(package-initialize)
;; Install use-package for non-Nix environments
(unless (package-installed-p 'use-package)
(package-refresh-contents)
(package-install 'use-package)))
;; Configure use-package for Nix integration
(require 'use-package)
;; Don't auto-install packages in Nix environment - they're pre-provided
(setq use-package-always-ensure (not (getenv "EMACS_PROFILE")))
;; Essential packages that should be available in all profiles
(use-package exec-path-from-shell
:if (memq window-system '(mac ns x))
:config
(exec-path-from-shell-initialize)
;; Ensure Nix environment is properly inherited
(exec-path-from-shell-copy-envs '("NIX_PATH" "NIX_EMACS_PROFILE")))
(use-package diminish)
(use-package bind-key)
;; Basic editing improvements
(use-package which-key
:config
(which-key-mode 1))
;; Load profile-specific configuration based on Nix profile
(defun load-profile-config ()
"Load configuration specific to the current Nix profile."
(let ((profile (getenv "EMACS_PROFILE")))
(pcase profile
("server"
(message "Loading minimal server configuration...")
;; Minimal config - only essential features
(setq gc-cons-threshold (* 2 1000 1000))) ; Lower memory usage
("laptop"
(message "Loading laptop development configuration...")
;; Laptop config - balanced features
(setq auto-save-timeout 30) ; More frequent saves
(setq lsp-idle-delay 0.3)) ; Moderate LSP responsiveness
("workstation"
(message "Loading workstation configuration...")
;; Workstation config - maximum performance
(setq gc-cons-threshold (* 50 1000 1000)) ; Higher performance
(setq lsp-idle-delay 0.1)) ; Fastest LSP response
(_
(message "Loading default configuration...")))))
;; Apply profile-specific settings
(load-profile-config)
;; Configure Nix integration after packages are loaded
(add-hook 'after-init-hook #'configure-nix-lsp-servers)
(add-hook 'after-init-hook #'configure-nix-formatters)
;; Org mode basic setup (always included)
(use-package org
:config
(setq org-startup-indented t)
(setq org-hide-emphasis-markers t))
;; Module loading system
;; Load modules based on availability and profile
(defvar my-modules-dir
(expand-file-name "modules/" user-emacs-directory)
"Directory containing modular configuration files.")
(defun load-module (module-name)
"Load MODULE-NAME from the modules directory."
(let ((module-file (expand-file-name (concat module-name ".el") my-modules-dir)))
(when (file-exists-p module-file)
(load-file module-file)
(message "Loaded module: %s" module-name))))
;; Load modules based on profile
(let ((profile (getenv "EMACS_PROFILE")))
(pcase profile
("server"
;; Minimal modules for server
(load-module "ui"))
((or "laptop" "workstation")
;; Full module set for development machines
(load-module "ui")
(load-module "completion")
(load-module "navigation")
(load-module "development")
(load-module "elisp-development")
(when (string= profile "workstation")
(load-module "claude-code")))
(_
;; Default module loading (non-Nix environment)
(load-module "ui")
(load-module "completion")
(load-module "navigation"))))
;; Display startup information
(add-hook 'emacs-startup-hook
(lambda ()
(let ((profile (getenv "EMACS_PROFILE")))
(message "=== Emacs Ready ===")
(message "Profile: %s" (or profile "default"))
(message "Nix Integration: %s" (if profile "enabled" "disabled"))
(message "Modules loaded based on profile")
(message "==================="))))
;;; init.el ends here

View file

@ -0,0 +1,126 @@
;;; claude-code.el --- Claude Code CLI integration module -*- lexical-binding: t; -*-
;;; Commentary:
;; Integration with Claude Code CLI for AI-assisted coding directly in Emacs
;; Provides terminal interface and commands for interacting with Claude AI
;;; Code:
;; Install claude-code via quelpa if not already installed
(unless (package-installed-p 'claude-code)
(quelpa '(claude-code :fetcher github :repo "stevemolitor/claude-code.el")))
;; Claude Code - AI assistant integration
(use-package claude-code
:ensure nil ; Already installed via quelpa
:bind-keymap ("C-c C-c" . claude-code-command-map)
:bind (("C-c C-c c" . claude-code)
("C-c C-c s" . claude-code-send-command)
("C-c C-c r" . claude-code-send-region)
("C-c C-c b" . claude-code-send-buffer)
("C-c C-c e" . claude-code-fix-error-at-point)
("C-c C-c t" . claude-code-toggle)
("C-c C-c k" . claude-code-kill)
("C-c C-c n" . claude-code-new))
:custom
;; Terminal backend preference (eat is now installed via quelpa)
(claude-code-terminal-type 'eat)
;; Enable desktop notifications
(claude-code-notifications t)
;; Startup delay to ensure proper initialization
(claude-code-startup-delay 1.0)
;; Confirm before killing Claude sessions
(claude-code-confirm-kill t)
;; Use modern keybinding style
(claude-code-newline-and-send-style 'modern)
:config
;; Smart terminal detection - eat should be available via quelpa
(defun claude-code-detect-best-terminal ()
"Detect the best available terminal for Claude Code."
(cond
((package-installed-p 'eat) 'eat)
((and (package-installed-p 'vterm)
(or (executable-find "cmake")
(file-exists-p "/usr/bin/cmake")
(file-exists-p "/nix/store/*/bin/cmake")))
'vterm)
(t 'eat))) ; fallback to eat, should be installed
;; Set terminal type based on detection
(setq claude-code-terminal-type (claude-code-detect-best-terminal))
;; Auto-start Claude in project root when opening coding files
(defun claude-code-auto-start-maybe ()
"Auto-start Claude Code if in a project and not already running."
(when (and (derived-mode-p 'prog-mode)
(project-current)
(not (claude-code-running-p)))
(claude-code)))
;; Optional: Auto-start when opening programming files
;; Uncomment the next line if you want this behavior
;; (add-hook 'prog-mode-hook #'claude-code-auto-start-maybe)
;; Add helpful message about Claude Code setup
(message "Claude Code module loaded. Use C-c C-c c to start Claude, C-c C-c h for help"))
;; Terminal emulator for Claude Code (eat installed via quelpa in init.el)
(use-package eat
:ensure nil ; Already installed via quelpa
:custom
(eat-term-name "xterm-256color")
(eat-kill-buffer-on-exit t))
;; Alternative terminal emulator (if eat fails or user prefers vterm)
(use-package vterm
:if (and (not (package-installed-p 'eat))
(executable-find "cmake"))
:custom
(vterm-always-compile-module t)
(vterm-kill-buffer-on-exit t)
(vterm-max-scrollback 10000))
;; Transient dependency for command menus
(use-package transient
:ensure t)
;; Enhanced error handling for Claude Code integration
(defun claude-code-send-error-context ()
"Send error at point with surrounding context to Claude."
(interactive)
(if (claude-code-running-p)
(let* ((error-line (line-number-at-pos))
(start (max 1 (- error-line 5)))
(end (min (line-number-at-pos (point-max)) (+ error-line 5)))
(context (buffer-substring-no-properties
(line-beginning-position (- start error-line))
(line-end-position (- end error-line)))))
(claude-code-send-command
(format "I'm getting an error around line %d. Here's the context:\n\n```%s\n%s\n```\n\nCan you help me fix this?"
error-line
(or (file-name-extension (buffer-file-name)) "")
context)))
(message "Claude Code is not running. Start it with C-c C-c c")))
;; Keybinding for enhanced error context
(global-set-key (kbd "C-c C-c x") #'claude-code-send-error-context)
;; Project-aware Claude instances
(defun claude-code-project-instance ()
"Start or switch to Claude instance for current project."
(interactive)
(if-let ((project (project-current)))
(let ((default-directory (project-root project)))
(claude-code))
(claude-code)))
;; Keybinding for project-specific Claude
(global-set-key (kbd "C-c C-c p") #'claude-code-project-instance)
(provide 'claude-code)
;;; claude-code.el ends here

View file

@ -0,0 +1,55 @@
;;; completion.el --- Completion framework configuration -*- lexical-binding: t; -*-
;;; Commentary:
;; Modern completion with Vertico, Consult, and Corfu
;;; Code:
;; Vertico - vertical completion UI
(use-package vertico
:init
(vertico-mode)
:custom
(vertico-cycle t))
;; Marginalia - rich annotations in minibuffer
(use-package marginalia
:init
(marginalia-mode))
;; Consult - enhanced search and navigation commands
(use-package consult
:bind (("C-s" . consult-line)
("C-x b" . consult-buffer)
("C-x 4 b" . consult-buffer-other-window)
("C-x 5 b" . consult-buffer-other-frame)
("M-y" . consult-yank-pop)
("M-g g" . consult-goto-line)
("M-g M-g" . consult-goto-line)
("C-x r b" . consult-bookmark)))
;; Orderless - flexible completion style
(use-package orderless
:custom
(completion-styles '(orderless basic))
(completion-category-defaults nil)
(completion-category-overrides '((file (styles partial-completion)))))
;; Corfu - in-buffer completion popup
(use-package corfu
:custom
(corfu-cycle t)
(corfu-auto t)
(corfu-auto-delay 0.2)
(corfu-auto-prefix 2)
:init
(global-corfu-mode))
;; Cape - completion at point extensions
(use-package cape
:init
(add-to-list 'completion-at-point-functions #'cape-dabbrev)
(add-to-list 'completion-at-point-functions #'cape-file))
(provide 'completion)
;;; completion.el ends here

View file

@ -0,0 +1,40 @@
;;; development.el --- Development tools configuration -*- lexical-binding: t; -*-
;;; Commentary:
;; LSP, Copilot, and other development tools
;;; Code:
;; LSP Mode
(use-package lsp-mode
:hook ((prog-mode . lsp-deferred))
:commands (lsp lsp-deferred)
:custom
(lsp-keymap-prefix "C-c l")
(lsp-idle-delay 0.5)
(lsp-log-io nil)
(lsp-completion-provider :none) ; Use corfu instead
:config
(lsp-enable-which-key-integration t))
;; LSP UI
(use-package lsp-ui
:after lsp-mode
:custom
(lsp-ui-doc-enable t)
(lsp-ui-doc-position 'bottom)
(lsp-ui-sideline-enable t)
(lsp-ui-sideline-show-hover nil))
;; Which Key - helpful for discovering keybindings
(use-package which-key
:config
(which-key-mode 1)
(setq which-key-idle-delay 0.3))
;; Magit - Git interface
(use-package magit
:bind ("C-x g" . magit-status))
(provide 'development)
;;; development.el ends here

View file

@ -0,0 +1,164 @@
;;; elisp-development.el --- Enhanced Emacs Lisp development setup -*- lexical-binding: t; -*-
;;; Commentary:
;; Specialized configuration for Emacs Lisp development
;; This module provides enhanced development tools specifically for .el files
;;; Code:
;; Enhanced Emacs Lisp mode with better defaults
(use-package elisp-mode
:ensure nil ; Built-in package
:mode "\\.el\\'"
:hook ((emacs-lisp-mode . eldoc-mode)
(emacs-lisp-mode . show-paren-mode)
(emacs-lisp-mode . electric-pair-mode))
:bind (:map emacs-lisp-mode-map
("C-c C-e" . eval-last-sexp)
("C-c C-b" . eval-buffer)
("C-c C-r" . eval-region)
("C-c C-d" . describe-function-at-point))
:config
;; Better indentation
(setq lisp-indent-function 'lisp-indent-function)
;; Show function signatures in minibuffer
(eldoc-mode 1))
;; Enhanced Elisp navigation
(use-package elisp-slime-nav
:hook (emacs-lisp-mode . elisp-slime-nav-mode)
:bind (:map elisp-slime-nav-mode-map
("M-." . elisp-slime-nav-find-elisp-thing-at-point)
("M-," . pop-tag-mark)))
;; Better parentheses handling
(use-package smartparens
:hook (emacs-lisp-mode . smartparens-strict-mode)
:config
(require 'smartparens-config)
(sp-local-pair 'emacs-lisp-mode "'" nil :actions nil)
(sp-local-pair 'emacs-lisp-mode "`" nil :actions nil))
;; Rainbow delimiters for better paren visibility
(use-package rainbow-delimiters
:hook (emacs-lisp-mode . rainbow-delimiters-mode))
;; Aggressive indentation
(use-package aggressive-indent
:hook (emacs-lisp-mode . aggressive-indent-mode))
;; Enhanced help and documentation
(use-package helpful
:bind (("C-h f" . helpful-callable)
("C-h v" . helpful-variable)
("C-h k" . helpful-key)
("C-h x" . helpful-command)
("C-h ." . helpful-at-point)))
;; Live examples for Elisp functions
(use-package elisp-demos
:after helpful
:config
(advice-add 'helpful-update :after #'elisp-demos-advice-helpful-update))
;; Package linting
(use-package package-lint
:commands package-lint-current-buffer)
;; Flycheck for syntax checking
(use-package flycheck
:hook (emacs-lisp-mode . flycheck-mode)
:config
;; Enhanced Emacs Lisp checking
(setq flycheck-emacs-lisp-load-path 'inherit))
;; Checkdoc for documentation linting
(use-package checkdoc
:ensure nil ; Built-in
:commands checkdoc)
;; Enhanced debugging
(use-package edebug
:ensure nil ; Built-in
:bind (:map emacs-lisp-mode-map
("C-c C-x C-d" . edebug-defun)
("C-c C-x C-b" . edebug-set-breakpoint)))
;; Package development helpers
(use-package auto-compile
:config
(auto-compile-on-load-mode)
(auto-compile-on-save-mode))
;; Enhanced REPL interaction
(use-package ielm
:ensure nil ; Built-in
:bind ("C-c C-z" . ielm)
:config
(add-hook 'ielm-mode-hook 'eldoc-mode))
;; Highlight defined functions and variables
(use-package highlight-defined
:hook (emacs-lisp-mode . highlight-defined-mode))
;; Better search and replace for symbols
(use-package expand-region
:bind ("C-=" . er/expand-region))
;; Multiple cursors for batch editing
(use-package multiple-cursors
:bind (("C-S-c C-S-c" . mc/edit-lines)
("C->" . mc/mark-next-like-this)
("C-<" . mc/mark-previous-like-this)))
;; Custom functions for Elisp development
(defun elisp-eval-and-replace ()
"Evaluate the sexp at point and replace it with its value."
(interactive)
(backward-kill-sexp)
(condition-case nil
(prin1 (eval (read (current-kill 0)))
(current-buffer))
(error (message "Invalid expression")
(insert (current-kill 0)))))
(defun elisp-describe-thing-at-point ()
"Show the documentation for the thing at point."
(interactive)
(let ((thing (symbol-at-point)))
(cond
((fboundp thing) (describe-function thing))
((boundp thing) (describe-variable thing))
(t (message "No documentation found for %s" thing)))))
;; Key bindings for custom functions
(define-key emacs-lisp-mode-map (kbd "C-c C-x C-e") 'elisp-eval-and-replace)
(define-key emacs-lisp-mode-map (kbd "C-c C-d") 'elisp-describe-thing-at-point)
;; Project-specific configurations
(defun setup-elisp-project ()
"Set up development environment for Elisp projects."
(interactive)
(when (and buffer-file-name
(string-match "\\.el\\'" buffer-file-name))
;; Add current directory to load-path for local requires
(add-to-list 'load-path (file-name-directory buffer-file-name))
;; Set up package development if this looks like a package
(when (or (file-exists-p "Cask")
(file-exists-p "Eask")
(string-match "-pkg\\.el\\'" buffer-file-name))
(message "Elisp package development mode enabled"))))
(add-hook 'emacs-lisp-mode-hook 'setup-elisp-project)
;; Better compilation output
(add-hook 'emacs-lisp-mode-hook
(lambda ()
(setq-local compile-command
(format "emacs -batch -f batch-byte-compile %s"
(shell-quote-argument buffer-file-name)))))
(provide 'elisp-development)
;;; elisp-development.el ends here

View file

@ -0,0 +1,51 @@
;;; navigation.el --- Navigation and file management -*- lexical-binding: t; -*-
;;; Commentary:
;; File navigation, project management, and window management
;;; Code:
;; Dired improvements
(use-package dired
:ensure nil
:custom
(dired-listing-switches "-alhF")
(dired-dwim-target t)
:config
(put 'dired-find-alternate-file 'disabled nil))
;; Project management
(use-package projectile
:config
(projectile-mode +1)
:bind-keymap
("C-c p" . projectile-command-map)
:custom
(projectile-completion-system 'default))
;; Treemacs - file tree
(use-package treemacs
:bind (("M-0" . treemacs-select-window)
("C-x t 1" . treemacs-delete-other-windows)
("C-x t t" . treemacs)
("C-x t d" . treemacs-select-directory)
("C-x t B" . treemacs-bookmark)
("C-x t C-t" . treemacs-find-file)
("C-x t M-t" . treemacs-find-tag))
:custom
(treemacs-width 30))
;; Ace Window - quick window switching
(use-package ace-window
:bind ("M-o" . ace-window)
:custom
(aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l)))
;; Winner mode - window configuration undo/redo
(use-package winner
:ensure nil
:config
(winner-mode 1))
(provide 'navigation)
;;; navigation.el ends here

View file

@ -0,0 +1,32 @@
;;; ui.el --- UI configuration module -*- lexical-binding: t; -*-
;;; Commentary:
;; Enhanced UI configuration - themes, modeline, icons
;;; Code:
;; Doom themes
(use-package doom-themes
:config
(load-theme 'doom-monokai-pro t)
(doom-themes-visual-bell-config)
(doom-themes-org-config))
;; Doom modeline
(use-package doom-modeline
:init (doom-modeline-mode 1)
:custom
(doom-modeline-height 15)
(doom-modeline-icon t)
(doom-modeline-buffer-file-name-style 'truncate-with-project))
;; All the icons
(use-package all-the-icons
:if (display-graphic-p)
:config
;; Install fonts if not already done
(unless (find-font (font-spec :name "all-the-icons"))
(all-the-icons-install-fonts t)))
(provide 'ui)
;;; ui.el ends here

6
flake.lock generated
View file

@ -54,11 +54,11 @@
}, },
"nixpkgs-unstable": { "nixpkgs-unstable": {
"locked": { "locked": {
"lastModified": 1751011381, "lastModified": 1751271578,
"narHash": "sha256-krGXKxvkBhnrSC/kGBmg5MyupUUT5R6IBCLEzx9jhMM=", "narHash": "sha256-P/SQmKDu06x8yv7i0s8bvnnuJYkxVGBWLWHaU+tt4YY=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "30e2e2857ba47844aa71991daa6ed1fc678bcbb7", "rev": "3016b4b15d13f3089db8a41ef937b13a9e33a8df",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -261,7 +261,7 @@
profiles.system = { profiles.system = {
user = "root"; user = "root";
path = deploy-rs.lib.x86_64-linux.activate.nixos self.nixosConfigurations.little-rascal; path = deploy-rs.lib.x86_64-linux.activate.nixos self.nixosConfigurations.little-rascal;
sshUser = "geir"; sshUser = "sma";
sudo = "sudo -u"; sudo = "sudo -u";
autoRollback = true; autoRollback = true;
magicRollback = true; magicRollback = true;

View file

@ -33,6 +33,10 @@
# Development tools # Development tools
../../modules/development/tools.nix ../../modules/development/tools.nix
../../modules/development/emacs.nix
# Emacs with workstation profile
../../modules/development/emacs.nix
# AI tools # AI tools
../../modules/ai/claude-code.nix ../../modules/ai/claude-code.nix
@ -61,6 +65,14 @@
]; ];
}; };
# Emacs workstation configuration
services.emacs-profiles = {
enable = true;
profile = "workstation";
enableDaemon = true;
user = "geir";
};
# Enable clean seatd/greetd login # Enable clean seatd/greetd login
services.seatd-clean.enable = true; services.seatd-clean.enable = true;

View file

@ -16,6 +16,9 @@
../../modules/virtualization/incus.nix ../../modules/virtualization/incus.nix
../../modules/users/sma.nix ../../modules/users/sma.nix
# Development (minimal for services host)
../../modules/development/emacs.nix
# NFS client with ID mapping # NFS client with ID mapping
../../modules/services/nfs-client.nix ../../modules/services/nfs-client.nix
@ -43,6 +46,14 @@
# Disks and Updates # Disks and Updates
services.fstrim.enable = true; services.fstrim.enable = true;
# Emacs server configuration (minimal for services host)
services.emacs-profiles = {
enable = true;
profile = "server";
enableDaemon = false;
user = "sma";
};
# Mount remote filesystem # Mount remote filesystem
fileSystems."/mnt/remote/media" = { fileSystems."/mnt/remote/media" = {
device = "sleeper-service:/mnt/storage/media"; device = "sleeper-service:/mnt/storage/media";

View file

@ -15,7 +15,6 @@
../../modules/common/base.nix ../../modules/common/base.nix
../../modules/common/nix.nix ../../modules/common/nix.nix
../../modules/common/tty.nix ../../modules/common/tty.nix
../../modules/common/emacs.nix
# Desktop # Desktop
../../modules/desktop/niri.nix ../../modules/desktop/niri.nix
@ -25,6 +24,7 @@
# Development # Development
../../modules/development/tools.nix ../../modules/development/tools.nix
../../modules/development/emacs.nix
../../modules/ai/claude-code.nix ../../modules/ai/claude-code.nix
# Users # Users
@ -79,6 +79,14 @@
kernel.sysctl."vm.swappiness" = 180; kernel.sysctl."vm.swappiness" = 180;
}; };
# Emacs laptop configuration
services.emacs-profiles = {
enable = true;
profile = "laptop";
enableDaemon = true;
user = "geir";
};
# zram configuration # zram configuration
zramSwap = { zramSwap = {
enable = true; enable = true;

View file

@ -10,6 +10,9 @@
../../modules/network/extraHosts.nix ../../modules/network/extraHosts.nix
../../modules/users/sma.nix ../../modules/users/sma.nix
../../modules/security/ssh-keys.nix ../../modules/security/ssh-keys.nix
# Development (minimal for edge server)
../../modules/development/emacs.nix
]; ];
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
@ -43,6 +46,14 @@
# Tailscale for secure management access # Tailscale for secure management access
services.tailscale.enable = true; services.tailscale.enable = true;
# Emacs server configuration (minimal for edge server)
services.emacs-profiles = {
enable = true;
profile = "server";
enableDaemon = false;
user = "sma";
};
# SSH configuration - temporarily simplified for testing # SSH configuration - temporarily simplified for testing
services.openssh = { services.openssh = {
enable = true; enable = true;

View file

@ -1,4 +1,11 @@
{ config, lib, pkgs, inputs, unstable, ... }: { {
config,
lib,
pkgs,
inputs,
unstable,
...
}: {
imports = [ imports = [
./hardware-configuration.nix ./hardware-configuration.nix
# Security modules # Security modules
@ -10,6 +17,9 @@
./nfs.nix ./nfs.nix
./services/transmission.nix ./services/transmission.nix
# Development (minimal for server)
../../modules/development/emacs.nix
# User modules - server only needs sma user # User modules - server only needs sma user
../../modules/users/sma.nix ../../modules/users/sma.nix
]; ];
@ -20,25 +30,37 @@
zfsSupport = true; zfsSupport = true;
efiSupport = true; efiSupport = true;
efiInstallAsRemovable = true; efiInstallAsRemovable = true;
mirroredBoots = [ mirroredBoots = [
{ devices = [ "nodev" ]; path = "/boot"; } ]; {
devices = ["nodev"];
path = "/boot";
}
];
}; };
boot.supportedFilesystems = [ "zfs" ]; boot.supportedFilesystems = ["zfs"];
boot.loader.grub.memtest86.enable = true; boot.loader.grub.memtest86.enable = true;
# Add nomodeset for graphics compatibility # Add nomodeset for graphics compatibility
boot.kernelParams = [ "nomodeset" ]; boot.kernelParams = ["nomodeset"];
# ZFS services for file server # ZFS services for file server
services.zfs = { services.zfs = {
autoScrub.enable = true; autoScrub.enable = true;
trim.enable = true; trim.enable = true;
}; };
# Emacs server configuration (minimal)
services.emacs-profiles = {
enable = true;
profile = "server";
enableDaemon = false; # Don't run daemon on server
user = "sma";
};
# Enable ZFS auto-mounting since we're using ZFS native mountpoints # Enable ZFS auto-mounting since we're using ZFS native mountpoints
# systemd.services.zfs-mount.enable = lib.mkForce false; # systemd.services.zfs-mount.enable = lib.mkForce false;
# Disable graphics for server use - comment out NVIDIA config for now # Disable graphics for server use - comment out NVIDIA config for now
# hardware.graphics = { # hardware.graphics = {
# enable = true; # enable = true;
@ -48,15 +70,15 @@
# open = false; # open = false;
# package = config.boot.kernelPackages.nvidiaPackages.legacy_470; # package = config.boot.kernelPackages.nvidiaPackages.legacy_470;
# }; # };
# Comment out NVIDIA kernel modules for now # Comment out NVIDIA kernel modules for now
# boot.kernelModules = [ "nvidia" "nvidia_modeset" "nvidia_uvm" "nvidia_drm" ]; # boot.kernelModules = [ "nvidia" "nvidia_modeset" "nvidia_uvm" "nvidia_drm" ];
# Comment out NVIDIA utilities for now # Comment out NVIDIA utilities for now
# environment.systemPackages = with pkgs; [ # environment.systemPackages = with pkgs; [
# config.boot.kernelPackages.nvidiaPackages.legacy_470 # config.boot.kernelPackages.nvidiaPackages.legacy_470
# ]; # ];
# Create mount directories early in boot process # Create mount directories early in boot process
# systemd.tmpfiles.rules = [ # systemd.tmpfiles.rules = [
# "d /mnt/storage 0755 root root -" # "d /mnt/storage 0755 root root -"
@ -93,4 +115,4 @@
# DO NOT CHANGE - maintains data compatibility # DO NOT CHANGE - maintains data compatibility
system.stateVersion = "23.11"; system.stateVersion = "23.11";
} }

View file

@ -0,0 +1,285 @@
{
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.services.emacs-profiles;
# Emacs package configurations for different profiles
packageSets = {
# Essential packages for all profiles
essential = epkgs:
with epkgs; [
use-package
diminish
bind-key
which-key
exec-path-from-shell # Critical for integrating with Nix environment
];
# Minimal packages for server profile
minimal = epkgs:
with epkgs; [
# Basic editing
smartparens
expand-region
# Essential navigation (pure Emacs, no external deps)
vertico
consult
marginalia
orderless
# Basic modes for config files
nix-mode # Essential for Nix ecosystem
yaml-mode
markdown-mode
# Org mode essentials
org
org-roam
];
# Development packages for laptop/workstation
development = epkgs:
with epkgs; [
# Advanced navigation and completion
vertico
consult
marginalia
orderless
embark
embark-consult
corfu
cape
# Project management
projectile
magit
forge
# Development tools
lsp-mode
lsp-ui
company
flycheck
yasnippet
# Language support
nix-mode
rust-mode
python-mode
typescript-mode
json-mode
yaml-mode
markdown-mode
# Org mode and knowledge management
org
org-roam
org-roam-ui
org-agenda
# UI enhancements
doom-themes
doom-modeline
all-the-icons
rainbow-delimiters
highlight-indent-guides
# Editing enhancements
smartparens
expand-region
multiple-cursors
avy
ace-window
# Terminal integration
vterm
eshell-git-prompt
];
# Full workstation packages
workstation = epkgs:
with epkgs; [
# All development packages plus extras
claude-code # AI assistance (when available)
pdf-tools
nov # EPUB reader
elfeed # RSS reader
mu4e # Email (if configured)
dired-sidebar
treemacs
treemacs-projectile
treemacs-magit
];
};
# Generate Emacs configuration based on profile
# Uses emacs-gtk to track upstream with GTK3 support for desktop profiles
# Uses emacs-nox for server profiles (no X11/GUI dependencies)
emacsWithProfile = profile: let
# Choose Emacs package based on profile
emacsPackage =
if profile == "server"
then pkgs.emacs-nox # No GUI for servers
else pkgs.emacs-gtk; # GTK3 for desktops
# Combine package sets based on profile
selectedPackages = epkgs:
(packageSets.essential epkgs)
++ (
if profile == "server"
then packageSets.minimal epkgs
else if profile == "laptop"
then packageSets.development epkgs
else if profile == "workstation"
then (packageSets.development epkgs) ++ (packageSets.workstation epkgs)
else packageSets.minimal epkgs
);
in
pkgs.emacsWithPackagesFromUsePackage {
config = builtins.readFile ../../dotfiles/geir/emacs-config/init-nix.el;
package = emacsPackage;
extraEmacsPackages = selectedPackages;
# Provide external tools that Emacs will use
# These will be available via environment variables
override = epkgs:
epkgs
// {
# External tools for Emacs integration
external-tools =
[
pkgs.ripgrep # for fast searching
pkgs.fd # for file finding
pkgs.sqlite # for org-roam database
pkgs.ag # the silver searcher
pkgs.git # version control
pkgs.direnv # environment management
# Language servers (when available)
pkgs.nixd # Nix language server
pkgs.nodePackages.bash-language-server
pkgs.nodePackages.yaml-language-server
pkgs.marksman # Markdown language server
# Formatters
pkgs.alejandra # Nix formatter
pkgs.shellcheck # Shell script analysis
pkgs.shfmt # Shell script formatter
]
++ lib.optionals (profile != "server") [
# Additional tools for development profiles
pkgs.nodejs # for various language servers
pkgs.python3 # for Python development
pkgs.rustup # Rust toolchain
pkgs.go # Go language
];
};
};
in {
options.services.emacs-profiles = {
enable = mkEnableOption "Emacs with machine-specific profiles";
profile = mkOption {
type = types.enum ["server" "laptop" "workstation"];
default = "laptop";
description = "Emacs profile to use based on machine type";
};
enableDaemon = mkOption {
type = types.bool;
default = true;
description = "Enable Emacs daemon service";
};
user = mkOption {
type = types.str;
default = "geir";
description = "User to run Emacs daemon for";
};
};
config = mkIf cfg.enable {
# Install Emacs with the selected profile
environment.systemPackages = [
(emacsWithProfile cfg.profile)
];
# System-wide Emacs daemon (optional)
services.emacs = mkIf cfg.enableDaemon {
enable = true;
package = emacsWithProfile cfg.profile;
};
# Create the Emacs configuration directory structure
environment.etc = {
"emacs/init.el" = {
source = ../../dotfiles/geir/emacs-config/init-nix.el;
mode = "0644";
};
# Module files
"emacs/modules/ui.el" = {
source = ../../dotfiles/geir/emacs-config/modules/ui.el;
mode = "0644";
};
"emacs/modules/completion.el" = {
source = ../../dotfiles/geir/emacs-config/modules/completion.el;
mode = "0644";
};
"emacs/modules/navigation.el" = {
source = ../../dotfiles/geir/emacs-config/modules/navigation.el;
mode = "0644";
};
"emacs/modules/development.el" = {
source = ../../dotfiles/geir/emacs-config/modules/development.el;
mode = "0644";
};
"emacs/modules/elisp-development.el" = {
source = ../../dotfiles/geir/emacs-config/modules/elisp-development.el;
mode = "0644";
};
"emacs/modules/claude-code.el" = mkIf (cfg.profile == "workstation") {
source = ../../dotfiles/geir/emacs-config/modules/claude-code.el;
mode = "0644";
};
};
# Environment variables for Nix integration
environment.variables = {
EMACS_PROFILE = cfg.profile;
# Tool paths for Emacs integration
RG_PATH = "${pkgs.ripgrep}/bin/rg";
FD_PATH = "${pkgs.fd}/bin/fd";
SQLITE_PATH = "${pkgs.sqlite}/bin/sqlite3";
AG_PATH = "${pkgs.ag}/bin/ag";
# Language servers
NIL_LSP_PATH = "${pkgs.nixd}/bin/nixd";
BASH_LSP_PATH = "${pkgs.nodePackages.bash-language-server}/bin/bash-language-server";
YAML_LSP_PATH = "${pkgs.nodePackages.yaml-language-server}/bin/yaml-language-server";
# Formatters
SHELLCHECK_PATH = "${pkgs.shellcheck}/bin/shellcheck";
ALEJANDRA_PATH = "${pkgs.alejandra}/bin/alejandra";
};
# Ensure the user can access the Emacs daemon
systemd.user.services.emacs = mkIf cfg.enableDaemon {
environment = {
EMACS_PROFILE = cfg.profile;
NIX_PATH = config.environment.variables.NIX_PATH or "";
};
};
};
}

View file

@ -8,9 +8,7 @@
# Editors # Editors
zed-editor zed-editor
neovim neovim
emacs
vscode vscode
vscodium-fhs
# Language servers # Language servers
nixd nixd
@ -35,12 +33,13 @@
direnv direnv
gh gh
github-copilot-cli github-copilot-cli
deploy-rs
# ai # ai
claude-code claude-code
]; ];
# System-wide Emacs daemon # Note: Emacs is now configured via modules/development/emacs.nix
services.emacs.enable = true; # with machine-specific profiles
# Enable ZSH system-wide for development # Enable ZSH system-wide for development
programs.zsh.enable = true; programs.zsh.enable = true;

View file

@ -78,6 +78,33 @@
User sma User sma
IdentityFile ~/.ssh/id_ed25519_admin IdentityFile ~/.ssh/id_ed25519_admin
# Direct sma user access via Tailscale for deployments
Host sma@sleeper-service.tail807ea.ts.net
Hostname sleeper-service.tail807ea.ts.net
User sma
IdentityFile ~/.ssh/id_ed25519_admin
Host sma@grey-area.tail807ea.ts.net
Hostname grey-area.tail807ea.ts.net
User sma
IdentityFile ~/.ssh/id_ed25519_admin
Host sma@reverse-proxy.tail807ea.ts.net
Hostname reverse-proxy.tail807ea.ts.net
User sma
IdentityFile ~/.ssh/id_ed25519_admin
Host sma@little-rascal.tail807ea.ts.net
Hostname little-rascal.tail807ea.ts.net
User sma
IdentityFile ~/.ssh/id_ed25519_admin
# Localhost sma user for local deployment from laptop
Host sma@localhost
Hostname localhost
User sma
IdentityFile ~/.ssh/id_ed25519_admin
# Tailscale network # Tailscale network
Host 100.* *.tail* Host 100.* *.tail*
User geir User geir

View file

@ -1,4 +1,4 @@
;; lab/deployment.scm - Deployment operations (impure) ;; lab/deployment.scm - Deploy-rs based deployment operations
(define-module (lab deployment) (define-module (lab deployment)
#:use-module (ice-9 format) #:use-module (ice-9 format)
@ -7,10 +7,10 @@
#:use-module (srfi srfi-1) #:use-module (srfi srfi-1)
#:use-module (utils logging) #:use-module (utils logging)
#:use-module (utils config) #:use-module (utils config)
#:use-module (utils ssh)
#:export (deploy-machine #:export (deploy-machine
update-flake update-flake
execute-nixos-rebuild deploy-all-machines
deploy-with-rollback
option-ref)) option-ref))
;; Helper function for option handling ;; Helper function for option handling
@ -19,26 +19,128 @@
(let ((value (assoc-ref options key))) (let ((value (assoc-ref options key)))
(if value value default))) (if value value default)))
;; Impure function: Deploy machine configuration ;; Main deployment function using deploy-rs
(define (deploy-machine machine-name . args) (define (deploy-machine machine-name . args)
"Deploy configuration to machine (impure - has side effects)" "Deploy configuration to machine using deploy-rs (impure - has side effects)"
(let* ((mode (if (null? args) "boot" (car args))) (let* ((mode (if (null? args) "default" (car args)))
(options (if (< (length args) 2) '() (cadr args))) (options (if (< (length args) 2) '() (cadr args)))
(valid-modes '("boot" "test" "switch")) (dry-run (option-ref options 'dry-run #f))
(dry-run (option-ref options 'dry-run #f))) (skip-checks (option-ref options 'skip-checks #f)))
(if (not (validate-machine-name machine-name)) (if (not (validate-machine-name machine-name))
#f #f
(if (not (member mode valid-modes)) (begin
(begin (log-info "Starting deploy-rs deployment: ~a" machine-name)
(log-error "Invalid deployment mode: ~a" mode) (execute-deploy-rs machine-name mode options)))))
(log-error "Valid modes: ~a" (string-join valid-modes ", "))
#f)
(begin
(log-info "Starting deployment: ~a (mode: ~a)" machine-name mode)
(execute-nixos-rebuild machine-name mode options))))))
;; Impure function: Update flake inputs ;; Execute deploy-rs deployment
(define (execute-deploy-rs machine-name mode options)
"Execute deployment using deploy-rs with automatic rollback"
(let* ((homelab-root (get-homelab-root))
(dry-run (option-ref options 'dry-run #f))
(skip-checks (option-ref options 'skip-checks #f))
(auto-rollback (option-ref options 'auto-rollback #t))
(magic-rollback (option-ref options 'magic-rollback #t)))
(log-info "Deploying ~a using deploy-rs..." machine-name)
(if dry-run
(begin
(log-info "DRY RUN: Would execute deploy-rs for ~a" machine-name)
(log-info "Command would be: deploy '.#~a'" machine-name)
#t)
(let* ((deploy-cmd (build-deploy-command machine-name skip-checks auto-rollback magic-rollback))
(start-time (current-time)))
(log-debug "Deploy command: ~a" deploy-cmd)
(log-info "Executing deployment with automatic rollback protection...")
(let* ((port (open-pipe* OPEN_READ "/bin/sh" "-c" deploy-cmd))
(output (get-string-all port))
(status (close-pipe port))
(elapsed (- (current-time) start-time)))
(if (zero? status)
(begin
(log-success "Deploy-rs deployment completed successfully in ~as" elapsed)
(log-info "Deployment output:")
(log-info "~a" output)
#t)
(begin
(log-error "Deploy-rs deployment failed (exit: ~a)" status)
(log-error "Error output:")
(log-error "~a" output)
(log-info "Deploy-rs automatic rollback should have been triggered")
#f)))))))
;; Build deploy-rs command with options
(define (build-deploy-command machine-name skip-checks auto-rollback magic-rollback)
"Build the deploy-rs command with appropriate flags"
(let ((base-cmd (format #f "cd ~a && deploy '.#~a'" (get-homelab-root) machine-name))
(flags '()))
;; Add flags based on options
(when skip-checks
(set! flags (cons "--skip-checks" flags)))
(when auto-rollback
(set! flags (cons "--auto-rollback" flags)))
(when magic-rollback
(set! flags (cons "--magic-rollback" flags)))
;; Combine command with flags
(if (null? flags)
base-cmd
(format #f "~a ~a" base-cmd (string-join flags " ")))))
;; Deploy to all machines
(define (deploy-all-machines . args)
"Deploy to all machines using deploy-rs"
(let* ((options (if (null? args) '() (car args)))
(dry-run (option-ref options 'dry-run #f))
(machines (get-all-machines)))
(log-info "Starting deployment to all machines (~a total)" (length machines))
(let ((results
(map (lambda (machine)
(log-info "Deploying to ~a..." machine)
(let ((result (deploy-machine machine "default" options)))
(if result
(log-success "✓ ~a deployed successfully" machine)
(log-error "✗ ~a deployment failed" machine))
(cons machine result)))
machines)))
;; Summary
(let ((successful (filter cdr results))
(failed (filter (lambda (r) (not (cdr r))) results)))
(log-info "Deployment summary:")
(log-info " Successful: ~a/~a machines" (length successful) (length machines))
(when (not (null? failed))
(log-error " Failed: ~a" (string-join (map car failed) ", ")))
;; Return true if all succeeded
(= (length successful) (length machines))))))
;; Deploy with explicit rollback testing
(define (deploy-with-rollback machine-name . args)
"Deploy with explicit rollback capability testing"
(let* ((options (if (null? args) '() (car args)))
(test-rollback (option-ref options 'test-rollback #f)))
(log-info "Deploying ~a with rollback testing..." machine-name)
(if test-rollback
(begin
(log-info "Testing rollback mechanism (deploy will be reverted)")
;; Deploy with magic rollback disabled to test manual rollback
(let ((modified-options (cons '(magic-rollback . #f) options)))
(execute-deploy-rs machine-name "default" modified-options)))
(execute-deploy-rs machine-name "default" options))))
;; Update flake inputs (moved from old deployment module)
(define (update-flake . args) (define (update-flake . args)
"Update flake inputs (impure - has side effects)" "Update flake inputs (impure - has side effects)"
(let* ((options (if (null? args) '() (car args))) (let* ((options (if (null? args) '() (car args)))
@ -64,76 +166,3 @@
(log-error "Flake update failed (exit: ~a)" status) (log-error "Flake update failed (exit: ~a)" status)
(log-error "Error output: ~a" output) (log-error "Error output: ~a" output)
#f)))))) #f))))))
;; Impure function: Execute nixos-rebuild
(define (execute-nixos-rebuild machine-name mode options)
"Execute nixos-rebuild command (impure - has side effects)"
(let* ((dry-run (option-ref options 'dry-run #f))
(ssh-config (get-ssh-config machine-name))
(is-local (and ssh-config (assoc-ref ssh-config 'is-local)))
(homelab-root (get-homelab-root)))
(if is-local
;; Local deployment
(let ((rebuild-cmd (format #f "sudo nixos-rebuild ~a --flake ~a#~a"
mode homelab-root machine-name)))
(log-debug "Local rebuild command: ~a" rebuild-cmd)
(if dry-run
(begin
(log-info "DRY RUN: Would execute: ~a" rebuild-cmd)
#t)
(let* ((port (open-pipe* OPEN_READ "/bin/sh" "-c" rebuild-cmd))
(output (get-string-all port))
(status (close-pipe port)))
(if (zero? status)
(begin
(log-success "Local nixos-rebuild completed")
#t)
(begin
(log-error "Local nixos-rebuild failed (exit: ~a)" status)
#f)))))
;; Remote deployment
(let* ((hostname (assoc-ref ssh-config 'hostname))
(ssh-alias (or (assoc-ref ssh-config 'ssh-alias) hostname))
(temp-dir "/tmp/homelab-deploy")
(sync-cmd (format #f "rsync -av --delete ~a/ ~a:~a/"
homelab-root ssh-alias temp-dir))
(rebuild-cmd (format #f "ssh ~a 'cd ~a && sudo nixos-rebuild ~a --flake .#~a'"
ssh-alias temp-dir mode machine-name)))
(log-debug "Remote sync command: ~a" sync-cmd)
(log-debug "Remote rebuild command: ~a" rebuild-cmd)
(if dry-run
(begin
(log-info "DRY RUN: Would sync and rebuild remotely")
#t)
(begin
;; Sync configuration
(log-info "Syncing configuration to ~a..." machine-name)
(let* ((sync-port (open-pipe* OPEN_READ "/bin/sh" "-c" sync-cmd))
(sync-output (get-string-all sync-port))
(sync-status (close-pipe sync-port)))
(if (zero? sync-status)
(begin
(log-success "Configuration synced")
;; Execute rebuild
(log-info "Executing nixos-rebuild on ~a..." machine-name)
(let* ((rebuild-port (open-pipe* OPEN_READ "/bin/sh" "-c" rebuild-cmd))
(rebuild-output (get-string-all rebuild-port))
(rebuild-status (close-pipe rebuild-port)))
(if (zero? rebuild-status)
(begin
(log-success "Remote nixos-rebuild completed")
#t)
(begin
(log-error "Remote nixos-rebuild failed (exit: ~a)" rebuild-status)
#f))))
(begin
(log-error "Configuration sync failed (exit: ~a)" sync-status)
#f)))))))))

View file

@ -22,44 +22,48 @@
;; Pure function: Command help text ;; Pure function: Command help text
(define (get-help-text) (define (get-help-text)
"Pure function returning help text" "Pure function returning help text"
"Home Lab Tool - K.I.S.S Refactored Edition "Home Lab Tool - Deploy-rs Edition
USAGE: lab <command> [args...] USAGE: lab <command> [args...]
COMMANDS: COMMANDS:
status Show infrastructure status status Show infrastructure status
machines List all machines machines List all machines
deploy <machine> [mode] Deploy configuration to machine deploy <machine> [options] Deploy configuration to machine using deploy-rs
Available modes: boot (default), test, switch Options: --dry-run, --skip-checks
deploy-all Deploy to all machines deploy-all [options] Deploy to all machines using deploy-rs
update Update flake inputs update Update flake inputs
auto-update Perform automatic system update with health checks auto-update Perform automatic system update with health checks
auto-update-status Show auto-update service status and logs auto-update-status Show auto-update service status and logs
health [machine] Check machine health (all if no machine specified) health [machine] Check machine health (all if no machine specified)
ssh <machine> SSH to machine ssh <machine> SSH to machine (using sma user)
test-modules Test modular implementation test-rollback <machine> Test deployment with rollback
help Show this help help Show this help
EXAMPLES: EXAMPLES:
lab status lab status
lab machines lab machines
lab deploy congenital-optimist # Deploy with boot mode (default) lab deploy congenital-optimist # Deploy with deploy-rs safety
lab deploy congenital-optimist switch # Deploy and activate immediately lab deploy sleeper-service --dry-run # Test deployment without applying
lab deploy congenital-optimist test # Deploy temporarily for testing lab deploy grey-area --skip-checks # Deploy without health checks
lab deploy-all lab deploy-all # Deploy to all machines
lab update lab deploy-all --dry-run # Test deployment to all machines
lab auto-update # Perform automatic update with reboot lab update # Update flake inputs
lab auto-update-status # Show auto-update logs and status lab test-rollback sleeper-service # Test rollback functionality
lab health lab ssh sleeper-service # SSH to machine as sma user
lab health sleeper-service
lab ssh sleeper-service
lab test-modules
This implementation follows K.I.S.S principles: Deploy-rs Features:
- Modular: Each module has single responsibility - Automatic rollback on deployment failure
- Functional: Pure functions separated from side effects - Health checks after deployment
- Small: Individual modules under 50 lines - Magic rollback for network connectivity issues
- Simple: One function does one thing well - Atomic deployments with safety guarantees
- Consistent sma user for all deployments
This implementation uses deploy-rs for all deployments:
- Robust: Automatic rollback protection
- Safe: Health checks and validation
- Consistent: Same deployment method for all machines
- Flexible: Dry-run and skip-checks options available
") ")
;; Pure function: Format machine list ;; Pure function: Format machine list
@ -109,36 +113,33 @@ Home lab root: ~a
(log-success "Machine list complete"))) (log-success "Machine list complete")))
(define (cmd-deploy machine-name . args) (define (cmd-deploy machine-name . args)
"Deploy configuration to machine" "Deploy configuration to machine using deploy-rs"
(let* ((mode (if (null? args) "boot" (car args))) (let* ((options (parse-deploy-options args)))
(valid-modes '("boot" "test" "switch"))) (log-info "Deploying to machine: ~a using deploy-rs" machine-name)
(log-info "Deploying to machine: ~a (mode: ~a)" machine-name mode) (if (validate-machine-name machine-name)
(if (not (member mode valid-modes)) (let ((result (deploy-machine machine-name "default" options)))
(if result
(log-success "Deploy-rs deployment to ~a completed successfully" machine-name)
(log-error "Deploy-rs deployment to ~a failed" machine-name)))
(begin (begin
(log-error "Invalid deployment mode: ~a" mode) (log-error "Invalid machine: ~a" machine-name)
(log-error "Valid modes: ~a" (string-join valid-modes ", ")) (log-info "Available machines: ~a" (string-join (get-all-machines) ", "))))))
(format #t "Usage: lab deploy <machine> [boot|test|switch]\n"))
(if (validate-machine-name machine-name)
(let ((result (deploy-machine machine-name mode '())))
(if result
(log-success "Deployment to ~a complete (mode: ~a)" machine-name mode)
(log-error "Deployment to ~a failed" machine-name)))
(begin
(log-error "Invalid machine: ~a" machine-name)
(log-info "Available machines: ~a" (string-join (get-all-machines) ", ")))))))
(define (cmd-ssh machine-name) (define (cmd-ssh machine-name)
"SSH to machine" "SSH to machine using sma user"
(log-info "Connecting to machine: ~a" machine-name) (log-info "Connecting to machine: ~a as sma user" machine-name)
(if (validate-machine-name machine-name) (if (validate-machine-name machine-name)
(let ((ssh-config (get-ssh-config machine-name))) (let ((ssh-config (get-ssh-config machine-name)))
(if ssh-config (if ssh-config
(let ((hostname (assoc-ref ssh-config 'hostname)) (let ((hostname (assoc-ref ssh-config 'hostname))
(ssh-alias (assoc-ref ssh-config 'ssh-alias)) (ssh-alias (assoc-ref ssh-config 'ssh-alias))
(ssh-user (assoc-ref ssh-config 'ssh-user))
(is-local (assoc-ref ssh-config 'is-local))) (is-local (assoc-ref ssh-config 'is-local)))
(if is-local (if is-local
(log-info "Machine ~a is local - no SSH needed" machine-name) (begin
(let ((target (or ssh-alias hostname))) (log-info "Machine ~a is local - switching to sma user locally" machine-name)
(system "sudo -u sma -i"))
(let ((target (format #f "~a@~a" (or ssh-user "sma") (or ssh-alias hostname))))
(log-info "Connecting to ~a via SSH..." target) (log-info "Connecting to ~a via SSH..." target)
(system (format #f "ssh ~a" target))))) (system (format #f "ssh ~a" target)))))
(log-error "No SSH configuration found for ~a" machine-name))) (log-error "No SSH configuration found for ~a" machine-name)))
@ -171,20 +172,12 @@ Home lab root: ~a
(log-error "Flake update failed")))) (log-error "Flake update failed"))))
(define (cmd-deploy-all) (define (cmd-deploy-all)
"Deploy to all machines" "Deploy to all machines using deploy-rs"
(log-info "Deploying to all machines...") (log-info "Deploying to all machines using deploy-rs...")
(let* ((machines (list-machines)) (let ((result (deploy-all-machines '())))
(results (map (lambda (machine) (if result
(log-info "Deploying to ~a..." machine) (log-success "All deploy-rs deployments completed successfully")
(let ((result (deploy-machine machine "boot" '()))) (log-error "Some deploy-rs deployments failed"))))
(if result
(log-success "✓ ~a deployed" machine)
(log-error "✗ ~a failed" machine))
result))
machines))
(successful (filter identity results)))
(log-info "Deployment summary: ~a/~a successful"
(length successful) (length machines))))
(define (cmd-health args) (define (cmd-health args)
"Check machine health" "Check machine health"
@ -219,6 +212,33 @@ Home lab root: ~a
"Show auto-update status and logs" "Show auto-update status and logs"
(auto-update-status)) (auto-update-status))
;; Parse deployment options from command line arguments
(define (parse-deploy-options args)
"Parse deployment options from command line arguments"
(let ((options '()))
(for-each
(lambda (arg)
(cond
((string=? arg "--dry-run")
(set! options (cons '(dry-run . #t) options)))
((string=? arg "--skip-checks")
(set! options (cons '(skip-checks . #t) options)))
(else
(log-warn "Unknown option: ~a" arg))))
args)
options))
(define (cmd-test-rollback machine-name)
"Test deployment with rollback functionality"
(log-info "Testing rollback deployment for machine: ~a" machine-name)
(if (validate-machine-name machine-name)
(let ((options '((test-rollback . #t))))
(let ((result (deploy-with-rollback machine-name options)))
(if result
(log-success "Rollback test completed for ~a" machine-name)
(log-error "Rollback test failed for ~a" machine-name))))
(log-error "Invalid machine: ~a" machine-name)))
;; Main command dispatcher ;; Main command dispatcher
(define (dispatch-command command args) (define (dispatch-command command args)
"Dispatch command with appropriate handler" "Dispatch command with appropriate handler"
@ -236,12 +256,20 @@ Home lab root: ~a
(if (null? args) (if (null? args)
(begin (begin
(log-error "deploy command requires machine name") (log-error "deploy command requires machine name")
(format #t "Usage: lab deploy <machine> [boot|test|switch]\n")) (format #t "Usage: lab deploy <machine> [options]\n")
(format #t "Options: --dry-run, --skip-checks\n"))
(apply cmd-deploy args))) (apply cmd-deploy args)))
('deploy-all ('deploy-all
(cmd-deploy-all)) (cmd-deploy-all))
('test-rollback
(if (null? args)
(begin
(log-error "test-rollback command requires machine name")
(format #t "Usage: lab test-rollback <machine>\n"))
(cmd-test-rollback (car args))))
('update ('update
(cmd-update)) (cmd-update))
@ -264,6 +292,13 @@ Home lab root: ~a
('test-modules ('test-modules
(cmd-test-modules)) (cmd-test-modules))
('test-rollback
(if (null? args)
(begin
(log-error "test-rollback command requires machine name")
(format #t "Usage: lab test-rollback <machine>\n"))
(cmd-test-rollback (car args))))
(_ (_
(log-error "Unknown command: ~a" command) (log-error "Unknown command: ~a" command)
(format #t "Use 'lab help' for available commands\n") (format #t "Use 'lab help' for available commands\n")
@ -272,7 +307,7 @@ Home lab root: ~a
;; Main entry point ;; Main entry point
(define (main args) (define (main args)
"Main entry point for lab tool" "Main entry point for lab tool"
(log-info "Home Lab Tool - K.I.S.S Refactored Edition") (log-info "Home Lab Tool - Deploy-rs Edition")
(let* ((parsed-cmd (if (> (length args) 1) (cdr args) '("help"))) (let* ((parsed-cmd (if (> (length args) 1) (cdr args) '("help")))
(command (string->symbol (car parsed-cmd))) (command (string->symbol (car parsed-cmd)))

View file

@ -22,26 +22,31 @@
(machines . ((congenital-optimist (machines . ((congenital-optimist
(type . local) (type . local)
(hostname . "localhost") (hostname . "localhost")
(ssh-user . "sma")
(services . (workstation development))) (services . (workstation development)))
(sleeper-service (sleeper-service
(type . remote) (type . remote)
(hostname . "sleeper-service.tail807ea.ts.net") (hostname . "sleeper-service.tail807ea.ts.net")
(ssh-alias . "admin-sleeper") (ssh-alias . "sleeper-service.tail807ea.ts.net")
(ssh-user . "sma")
(services . (nfs zfs storage))) (services . (nfs zfs storage)))
(grey-area (grey-area
(type . remote) (type . remote)
(hostname . "grey-area.tail807ea.ts.net") (hostname . "grey-area.tail807ea.ts.net")
(ssh-alias . "admin-grey") (ssh-alias . "grey-area.tail807ea.ts.net")
(ssh-user . "sma")
(services . (ollama forgejo git))) (services . (ollama forgejo git)))
(reverse-proxy (reverse-proxy
(type . remote) (type . remote)
(hostname . "reverse-proxy.tail807ea.ts.net") (hostname . "reverse-proxy.tail807ea.ts.net")
(ssh-alias . "admin-reverse") (ssh-alias . "reverse-proxy.tail807ea.ts.net")
(ssh-user . "sma")
(services . (nginx proxy ssl))) (services . (nginx proxy ssl)))
(little-rascal (little-rascal
(type . remote) (type . remote)
(hostname . "little-rascal.tail807ea.ts.net") (hostname . "little-rascal.tail807ea.ts.net")
(ssh-alias . "little-rascal") (ssh-alias . "little-rascal.tail807ea.ts.net")
(ssh-user . "sma")
(services . (development niri desktop ai-tools))))) (services . (development niri desktop ai-tools)))))
(deployment . ((default-mode . "boot") (deployment . ((default-mode . "boot")
(timeout . 300) (timeout . 300)
@ -124,10 +129,12 @@
(if machine-config (if machine-config
(let ((type (assoc-ref machine-config 'type)) (let ((type (assoc-ref machine-config 'type))
(hostname (assoc-ref machine-config 'hostname)) (hostname (assoc-ref machine-config 'hostname))
(ssh-alias (assoc-ref machine-config 'ssh-alias))) (ssh-alias (assoc-ref machine-config 'ssh-alias))
(ssh-user (assoc-ref machine-config 'ssh-user)))
`((type . ,type) `((type . ,type)
(hostname . ,hostname) (hostname . ,hostname)
(ssh-alias . ,ssh-alias) (ssh-alias . ,ssh-alias)
(ssh-user . ,ssh-user)
(is-local . ,(eq? type 'local)))) (is-local . ,(eq? type 'local))))
#f))) #f)))

34
shell.nix Normal file
View file

@ -0,0 +1,34 @@
# Nix shell for Home Lab development with deploy-rs and lab-tool
{
description = "Home Lab dev shell with deploy-rs and lab-tool";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
deploy-rs.url = "github:serokell/deploy-rs";
};
outputs = { self, nixpkgs, deploy-rs, ... }@inputs: let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
in {
devShells.${system}.default = pkgs.mkShell {
buildInputs = [
pkgs.git
pkgs.guile_3_0
pkgs.guile-ssh
pkgs.guile-json
pkgs.guile-git
pkgs.guile-gcrypt
pkgs.openssh
pkgs.nixos-rebuild
deploy-rs.packages.${system}.deploy-rs
(import ./packages/lab-tool/default.nix { inherit (pkgs) lib stdenv makeWrapper guile_3_0 guile-ssh guile-json guile-git guile-gcrypt openssh git nixos-rebuild; })
];
shellHook = ''
echo "Dev shell: deploy-rs and lab-tool available."
echo "Try: lab status, lab deploy <machine>, or deploy . <target>"
'';
};
};
}