grokking simplicity and refactoring

This commit is contained in:
Geir Okkenhaug Jerstad 2025-06-16 13:43:21 +02:00
parent 89a7fe100d
commit fb4361d938
67 changed files with 6275 additions and 56 deletions

19
packages/.instructions.md Normal file
View file

@ -0,0 +1,19 @@
# AI Agent General Instructions
## Overview
This part of the document provides general instructions for the AI agent.
## General Instructions
- Treat this as iterative collaboration between user and AI agent
- **Context7 MCP is mandatory** for all technical documentation queries
- **Taskmaster** use it.
- Use casual but knowledgeable tone - hobby/passion project, not corporate, no/little humor , be terse
- Use K.I.S.S priciples in both code and written language
- Update documentation frequently as project evolves - but keep it short
- Research is in the research folder keep research topical for the project
# Coding style
- K.I.S.S - Modularize everything - keep it simple, keep it small
- UNIX - DO one thing well - prefer one function per file and one function does one thing
- Functional principles - use functional patterns - mark actions as imure ie has side effects
- Minimalism - keeping the lines of code low is better - more than 200 lines indicate a need to modularize

View file

@ -1,6 +1,6 @@
{
"currentTag": "master",
"lastSwitched": "2025-06-15T19:47:47.438Z",
"lastSwitched": "2025-06-16T11:12:46.967Z",
"branchTagMapping": {},
"migrationNoticeShown": false
}

View file

@ -522,3 +522,530 @@ alwaysApply: true
- Document breaking changes
Follow WINDSURF_RULES for proper rule formatting and structure of windsurf rule sections.
# Added by Task Master - Development Workflow Rules
Below you will find a variety of important rules spanning:
- the dev_workflow
- the .windsurfrules document self-improvement workflow
- the template to follow when modifying or adding new sections/rules to this document.
---
## DEV_WORKFLOW
description: Guide for using meta-development script (scripts/dev.js) to manage task-driven development workflows
globs: **/\*
filesToApplyRule: **/\*
alwaysApply: true
---
- **Global CLI Commands**
- Task Master now provides a global CLI through the `task-master` command
- All functionality from `scripts/dev.js` is available through this interface
- Install globally with `npm install -g claude-task-master` or use locally via `npx`
- Use `task-master <command>` instead of `node scripts/dev.js <command>`
- Examples:
- `task-master list` instead of `node scripts/dev.js list`
- `task-master next` instead of `node scripts/dev.js next`
- `task-master expand --id=3` instead of `node scripts/dev.js expand --id=3`
- All commands accept the same options as their script equivalents
- The CLI provides additional commands like `task-master init` for project setup
- **Development Workflow Process**
- Start new projects by running `task-master init` or `node scripts/dev.js parse-prd --input=<prd-file.txt>` to generate initial tasks.json
- Begin coding sessions with `task-master list` to see current tasks, status, and IDs
- Analyze task complexity with `task-master analyze-complexity --research` before breaking down tasks
- Select tasks based on dependencies (all marked 'done'), priority level, and ID order
- Clarify tasks by checking task files in tasks/ directory or asking for user input
- View specific task details using `task-master show <id>` to understand implementation requirements
- Break down complex tasks using `task-master expand --id=<id>` with appropriate flags
- Clear existing subtasks if needed using `task-master clear-subtasks --id=<id>` before regenerating
- Implement code following task details, dependencies, and project standards
- Verify tasks according to test strategies before marking as complete
- Mark completed tasks with `task-master set-status --id=<id> --status=done`
- Update dependent tasks when implementation differs from original plan
- Generate task files with `task-master generate` after updating tasks.json
- Maintain valid dependency structure with `task-master fix-dependencies` when needed
- Respect dependency chains and task priorities when selecting work
- Report progress regularly using the list command
- **Task Complexity Analysis**
- Run `node scripts/dev.js analyze-complexity --research` for comprehensive analysis
- Review complexity report in scripts/task-complexity-report.json
- Or use `node scripts/dev.js complexity-report` for a formatted, readable version of the report
- Focus on tasks with highest complexity scores (8-10) for detailed breakdown
- Use analysis results to determine appropriate subtask allocation
- Note that reports are automatically used by the expand command
- **Task Breakdown Process**
- For tasks with complexity analysis, use `node scripts/dev.js expand --id=<id>`
- Otherwise use `node scripts/dev.js expand --id=<id> --subtasks=<number>`
- Add `--research` flag to leverage Perplexity AI for research-backed expansion
- Use `--prompt="<context>"` to provide additional context when needed
- Review and adjust generated subtasks as necessary
- Use `--all` flag to expand multiple pending tasks at once
- If subtasks need regeneration, clear them first with `clear-subtasks` command
- **Implementation Drift Handling**
- When implementation differs significantly from planned approach
- When future tasks need modification due to current implementation choices
- When new dependencies or requirements emerge
- Call `node scripts/dev.js update --from=<futureTaskId> --prompt="<explanation>"` to update tasks.json
- **Task Status Management**
- Use 'pending' for tasks ready to be worked on
- Use 'done' for completed and verified tasks
- Use 'deferred' for postponed tasks
- Add custom status values as needed for project-specific workflows
- **Task File Format Reference**
```
# Task ID: <id>
# Title: <title>
# Status: <status>
# Dependencies: <comma-separated list of dependency IDs>
# Priority: <priority>
# Description: <brief description>
# Details:
<detailed implementation notes>
# Test Strategy:
<verification approach>
```
- **Command Reference: parse-prd**
- Legacy Syntax: `node scripts/dev.js parse-prd --input=<prd-file.txt>`
- CLI Syntax: `task-master parse-prd --input=<prd-file.txt>`
- Description: Parses a PRD document and generates a tasks.json file with structured tasks
- Parameters:
- `--input=<file>`: Path to the PRD text file (default: sample-prd.txt)
- Example: `task-master parse-prd --input=requirements.txt`
- Notes: Will overwrite existing tasks.json file. Use with caution.
- **Command Reference: update**
- Legacy Syntax: `node scripts/dev.js update --from=<id> --prompt="<prompt>"`
- CLI Syntax: `task-master update --from=<id> --prompt="<prompt>"`
- Description: Updates tasks with ID >= specified ID based on the provided prompt
- Parameters:
- `--from=<id>`: Task ID from which to start updating (required)
- `--prompt="<text>"`: Explanation of changes or new context (required)
- Example: `task-master update --from=4 --prompt="Now we are using Express instead of Fastify."`
- Notes: Only updates tasks not marked as 'done'. Completed tasks remain unchanged.
- **Command Reference: generate**
- Legacy Syntax: `node scripts/dev.js generate`
- CLI Syntax: `task-master generate`
- Description: Generates individual task files based on tasks.json
- Parameters:
- `--file=<path>, -f`: Use alternative tasks.json file (default: '.taskmaster/tasks/tasks.json')
- `--output=<dir>, -o`: Output directory (default: '.taskmaster/tasks')
- Example: `task-master generate`
- Notes: Overwrites existing task files. Creates output directory if needed.
- **Command Reference: set-status**
- Legacy Syntax: `node scripts/dev.js set-status --id=<id> --status=<status>`
- CLI Syntax: `task-master set-status --id=<id> --status=<status>`
- Description: Updates the status of a specific task in tasks.json
- Parameters:
- `--id=<id>`: ID of the task to update (required)
- `--status=<status>`: New status value (required)
- Example: `task-master set-status --id=3 --status=done`
- Notes: Common values are 'done', 'pending', and 'deferred', but any string is accepted.
- **Command Reference: list**
- Legacy Syntax: `node scripts/dev.js list`
- CLI Syntax: `task-master list`
- Description: Lists all tasks in tasks.json with IDs, titles, and status
- Parameters:
- `--status=<status>, -s`: Filter by status
- `--with-subtasks`: Show subtasks for each task
- `--file=<path>, -f`: Use alternative tasks.json file (default: 'tasks/tasks.json')
- Example: `task-master list`
- Notes: Provides quick overview of project progress. Use at start of sessions.
- **Command Reference: expand**
- Legacy Syntax: `node scripts/dev.js expand --id=<id> [--num=<number>] [--research] [--prompt="<context>"]`
- CLI Syntax: `task-master expand --id=<id> [--num=<number>] [--research] [--prompt="<context>"]`
- Description: Expands a task with subtasks for detailed implementation
- Parameters:
- `--id=<id>`: ID of task to expand (required unless using --all)
- `--all`: Expand all pending tasks, prioritized by complexity
- `--num=<number>`: Number of subtasks to generate (default: from complexity report)
- `--research`: Use Perplexity AI for research-backed generation
- `--prompt="<text>"`: Additional context for subtask generation
- `--force`: Regenerate subtasks even for tasks that already have them
- Example: `task-master expand --id=3 --num=5 --research --prompt="Focus on security aspects"`
- Notes: Uses complexity report recommendations if available.
- **Command Reference: analyze-complexity**
- Legacy Syntax: `node scripts/dev.js analyze-complexity [options]`
- CLI Syntax: `task-master analyze-complexity [options]`
- Description: Analyzes task complexity and generates expansion recommendations
- Parameters:
- `--output=<file>, -o`: Output file path (default: scripts/task-complexity-report.json)
- `--model=<model>, -m`: Override LLM model to use
- `--threshold=<number>, -t`: Minimum score for expansion recommendation (default: 5)
- `--file=<path>, -f`: Use alternative tasks.json file
- `--research, -r`: Use Perplexity AI for research-backed analysis
- Example: `task-master analyze-complexity --research`
- Notes: Report includes complexity scores, recommended subtasks, and tailored prompts.
- **Command Reference: clear-subtasks**
- Legacy Syntax: `node scripts/dev.js clear-subtasks --id=<id>`
- CLI Syntax: `task-master clear-subtasks --id=<id>`
- Description: Removes subtasks from specified tasks to allow regeneration
- Parameters:
- `--id=<id>`: ID or comma-separated IDs of tasks to clear subtasks from
- `--all`: Clear subtasks from all tasks
- Examples:
- `task-master clear-subtasks --id=3`
- `task-master clear-subtasks --id=1,2,3`
- `task-master clear-subtasks --all`
- Notes:
- Task files are automatically regenerated after clearing subtasks
- Can be combined with expand command to immediately generate new subtasks
- Works with both parent tasks and individual subtasks
- **Task Structure Fields**
- **id**: Unique identifier for the task (Example: `1`)
- **title**: Brief, descriptive title (Example: `"Initialize Repo"`)
- **description**: Concise summary of what the task involves (Example: `"Create a new repository, set up initial structure."`)
- **status**: Current state of the task (Example: `"pending"`, `"done"`, `"deferred"`)
- **dependencies**: IDs of prerequisite tasks (Example: `[1, 2]`)
- Dependencies are displayed with status indicators (✅ for completed, ⏱️ for pending)
- This helps quickly identify which prerequisite tasks are blocking work
- **priority**: Importance level (Example: `"high"`, `"medium"`, `"low"`)
- **details**: In-depth implementation instructions (Example: `"Use GitHub client ID/secret, handle callback, set session token."`)
- **testStrategy**: Verification approach (Example: `"Deploy and call endpoint to confirm 'Hello World' response."`)
- **subtasks**: List of smaller, more specific tasks (Example: `[{"id": 1, "title": "Configure OAuth", ...}]`)
- **Environment Variables Configuration**
- **ANTHROPIC_API_KEY** (Required): Your Anthropic API key for Claude (Example: `ANTHROPIC_API_KEY=sk-ant-api03-...`)
- **MODEL** (Default: `"claude-3-7-sonnet-20250219"`): Claude model to use (Example: `MODEL=claude-3-opus-20240229`)
- **MAX_TOKENS** (Default: `"4000"`): Maximum tokens for responses (Example: `MAX_TOKENS=8000`)
- **TEMPERATURE** (Default: `"0.7"`): Temperature for model responses (Example: `TEMPERATURE=0.5`)
- **DEBUG** (Default: `"false"`): Enable debug logging (Example: `DEBUG=true`)
- **TASKMASTER_LOG_LEVEL** (Default: `"info"`): Console output level (Example: `TASKMASTER_LOG_LEVEL=debug`)
- **DEFAULT_SUBTASKS** (Default: `"3"`): Default subtask count (Example: `DEFAULT_SUBTASKS=5`)
- **DEFAULT_PRIORITY** (Default: `"medium"`): Default priority (Example: `DEFAULT_PRIORITY=high`)
- **PROJECT_NAME** (Default: `"MCP SaaS MVP"`): Project name in metadata (Example: `PROJECT_NAME=My Awesome Project`)
- **PROJECT_VERSION** (Default: `"1.0.0"`): Version in metadata (Example: `PROJECT_VERSION=2.1.0`)
- **PERPLEXITY_API_KEY**: For research-backed features (Example: `PERPLEXITY_API_KEY=pplx-...`)
- **PERPLEXITY_MODEL** (Default: `"sonar-medium-online"`): Perplexity model (Example: `PERPLEXITY_MODEL=sonar-large-online`)
- **Determining the Next Task**
- Run `task-master next` to show the next task to work on
- The next command identifies tasks with all dependencies satisfied
- Tasks are prioritized by priority level, dependency count, and ID
- The command shows comprehensive task information including:
- Basic task details and description
- Implementation details
- Subtasks (if they exist)
- Contextual suggested actions
- Recommended before starting any new development work
- Respects your project's dependency structure
- Ensures tasks are completed in the appropriate sequence
- Provides ready-to-use commands for common task actions
- **Viewing Specific Task Details**
- Run `task-master show <id>` or `task-master show --id=<id>` to view a specific task
- Use dot notation for subtasks: `task-master show 1.2` (shows subtask 2 of task 1)
- Displays comprehensive information similar to the next command, but for a specific task
- For parent tasks, shows all subtasks and their current status
- For subtasks, shows parent task information and relationship
- Provides contextual suggested actions appropriate for the specific task
- Useful for examining task details before implementation or checking status
- **Managing Task Dependencies**
- Use `task-master add-dependency --id=<id> --depends-on=<id>` to add a dependency
- Use `task-master remove-dependency --id=<id> --depends-on=<id>` to remove a dependency
- The system prevents circular dependencies and duplicate dependency entries
- Dependencies are checked for existence before being added or removed
- Task files are automatically regenerated after dependency changes
- Dependencies are visualized with status indicators in task listings and files
- **Command Reference: add-dependency**
- Legacy Syntax: `node scripts/dev.js add-dependency --id=<id> --depends-on=<id>`
- CLI Syntax: `task-master add-dependency --id=<id> --depends-on=<id>`
- Description: Adds a dependency relationship between two tasks
- Parameters:
- `--id=<id>`: ID of task that will depend on another task (required)
- `--depends-on=<id>`: ID of task that will become a dependency (required)
- Example: `task-master add-dependency --id=22 --depends-on=21`
- Notes: Prevents circular dependencies and duplicates; updates task files automatically
- **Command Reference: remove-dependency**
- Legacy Syntax: `node scripts/dev.js remove-dependency --id=<id> --depends-on=<id>`
- CLI Syntax: `task-master remove-dependency --id=<id> --depends-on=<id>`
- Description: Removes a dependency relationship between two tasks
- Parameters:
- `--id=<id>`: ID of task to remove dependency from (required)
- `--depends-on=<id>`: ID of task to remove as a dependency (required)
- Example: `task-master remove-dependency --id=22 --depends-on=21`
- Notes: Checks if dependency actually exists; updates task files automatically
- **Command Reference: validate-dependencies**
- Legacy Syntax: `node scripts/dev.js validate-dependencies [options]`
- CLI Syntax: `task-master validate-dependencies [options]`
- Description: Checks for and identifies invalid dependencies in tasks.json and task files
- Parameters:
- `--file=<path>, -f`: Use alternative tasks.json file (default: 'tasks/tasks.json')
- Example: `task-master validate-dependencies`
- Notes:
- Reports all non-existent dependencies and self-dependencies without modifying files
- Provides detailed statistics on task dependency state
- Use before fix-dependencies to audit your task structure
- **Command Reference: fix-dependencies**
- Legacy Syntax: `node scripts/dev.js fix-dependencies [options]`
- CLI Syntax: `task-master fix-dependencies [options]`
- Description: Finds and fixes all invalid dependencies in tasks.json and task files
- Parameters:
- `--file=<path>, -f`: Use alternative tasks.json file (default: 'tasks/tasks.json')
- Example: `task-master fix-dependencies`
- Notes:
- Removes references to non-existent tasks and subtasks
- Eliminates self-dependencies (tasks depending on themselves)
- Regenerates task files with corrected dependencies
- Provides detailed report of all fixes made
- **Command Reference: complexity-report**
- Legacy Syntax: `node scripts/dev.js complexity-report [options]`
- CLI Syntax: `task-master complexity-report [options]`
- Description: Displays the task complexity analysis report in a formatted, easy-to-read way
- Parameters:
- `--file=<path>, -f`: Path to the complexity report file (default: 'scripts/task-complexity-report.json')
- Example: `task-master complexity-report`
- Notes:
- Shows tasks organized by complexity score with recommended actions
- Provides complexity distribution statistics
- Displays ready-to-use expansion commands for complex tasks
- If no report exists, offers to generate one interactively
- **Command Reference: add-task**
- CLI Syntax: `task-master add-task [options]`
- Description: Add a new task to tasks.json using AI
- Parameters:
- `--file=<path>, -f`: Path to the tasks file (default: 'tasks/tasks.json')
- `--prompt=<text>, -p`: Description of the task to add (required)
- `--dependencies=<ids>, -d`: Comma-separated list of task IDs this task depends on
- `--priority=<priority>`: Task priority (high, medium, low) (default: 'medium')
- Example: `task-master add-task --prompt="Create user authentication using Auth0"`
- Notes: Uses AI to convert description into structured task with appropriate details
- **Command Reference: init**
- CLI Syntax: `task-master init`
- Description: Initialize a new project with Task Master structure
- Parameters: None
- Example: `task-master init`
- Notes:
- Creates initial project structure with required files
- Prompts for project settings if not provided
- Merges with existing files when appropriate
- Can be used to bootstrap a new Task Master project quickly
- **Code Analysis & Refactoring Techniques**
- **Top-Level Function Search**
- Use grep pattern matching to find all exported functions across the codebase
- Command: `grep -E "export (function|const) \w+|function \w+\(|const \w+ = \(|module\.exports" --include="*.js" -r ./`
- Benefits:
- Quickly identify all public API functions without reading implementation details
- Compare functions between files during refactoring (e.g., monolithic to modular structure)
- Verify all expected functions exist in refactored modules
- Identify duplicate functionality or naming conflicts
- Usage examples:
- When migrating from `scripts/dev.js` to modular structure: `grep -E "function \w+\(" scripts/dev.js`
- Check function exports in a directory: `grep -E "export (function|const)" scripts/modules/`
- Find potential naming conflicts: `grep -E "function (get|set|create|update)\w+\(" -r ./`
- Variations:
- Add `-n` flag to include line numbers
- Add `--include="*.ts"` to filter by file extension
- Use with `| sort` to alphabetize results
- Integration with refactoring workflow:
- Start by mapping all functions in the source file
- Create target module files based on function grouping
- Verify all functions were properly migrated
- Check for any unintentional duplications or omissions
---
## WINDSURF_RULES
description: Guidelines for creating and maintaining Windsurf rules to ensure consistency and effectiveness.
globs: .windsurfrules
filesToApplyRule: .windsurfrules
alwaysApply: true
---
The below describes how you should be structuring new rule sections in this document.
- **Required Rule Structure:**
```markdown
---
description: Clear, one-line description of what the rule enforces
globs: path/to/files/*.ext, other/path/**/*
alwaysApply: boolean
---
- **Main Points in Bold**
- Sub-points with details
- Examples and explanations
```
- **Section References:**
- Use `ALL_CAPS_SECTION` to reference files
- Example: `WINDSURF_RULES`
- **Code Examples:**
- Use language-specific code blocks
```typescript
// ✅ DO: Show good examples
const goodExample = true;
// ❌ DON'T: Show anti-patterns
const badExample = false;
```
- **Rule Content Guidelines:**
- Start with high-level overview
- Include specific, actionable requirements
- Show examples of correct implementation
- Reference existing code when possible
- Keep rules DRY by referencing other rules
- **Rule Maintenance:**
- Update rules when new patterns emerge
- Add examples from actual codebase
- Remove outdated patterns
- Cross-reference related rules
- **Best Practices:**
- Use bullet points for clarity
- Keep descriptions concise
- Include both DO and DON'T examples
- Reference actual code over theoretical examples
- Use consistent formatting across rules
---
## SELF_IMPROVE
description: Guidelines for continuously improving this rules document based on emerging code patterns and best practices.
globs: **/\*
filesToApplyRule: **/\*
alwaysApply: true
---
- **Rule Improvement Triggers:**
- New code patterns not covered by existing rules
- Repeated similar implementations across files
- Common error patterns that could be prevented
- New libraries or tools being used consistently
- Emerging best practices in the codebase
- **Analysis Process:**
- Compare new code with existing rules
- Identify patterns that should be standardized
- Look for references to external documentation
- Check for consistent error handling patterns
- Monitor test patterns and coverage
- **Rule Updates:**
- **Add New Rules When:**
- A new technology/pattern is used in 3+ files
- Common bugs could be prevented by a rule
- Code reviews repeatedly mention the same feedback
- New security or performance patterns emerge
- **Modify Existing Rules When:**
- Better examples exist in the codebase
- Additional edge cases are discovered
- Related rules have been updated
- Implementation details have changed
- **Example Pattern Recognition:**
```typescript
// If you see repeated patterns like:
const data = await prisma.user.findMany({
select: { id: true, email: true },
where: { status: "ACTIVE" },
});
// Consider adding a PRISMA section in the .windsurfrules:
// - Standard select fields
// - Common where conditions
// - Performance optimization patterns
```
- **Rule Quality Checks:**
- Rules should be actionable and specific
- Examples should come from actual code
- References should be up to date
- Patterns should be consistently enforced
- **Continuous Improvement:**
- Monitor code review comments
- Track common development questions
- Update rules after major refactors
- Add links to relevant documentation
- Cross-reference related rules
- **Rule Deprecation:**
- Mark outdated patterns as deprecated
- Remove rules that no longer apply
- Update references to deprecated rules
- Document migration paths for old patterns
- **Documentation Updates:**
- Keep examples synchronized with code
- Update references to external docs
- Maintain links between related rules
- Document breaking changes
Follow WINDSURF_RULES for proper rule formatting and structure of windsurf rule sections.

542
packages/lab-tool/README.md Normal file
View file

@ -0,0 +1,542 @@
# Lab Package - Home Lab Infrastructure Management in Guile Scheme
A comprehensive home lab management tool implemented in GNU Guile Scheme, providing infrastructure monitoring, deployment automation, and health checking capabilities for NixOS-based systems.
## Table of Contents
- [Overview](#overview)
- [Background on GNU Guile](#background-on-gnu-guile)
- [Code Architecture](#code-architecture)
- [Core Modules](#core-modules)
- [Installation & Setup](#installation--setup)
- [Usage Examples](#usage-examples)
- [API Reference](#api-reference)
- [Development](#development)
- [Integration](#integration)
## Overview
The Lab package is a sophisticated infrastructure management tool designed for home lab environments running NixOS. It provides:
- **Infrastructure Status Monitoring**: Real-time status checking across multiple machines
- **Deployment Automation**: Safe NixOS configuration deployment with multiple strategies
- **Health Checking**: Comprehensive system health validation
- **SSH-based Operations**: Secure remote command execution and file operations
- **Error Handling**: Robust error reporting and recovery mechanisms
- **MCP Integration**: Model Context Protocol server for IDE integration
## Background on GNU Guile
### What is GNU Guile?
GNU Guile is the official extension language for the GNU Project and is an implementation of the Scheme programming language. Scheme is a minimalist dialect of Lisp, designed for clarity and elegance.
### Why Guile for Infrastructure Management?
**Advantages over traditional shell scripting:**
1. **Rich Data Structures**: Native support for complex data manipulation with lists, association lists, and records
2. **Error Handling**: Sophisticated condition system for graceful error recovery
3. **Modularity**: Built-in module system for organizing large codebases
4. **Functional Programming**: Immutable data structures and pure functions reduce bugs
5. **REPL-Driven Development**: Interactive development and debugging capabilities
6. **Extensibility**: Easy integration with C libraries and external tools
**Scheme Language Features:**
- **S-expressions**: Code as data, enabling powerful metaprogramming
- **First-class functions**: Functions can be passed as arguments and returned as values
- **Pattern matching**: Elegant control flow with the `match` macro
- **Tail call optimization**: Efficient recursive algorithms
- **Hygienic macros**: Safe code generation and transformation
**Example of Scheme expressiveness:**
```scheme
;; Traditional imperative approach (pseudo-code)
machines = get_machines()
results = []
for machine in machines:
status = check_machine(machine)
results.append((machine, status))
;; Functional Scheme approach
(map (lambda (machine)
`(,machine . ,(check-machine machine)))
(get-machines))
```
## Code Architecture
The lab package follows a modular architecture with clear separation of concerns:
```
packages/lab/
├── core.scm # Core infrastructure operations
├── deployment.scm # Deployment strategies and execution
├── machines.scm # Machine management and discovery
├── monitoring.scm # Health checking and status monitoring
└── utils/
├── config.scm # Configuration management
├── logging.scm # Structured logging system
└── ssh.scm # SSH operations and connectivity
```
### Design Principles
1. **Functional Core, Imperative Shell**: Pure functions for business logic, side effects at boundaries
2. **Data-Driven Design**: Configuration and machine definitions as data structures
3. **Composable Operations**: Small, focused functions that can be combined
4. **Error Boundaries**: Comprehensive error handling with informative messages
5. **Testable Components**: Functions designed for easy unit testing
## Core Modules
### core.scm - Main Infrastructure Operations
The core module provides the primary interface for infrastructure management:
```scheme
(define-module (lab core)
#:export (get-infrastructure-status
check-system-health
update-flake
validate-environment
execute-nixos-rebuild))
```
#### Key Functions
**`get-infrastructure-status`**
- Retrieves comprehensive status of all machines or a specific machine
- Returns structured data including connectivity, services, and system metrics
- Supports both local and remote machine checking
**`check-system-health`**
- Performs comprehensive health checks including:
- SSH connectivity testing
- Disk space validation (< 90% usage)
- System load monitoring (< 5.0 load average)
- Critical service status (sshd, etc.)
- Network connectivity verification
**`execute-nixos-rebuild`**
- Safe NixOS deployment with comprehensive error handling
- Supports multiple deployment modes (switch, boot, test)
- Handles both local and remote deployments
- Includes dry-run capabilities for testing
#### Code Analysis Example
```scheme
;; Infrastructure status checking with error handling
(define (get-infrastructure-status . args)
"Get status of all machines or specific machine if provided"
(let ((target-machine (if (null? args) #f (car args)))
(machines (if (null? args)
(get-all-machines)
(list (car args)))))
(log-info "Checking infrastructure status...")
(map (lambda (machine-name)
(let ((start-time (current-time)))
(log-debug "Checking ~a..." machine-name)
;; Gather machine information with error isolation
(let* ((ssh-config (get-ssh-config machine-name))
(is-local (and ssh-config (assoc-ref ssh-config 'is-local)))
(connection-status (test-ssh-connection machine-name))
(services-status (if connection-status
(get-machine-services-status machine-name)
'()))
(system-info (if connection-status
(get-machine-system-info machine-name)
#f))
(elapsed (- (current-time) start-time)))
;; Return structured status data
`((machine . ,machine-name)
(type . ,(if is-local 'local 'remote))
(connection . ,(if connection-status 'online 'offline))
(services . ,services-status)
(system . ,system-info)
(check-time . ,elapsed)))))
machines)))
```
This code demonstrates several Scheme/Guile idioms:
1. **Optional Arguments**: Using `(args)` with `null?` check for flexible function signatures
2. **Let Bindings**: Structured variable binding with `let*` for dependent calculations
3. **Conditional Expressions**: Using `if` expressions for control flow
4. **Association Lists**: Structured data representation with `assoc-ref`
5. **Quasiquote/Unquote**: Building structured data with `` `((key . ,value))` syntax
### utils/ssh.scm - SSH Operations
Handles all SSH-related operations with comprehensive error handling:
```scheme
(define (test-ssh-connection machine-name)
(let ((ssh-config (get-ssh-config machine-name)))
(if (not ssh-config)
(begin
(log-error "No SSH configuration found for ~a" machine-name)
#f)
(if (assoc-ref ssh-config 'is-local)
(begin
(log-debug "Machine ~a is local, skipping SSH test" machine-name)
#t)
;; Remote SSH testing logic
(let ((hostname (assoc-ref ssh-config 'hostname))
(ssh-alias (assoc-ref ssh-config 'ssh-alias)))
(catch #t
(lambda ()
(let* ((test-cmd (if ssh-alias
(format #f "ssh -o ConnectTimeout=5 -o BatchMode=yes ~a echo OK" ssh-alias)
(format #f "ssh -o ConnectTimeout=5 -o BatchMode=yes ~a echo OK" hostname)))
(port (open-pipe* OPEN_READ "/bin/sh" "-c" test-cmd))
(output (get-string-all port))
(status (close-pipe port)))
(zero? status)))
(lambda (key . args)
(log-warn "SSH connection test failed for ~a: ~a" machine-name key)
#f)))))))
```
### utils/config.scm - Configuration Management
Provides structured configuration handling:
```scheme
(define default-config
`((homelab-root . "/home/geir/Home-lab")
(machines . ((congenital-optimist
(type . local)
(hostname . "localhost")
(services . (workstation development)))
(sleeper-service
(type . remote)
(hostname . "sleeper-service.tail807ea.ts.net")
(ssh-alias . "admin-sleeper")
(services . (nfs zfs storage)))
;; Additional machines...
))))
```
### utils/logging.scm - Structured Logging
Comprehensive logging system with color coding and log levels:
```scheme
;; ANSI color codes for terminal output
(define color-codes
'((reset . "\x1b[0m")
(bold . "\x1b[1m")
(red . "\x1b[31m")
(green . "\x1b[32m")
(yellow . "\x1b[33m")
(blue . "\x1b[34m")
(magenta . "\x1b[35m")
(cyan . "\x1b[36m")))
;; Log level hierarchy
(define log-levels
'((debug . 0)
(info . 1)
(warn . 2)
(error . 3)))
```
## Installation & Setup
### Prerequisites
1. **GNU Guile**: Version 3.0 or later
2. **NixOS System**: Target machines running NixOS
3. **SSH Access**: Configured SSH keys for remote machines
4. **Required Guile Libraries**:
- `guile-ssh` - SSH operations
- `guile-json` - JSON data handling
- `srfi-1`, `srfi-19` - Standard Scheme libraries
### NixOS Integration
Add to your NixOS configuration:
```nix
environment.systemPackages = with pkgs; [
guile
guile-ssh
guile-json-4
];
```
### Environment Setup
```bash
# Set environment variables
export HOMELAB_ROOT="/path/to/your/home-lab"
export GUILE_LOAD_PATH="/path/to/packages:$GUILE_LOAD_PATH"
# Test installation
guile -c "(use-modules (lab core)) (display \"Lab package loaded successfully\\n\")"
```
## Usage Examples
### Basic Infrastructure Status
```scheme
#!/usr/bin/env guile
!#
(use-modules (lab core))
;; Check all machines
(define all-status (get-infrastructure-status))
(display all-status)
;; Check specific machine
(define machine-status (get-infrastructure-status "sleeper-service"))
(display machine-status)
```
### Health Checking
```scheme
(use-modules (lab core))
;; Comprehensive health check
(define health-report (check-system-health "grey-area"))
(for-each (lambda (check)
(let ((name (car check))
(result (cdr check)))
(format #t "~a: ~a~%" name
(assoc-ref result 'status))))
health-report)
```
### Deployment Operations
```scheme
(use-modules (lab core))
;; Validate environment before deployment
(if (validate-environment)
(begin
;; Update flake inputs
(update-flake '((dry-run . #f)))
;; Deploy to machine
(execute-nixos-rebuild "sleeper-service" "switch"
'((dry-run . #f))))
(display "Environment validation failed\n"))
```
### Interactive REPL Usage
```sh
;; Start Guile REPL
$ guile -L packages
;; Load modules interactively
scheme@(guile-user)> (use-modules (lab core) (utils logging))
;; Set debug logging
scheme@(guile-user)> (set-log-level! 'debug)
;; Check machine status interactively
scheme@(guile-user)> (get-infrastructure-status "congenital-optimist")
;; Test health checks
scheme@(guile-user)> (check-system-health "grey-area")
```
## API Reference
### Infrastructure Management
#### `(get-infrastructure-status [machine-name])`
Returns structured status information for all machines or specific machine.
**Parameters:**
- `machine-name` (optional): Specific machine to check
**Returns:**
Association list with machine status including:
- `machine`: Machine name
- `type`: 'local or 'remote
- `connection`: 'online or 'offline
- `services`: List of service statuses
- `system`: System information (uptime, load, memory, disk)
- `check-time`: Time taken for status check
#### `(check-system-health machine-name)`
Performs comprehensive health validation.
**Parameters:**
- `machine-name`: Target machine name
**Returns:**
List of health check results with status ('pass, 'fail, 'error) and details.
#### `(execute-nixos-rebuild machine-name mode options)`
Executes NixOS rebuild with error handling.
**Parameters:**
- `machine-name`: Target machine
- `mode`: Rebuild mode ("switch", "boot", "test")
- `options`: Configuration options (dry-run, etc.)
**Returns:**
Boolean indicating success/failure.
### Utility Functions
#### `(validate-environment)`
Validates home lab environment configuration.
#### `(update-flake options)`
Updates Nix flake inputs with optional dry-run.
### Configuration Access
#### `(get-machine-config machine-name)`
Retrieves machine-specific configuration.
#### `(get-all-machines)`
Returns list of all configured machines.
#### `(get-ssh-config machine-name)`
Gets SSH configuration for machine.
## Development
### Code Style Guidelines
1. **Naming Conventions**:
- Use kebab-case for functions and variables
- Predicates end with `?` (e.g., `machine-online?`)
- Mutating procedures end with `!` (e.g., `set-log-level!`)
2. **Module Organization**:
- One module per file
- Clear export lists
- Minimal external dependencies
3. **Error Handling**:
- Use `catch` for exception handling
- Provide informative error messages
- Log errors with appropriate levels
4. **Documentation**:
- Docstrings for all public functions
- Inline comments for complex logic
- Example usage in comments
### Testing
```scheme
;; Example test structure
(use-modules (srfi srfi-64) ; Testing framework
(lab core))
(test-begin "infrastructure-tests")
(test-assert "validate-environment"
(validate-environment))
(test-assert "machine-connectivity"
(test-ssh-connection "congenital-optimist"))
(test-end "infrastructure-tests")
```
### REPL-Driven Development
Guile's strength lies in interactive development:
```scheme
;; Reload modules during development
scheme@(guile-user)> (reload-module (resolve-module '(lab core)))
;; Test functions interactively
scheme@(guile-user)> (get-machine-config "sleeper-service")
;; Debug with modified log levels
scheme@(guile-user)> (set-log-level! 'debug)
scheme@(guile-user)> (test-ssh-connection "grey-area")
```
## Integration
### Model Context Protocol (MCP) Server
The lab package includes MCP server capabilities for IDE integration, providing:
- **Tools**: Infrastructure management operations
- **Resources**: Machine configurations and status
- **Prompts**: Common operational workflows
### VS Code Integration
Through the MCP server, the lab package integrates with VS Code to provide:
- Real-time infrastructure status
- Contextual deployment suggestions
- Configuration validation
- Automated documentation generation
### Command Line Interface
The package provides a CLI wrapper for common operations:
```bash
# Using the Guile-based CLI
./home-lab-tool.scm status
./home-lab-tool.scm deploy sleeper-service
./home-lab-tool.scm health-check grey-area
```
### Future Enhancements
1. **Web Dashboard**: Real-time monitoring interface
2. **Metrics Collection**: Prometheus integration
3. **Automated Recovery**: Self-healing capabilities
4. **Configuration Validation**: Pre-deployment checks
5. **Rollback Automation**: Automatic failure recovery
---
## Contributing
1. Follow the established code style
2. Add tests for new functionality
3. Update documentation
4. Test with multiple machine configurations
## License
This project follows the same license as the Home Lab repository.
---
**Note**: This tool represents a migration from traditional Bash scripting to a more robust, functional programming approach using GNU Guile Scheme. The benefits include better error handling, structured data management, and more maintainable code for complex infrastructure management tasks.

View file

@ -0,0 +1,107 @@
# K.I.S.S Refactoring Summary
## Applied Principles
### 1. Modularization (Keep It Simple, Keep It Small)
- **Before**: Large monolithic modules (138+ lines)
- **After**: Small focused modules (each under 50 lines)
- **Example**: SSH module split into 5 specialized modules
### 2. Single Responsibility Principle (UNIX Philosophy: Do One Thing Well)
- **connection-test.scm**: Only SSH connectivity testing
- **remote-command.scm**: Only remote command execution
- **file-copy.scm**: Only file transfer operations
- **retry.scm**: Only retry logic
- **context.scm**: Only connection context management
### 3. Functional Programming Patterns
- **Pure Functions First**: All core logic implemented as pure functions
- **Immutable Data**: Configuration and data structures remain immutable
- **Separation of Concerns**: Pure functions separated from side effects
### 4. Function-Level Modularity
Each module exports both:
- **Pure functions**: For testing, composition, and functional programming
- **Impure wrappers**: For convenience and logging
## Module Structure
```
utils/
├── ssh/
│ ├── connection-test.scm # Pure SSH connectivity testing
│ ├── remote-command.scm # Pure command execution logic
│ ├── file-copy.scm # Pure file transfer operations
│ ├── retry.scm # Pure retry logic with backoff
│ └── context.scm # Connection context management
├── config/
│ ├── defaults.scm # Pure data: default configuration
│ ├── loader.scm # File I/O operations
│ ├── accessor.scm # Pure configuration access functions
│ └── state.scm # Mutable state management
├── logging/
│ ├── format.scm # Pure formatting and color codes
│ ├── level.scm # Pure log level management
│ ├── state.scm # Mutable log level state
│ ├── output.scm # Pure output formatting
│ ├── core.scm # Main logging functions
│ └── spinner.scm # Progress indication
└── json/
├── parse.scm # Pure JSON parsing
├── serialize.scm # Pure JSON serialization
├── file-io.scm # File I/O with pure/impure versions
├── validation.scm # Pure schema validation
├── manipulation.scm # Pure object manipulation
└── pretty-print.scm # Output formatting
```
## Benefits Achieved
### 1. Testability
- Pure functions can be tested in isolation
- No side effects to mock or manage
- Clear input/output contracts
### 2. Composability
- Small functions can be easily combined
- Pure functions enable functional composition
- Reusable building blocks
### 3. Maintainability
- Single responsibility makes modules easy to understand
- Changes are localized to specific modules
- Clear separation between pure and impure code
### 4. Code Reuse
- Pure functions can be reused across different contexts
- Both pure and impure versions available
- Facade modules provide convenient interfaces
## Usage Examples
### Pure Function Composition
```scheme
;; Test connection and get config in one go
(let ((ssh-config (get-ssh-config-pure config "machine-name")))
(if (test-ssh-connection-pure ssh-config)
(run-remote-command-pure ssh-config "uptime" '())
#f))
```
### Convenient Impure Wrappers
```scheme
;; Same operation with logging and error handling
(with-ssh-connection "machine-name"
(lambda () (run-remote-command "machine-name" "uptime")))
```
### Functional Pipeline
```scheme
;; Pure validation pipeline
(let* ((config (load-config-from-file "config.json"))
(valid? (validate-json-schema config machine-schema))
(machines (if valid? (get-all-machines-pure config) '())))
machines)
```
This refactoring transforms the codebase from monolithic modules into a collection of small, focused, composable functions that follow functional programming principles while maintaining practical usability.

View file

@ -0,0 +1,32 @@
;; lab/core/config.scm - Configuration functionality
(define-module (lab core config)
#:use-module (ice-9 format)
#:export (get-all-machines
get-machine-config
get-ssh-config
get-homelab-root
option-ref))
(define (option-ref options key default)
"Get option value from options alist with default"
(let ((value (assoc-ref options key)))
(if value value default)))
(define (get-all-machines)
"Get list of all machines"
'(grey-area sleeper-service congenital-optimist reverse-proxy))
(define (get-machine-config machine-name)
"Get configuration for a machine"
`((services . (systemd ssh))
(type . server)))
(define (get-ssh-config machine-name)
"Get SSH configuration for a machine"
`((hostname . ,(symbol->string machine-name))
(is-local . #f)))
(define (get-homelab-root)
"Get home lab root directory"
"/home/geir/Home-lab")

View file

@ -0,0 +1,75 @@
;; lab/core/health.scm - Health check functionality
(define-module (lab core health)
#:use-module (ice-9 rdelim)
#:use-module (srfi srfi-1)
#:use-module (lab core logging)
#:use-module (lab core ssh)
#:export (check-system-health
check-disk-space
check-system-load
check-critical-services
check-network-connectivity))
(define (check-system-health machine-name)
"Perform comprehensive health check on a machine"
(log-info "Performing health check on ~a..." machine-name)
(let ((health-checks
'(("connectivity" . test-ssh-connection)
("disk-space" . check-disk-space)
("system-load" . check-system-load)
("critical-services" . check-critical-services)
("network" . check-network-connectivity))))
(map (lambda (check-pair)
(let ((check-name (car check-pair))
(check-proc (cdr check-pair)))
(log-debug "Running ~a check..." check-name)
(catch #t
(lambda ()
(let ((result (check-proc machine-name)))
`(,check-name . ((status . ,(if result 'pass 'fail))
(result . ,result)))))
(lambda (key . args)
(log-warn "Health check ~a failed: ~a" check-name key)
`(,check-name . ((status . error)
(error . ,key)))))))
health-checks)))
(define (check-disk-space machine-name)
"Check if disk space is below critical threshold"
(call-with-values
(lambda () (run-remote-command machine-name "df / | tail -1 | awk '{print $5}' | sed 's/%//'"))
(lambda (success output)
(if success
(let ((usage (string->number (string-trim-right output))))
(< usage 90)) ; Pass if usage < 90%
#f))))
(define (check-system-load machine-name)
"Check if system load is reasonable"
(call-with-values
(lambda () (run-remote-command machine-name "cat /proc/loadavg | cut -d' ' -f1"))
(lambda (success output)
(if success
(let ((load (string->number (string-trim-right output))))
(< load 5.0)) ; Pass if load < 5.0
#f))))
(define (check-critical-services machine-name)
"Check that critical services are running"
(let ((critical-services '("sshd")))
(every (lambda (service)
(call-with-values
(lambda () (run-remote-command machine-name "systemctl is-active" service))
(lambda (success output)
(and success (string=? (string-trim-right output) "active")))))
critical-services)))
(define (check-network-connectivity machine-name)
"Check basic network connectivity"
(call-with-values
(lambda () (run-remote-command machine-name "ping -c 1 -W 5 8.8.8.8 > /dev/null 2>&1; echo $?"))
(lambda (success output)
(and success (string=? (string-trim-right output) "0")))))

View file

@ -0,0 +1,29 @@
;; lab/core/logging.scm - Logging functionality
(define-module (lab core logging)
#:use-module (ice-9 format)
#:export (log-info
log-debug
log-success
log-error
log-warn))
(define (log-info format-str . args)
"Log info message"
(apply format #t (string-append "[INFO] " format-str "~%") args))
(define (log-debug format-str . args)
"Log debug message"
(apply format #t (string-append "[DEBUG] " format-str "~%") args))
(define (log-success format-str . args)
"Log success message"
(apply format #t (string-append "[SUCCESS] " format-str "~%") args))
(define (log-error format-str . args)
"Log error message"
(apply format #t (string-append "[ERROR] " format-str "~%") args))
(define (log-warn format-str . args)
"Log warning message"
(apply format #t (string-append "[WARN] " format-str "~%") args))

View file

@ -0,0 +1,24 @@
;; lab/core/ssh.scm - SSH operations
(define-module (lab core ssh)
#:use-module (ice-9 format)
#:use-module (ice-9 popen)
#:use-module (ice-9 rdelim)
#:use-module (ice-9 textual-ports)
#:export (test-ssh-connection
run-remote-command))
(define (test-ssh-connection machine-name)
"Test SSH connection to machine"
(zero? (system (format #f "ssh -o ConnectTimeout=5 -o BatchMode=yes ~a exit 2>/dev/null" machine-name))))
(define (run-remote-command machine-name command . args)
"Run command on remote machine via SSH"
(let* ((full-command (if (null? args)
command
(string-join (cons command args) " ")))
(ssh-command (format #f "ssh ~a '~a'" machine-name full-command))
(port (open-input-pipe ssh-command))
(output (read-string port))
(status (close-pipe port)))
(values (zero? status) output)))

View file

@ -0,0 +1,84 @@
;; lab/core/status.scm - Infrastructure status functionality
(define-module (lab core status)
#:use-module (ice-9 rdelim)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-19)
#:use-module (lab core logging)
#:use-module (lab core config)
#:use-module (lab core ssh)
#:export (get-infrastructure-status
get-machine-services-status
get-machine-system-info))
(define (get-infrastructure-status . args)
"Get status of all machines or specific machine if provided"
(let ((target-machine (if (null? args) #f (car args)))
(machines (if (null? args)
(get-all-machines)
(list (car args)))))
(log-info "Checking infrastructure status...")
(map (lambda (machine-name)
(let ((start-time (current-time)))
(log-debug "Checking ~a..." machine-name)
(let* ((ssh-config (get-ssh-config machine-name))
(is-local (and ssh-config (assoc-ref ssh-config 'is-local)))
(connection-status (test-ssh-connection machine-name))
(services-status (if connection-status
(get-machine-services-status machine-name)
'()))
(system-info (if connection-status
(get-machine-system-info machine-name)
#f))
(elapsed (- (current-time) start-time)))
`((machine . ,machine-name)
(type . ,(if is-local 'local 'remote))
(connection . ,(if connection-status 'online 'offline))
(services . ,services-status)
(system . ,system-info)
(check-time . ,elapsed)))))
machines)))
(define (get-machine-services-status machine-name)
"Check status of services on a machine"
(let ((machine-config (get-machine-config machine-name)))
(if machine-config
(let ((services (assoc-ref machine-config 'services)))
(if services
(map (lambda (service)
(call-with-values
(lambda () (run-remote-command machine-name
"systemctl is-active"
(symbol->string service)))
(lambda (success output)
`(,service . ,(if success
(string-trim-right output)
"unknown")))))
services)
'()))
'())))
(define (get-machine-system-info machine-name)
"Get basic system information from a machine"
(let ((info-commands
'(("uptime" "uptime -p")
("load" "cat /proc/loadavg | cut -d' ' -f1-3")
("memory" "free -h | grep Mem | awk '{print $3\"/\"$2}'")
("disk" "df -h / | tail -1 | awk '{print $5}'")
("kernel" "uname -r"))))
(fold (lambda (cmd-pair acc)
(let ((key (car cmd-pair))
(command (cadr cmd-pair)))
(call-with-values
(lambda () (run-remote-command machine-name command))
(lambda (success output)
(if success
(assoc-set! acc (string->symbol key) (string-trim-right output))
acc)))))
'()
info-commands)))

View file

@ -0,0 +1,12 @@
;; lab/core/utils.scm - Utility functions
(define-module (lab core utils)
#:use-module (ice-9 format)
#:export (with-spinner))
(define (with-spinner message proc)
"Execute procedure with spinner (stub implementation)"
(display (format #f "~a..." message))
(let ((result (proc)))
(display " done.\n")
result))

View file

@ -0,0 +1,3 @@
# Default.nix for lab-tool
# Provides the lab-tool package for inclusion in other Nix expressions
(import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/nixos-unstable.tar.gz") {}).callPackage ./. {}

View file

@ -0,0 +1,109 @@
;; lab/core/deployment.scm - Deployment functionality
(define-module (lab core deployment)
#:use-module (ice-9 format)
#:use-module (ice-9 popen)
#:use-module (ice-9 textual-ports)
#:use-module (lab core logging)
#:use-module (lab core config)
#:use-module (lab core utils)
#:export (update-flake
validate-environment
execute-nixos-rebuild))
(define (update-flake options)
"Update flake inputs in the home lab repository"
(let ((homelab-root (get-homelab-root))
(dry-run (option-ref options 'dry-run #f)))
(log-info "Updating flake inputs...")
(if dry-run
(begin
(log-info "DRY RUN: Would execute: nix flake update")
#t)
(let* ((update-cmd (format #f "cd ~a && nix flake update" homelab-root))
(port (open-pipe* OPEN_READ "/bin/sh" "-c" update-cmd))
(output (get-string-all port))
(status (close-pipe port)))
(if (zero? status)
(begin
(log-success "Flake inputs updated successfully")
(log-debug "Update output: ~a" output)
#t)
(begin
(log-error "Flake update failed (exit: ~a)" status)
(log-error "Error output: ~a" output)
#f))))))
(define (validate-environment)
"Validate that the home lab environment is properly configured"
(log-info "Validating home lab environment...")
(let ((checks
`(("homelab-root" . ,(lambda () (file-exists? (get-homelab-root))))
("flake-file" . ,(lambda () (file-exists? (string-append (get-homelab-root) "/flake.nix"))))
("ssh-config" . ,(lambda () (file-exists? (string-append (getenv "HOME") "/.ssh/config"))))
("nix-command" . ,(lambda () (zero? (system "which nix > /dev/null 2>&1"))))
("machines-config" . ,(lambda () (not (null? (get-all-machines))))))))
(let ((results (map (lambda (check-pair)
(let ((check-name (car check-pair))
(check-proc (cdr check-pair)))
(let ((result (check-proc)))
(if result
(log-success "✓ ~a" check-name)
(log-error "✗ ~a" check-name))
`(,check-name . ,result))))
checks)))
(let ((failures (filter (lambda (result) (not (cdr result))) results)))
(if (null? failures)
(begin
(log-success "Environment validation passed")
#t)
(begin
(log-error "Environment validation failed: ~a" (map car failures))
#f))))))
(define (execute-nixos-rebuild machine-name mode options)
"Execute nixos-rebuild on a machine with comprehensive error handling"
(let ((homelab-root (get-homelab-root))
(dry-run (option-ref options 'dry-run #f))
(ssh-config (get-ssh-config machine-name)))
(if (not ssh-config)
(begin
(log-error "No SSH configuration for machine: ~a" machine-name)
#f)
(let* ((is-local (assoc-ref ssh-config 'is-local))
(flake-ref (format #f "~a#~a" homelab-root machine-name))
(rebuild-cmd (if is-local
(format #f "sudo nixos-rebuild ~a --flake ~a" mode flake-ref)
(format #f "nixos-rebuild ~a --flake ~a --target-host ~a --use-remote-sudo"
mode flake-ref (assoc-ref ssh-config 'hostname)))))
(log-info "Executing nixos-rebuild for ~a (mode: ~a)" machine-name mode)
(log-debug "Command: ~a" rebuild-cmd)
(if dry-run
(begin
(log-info "DRY RUN: Would execute: ~a" rebuild-cmd)
#t)
(with-spinner
(format #f "Rebuilding ~a" machine-name)
(lambda ()
(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 "nixos-rebuild completed successfully for ~a" machine-name)
(log-debug "Build output: ~a" output)
#t)
(begin
(log-error "nixos-rebuild failed for ~a (exit: ~a)" machine-name status)
(log-error "Error output: ~a" output)
#f))))))))

61
packages/lab-tool/flake.lock generated Normal file
View file

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1749794982,
"narHash": "sha256-Kh9K4taXbVuaLC0IL+9HcfvxsSUx8dPB5s5weJcc9pc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ee930f9755f58096ac6e8ca94a1887e0534e2d81",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

133
packages/lab-tool/flake.nix Normal file
View file

@ -0,0 +1,133 @@
{
description = "Home Lab Tool - Guile implementation with MCP server";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = {
self,
nixpkgs,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages.${system};
# Guile libraries we need
guileLibs = with pkgs; [
guile_3_0
guile-ssh
guile-json
guile-git
guile-gcrypt
];
# Build the Guile lab tool
lab-tool = pkgs.stdenv.mkDerivation {
pname = "lab-tool";
version = "0.1.0";
src = ./.;
buildInputs = guileLibs;
nativeBuildInputs = [pkgs.makeWrapper];
buildPhase = ''
# Compile Guile modules for better performance
mkdir -p $out/share/guile/site/3.0
cp -r . $out/share/guile/site/3.0/lab-tool/
# Compile .scm files to .go files
for file in $(find . -name "*.scm"); do
echo "Compiling $file"
guild compile -L . -o $out/share/guile/site/3.0/''${file%.scm}.go $file || true
done
'';
installPhase = ''
mkdir -p $out/bin
# Create the main lab executable
cat > $out/bin/lab << EOF
#!/usr/bin/env bash
export GUILE_LOAD_PATH="$out/share/guile/site/3.0/lab-tool:\$GUILE_LOAD_PATH"
export GUILE_LOAD_COMPILED_PATH="$out/share/guile/site/3.0/lab-tool:\$GUILE_LOAD_COMPILED_PATH"
exec ${pkgs.guile_3_0}/bin/guile "$out/share/guile/site/3.0/lab-tool/main.scm" "\$@"
EOF
chmod +x $out/bin/lab
# Create MCP server executable
cat > $out/bin/lab-mcp-server << EOF
#!/usr/bin/env bash
export GUILE_LOAD_PATH="$out/share/guile/site/3.0/lab-tool:\$GUILE_LOAD_PATH"
export GUILE_LOAD_COMPILED_PATH="$out/share/guile/site/3.0/lab-tool:\$GUILE_LOAD_COMPILED_PATH"
exec ${pkgs.guile_3_0}/bin/guile -L "$out/share/guile/site/3.0/lab-tool" -c "(use-modules (mcp server)) (run-mcp-server)"
EOF
chmod +x $out/bin/lab-mcp-server
# Wrap executables with proper environment
wrapProgram $out/bin/lab \
--prefix PATH : ${pkgs.lib.makeBinPath [pkgs.openssh pkgs.git pkgs.nixos-rebuild]}
wrapProgram $out/bin/lab-mcp-server \
--prefix PATH : ${pkgs.lib.makeBinPath [pkgs.openssh pkgs.git pkgs.nixos-rebuild]}
'';
meta = with pkgs.lib; {
description = "Home Lab Tool - Guile implementation with MCP integration";
license = licenses.mit;
platforms = platforms.linux;
maintainers = ["geir@home-lab"];
};
};
in {
packages = {
default = lab-tool;
lab-tool = lab-tool;
};
devShells.default = pkgs.mkShell {
buildInputs =
guileLibs
++ (with pkgs; [
# Development tools
emacs
# System tools for lab operations
openssh
git
nixos-rebuild
# Optional for advanced features
sqlite
redis
]);
shellHook = ''
echo "🧪 Home Lab Tool Development Environment"
echo "Available commands:"
echo " guile - Start Guile REPL"
echo " guild compile <file> - Compile Guile modules"
echo " ./main.scm help - Test the lab tool"
echo ""
echo "Module path: $(pwd)"
export GUILE_LOAD_PATH="$(pwd):$GUILE_LOAD_PATH"
export LAB_DEV_MODE=1
'';
};
apps = {
default = flake-utils.lib.mkApp {
drv = lab-tool;
name = "lab";
};
mcp-server = flake-utils.lib.mkApp {
drv = lab-tool;
name = "lab-mcp-server";
};
};
});
}

187
packages/lab-tool/main.scm Normal file
View file

@ -0,0 +1,187 @@
#!/usr/bin/env guile
!#
;; Home Lab Tool - Main Entry Point
;; K.I.S.S Refactored Implementation
(add-to-load-path (dirname (current-filename)))
(use-modules (ice-9 match)
(ice-9 format)
(utils config)
(utils logging))
;; Initialize logging
(set-log-level! 'info)
;; Pure function: Command help text
(define (get-help-text)
"Pure function returning help text"
"Home Lab Tool - K.I.S.S Refactored Edition
USAGE: lab <command> [args...]
COMMANDS:
status Show infrastructure status
machines List all machines
deploy <machine> Deploy configuration to machine
ssh <machine> SSH to machine
test-modules Test modular implementation
help Show this help
EXAMPLES:
lab status
lab machines
lab deploy congenital-optimist
lab ssh sleeper-service
lab test-modules
This implementation follows K.I.S.S principles:
- Modular: Each module has single responsibility
- Functional: Pure functions separated from side effects
- Small: Individual modules under 50 lines
- Simple: One function does one thing well
")
;; Pure function: Format machine list
(define (format-machine-list machines)
"Pure function to format machine list for display"
(if (null? machines)
"No machines configured"
(string-join
(map (lambda (machine) (format #f " - ~a" machine)) machines)
"\n")))
;; Pure function: Format status info
(define (format-status-info machines config)
"Pure function to format infrastructure status"
(format #f "Infrastructure Status:
Total machines: ~a
Home lab root: ~a
~a"
(length machines)
(get-config-value '(homelab-root))
(format-machine-list machines)))
;; Command implementations
(define (cmd-status)
"Show infrastructure status"
(log-info "Checking infrastructure status...")
(let* ((machines (get-all-machines))
(config (get-current-config))
(status-text (format-status-info machines config)))
(display status-text)
(newline)
(log-success "Status check complete")))
(define (cmd-machines)
"List all configured machines"
(log-info "Listing configured machines...")
(let* ((machines (get-all-machines))
(machine-list (format-machine-list machines)))
(format #t "Configured Machines:\n~a\n" machine-list)
(log-success "Machine list complete")))
(define (cmd-deploy machine-name)
"Deploy configuration to machine"
(log-info "Deploying to machine: ~a" machine-name)
(if (validate-machine-name machine-name)
(begin
(log-info "Machine ~a is valid" machine-name)
(log-info "Deployment simulation complete (no actual deployment)")
(log-success "Deployment to ~a complete" 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)
"SSH to machine"
(log-info "Connecting to machine: ~a" machine-name)
(if (validate-machine-name machine-name)
(let ((ssh-config (get-ssh-config machine-name)))
(if ssh-config
(let ((hostname (assoc-ref ssh-config 'hostname))
(ssh-alias (assoc-ref ssh-config 'ssh-alias))
(is-local (assoc-ref ssh-config 'is-local)))
(if is-local
(log-info "Machine ~a is local - no SSH needed" machine-name)
(let ((target (or ssh-alias hostname)))
(log-info "Connecting to ~a via SSH..." target)
(system (format #f "ssh ~a" target)))))
(log-error "No SSH configuration found for ~a" machine-name)))
(log-error "Invalid machine: ~a" machine-name)))
(define (cmd-test-modules)
"Test the modular implementation"
(log-info "Testing modular implementation...")
;; Test pure functions
(use-modules (utils config accessor)
(utils logging format))
(let* ((config (get-current-config))
(machines (get-all-machines-pure config))
(blue-color (get-color 'blue)))
(format #t "\n=== Modular Implementation Test ===\n")
(format #t "Pure config access: ~a machines\n" (length machines))
(format #t "Pure color function: ~ablue text~a\n" blue-color (get-color 'reset))
(format #t "\n✅ All pure functions working correctly!\n\n")
(log-success "Modular implementation test complete")))
;; Main command dispatcher
(define (dispatch-command command args)
"Dispatch command with appropriate handler"
(match command
('help
(display (get-help-text)))
('status
(cmd-status))
('machines
(cmd-machines))
('deploy
(if (null? args)
(begin
(log-error "deploy command requires machine name")
(format #t "Usage: lab deploy <machine>\n"))
(cmd-deploy (car args))))
('ssh
(if (null? args)
(begin
(log-error "ssh command requires machine name")
(format #t "Usage: lab ssh <machine>\n"))
(cmd-ssh (car args))))
('test-modules
(cmd-test-modules))
(_
(log-error "Unknown command: ~a" command)
(format #t "Use 'lab help' for available commands\n"))))
;; Main entry point
(define (main args)
"Main entry point for lab tool"
(log-info "Home Lab Tool - K.I.S.S Refactored Edition")
(let* ((parsed-cmd (if (> (length args) 1) (cdr args) '("help")))
(command (string->symbol (car parsed-cmd)))
(cmd-args (cdr parsed-cmd)))
(catch #t
(lambda () (dispatch-command command cmd-args))
(lambda (key . error-args)
(log-error "Command failed: ~a ~a" key error-args)
(exit 1))))
(log-debug "Command execution complete"))
;; Run main function if script is executed directly
(when (and (defined? 'command-line) (not (null? (command-line))))
(main (command-line)))

View file

@ -5,17 +5,85 @@
#:use-module (ice-9 popen)
#:use-module (ice-9 rdelim)
#:use-module (ice-9 textual-ports)
#:use-module (ice-9 call-with-values)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-19)
#:use-module (utils logging)
#:use-module (utils config)
#:use-module (utils ssh)
#:export (get-infrastructure-status
check-system-health
update-flake
validate-environment
execute-nixos-rebuild))
execute-nixos-rebuild
check-network-connectivity
option-ref))
;; Simple option reference function
(define (option-ref options key default)
"Get option value from options alist with default"
(let ((value (assoc-ref options key)))
(if value value default)))
;; Stub logging functions (to be replaced with proper logging module)
(define (log-info format-str . args)
"Log info message"
(apply format #t (string-append "[INFO] " format-str "~%") args))
(define (log-debug format-str . args)
"Log debug message"
(apply format #t (string-append "[DEBUG] " format-str "~%") args))
(define (log-success format-str . args)
"Log success message"
(apply format #t (string-append "[SUCCESS] " format-str "~%") args))
(define (log-error format-str . args)
"Log error message"
(apply format #t (string-append "[ERROR] " format-str "~%") args))
(define (log-warn format-str . args)
"Log warning message"
(apply format #t (string-append "[WARN] " format-str "~%") args))
;; Stub configuration functions
(define (get-all-machines)
"Get list of all machines"
'(grey-area sleeper-service congenital-optimist reverse-proxy))
(define (get-machine-config machine-name)
"Get configuration for a machine"
`((services . (systemd ssh))
(type . server)))
(define (get-ssh-config machine-name)
"Get SSH configuration for a machine"
`((hostname . ,(symbol->string machine-name))
(is-local . #f)))
(define (get-homelab-root)
"Get home lab root directory"
"/home/geir/Home-lab")
;; Stub SSH functions
(define (test-ssh-connection machine-name)
"Test SSH connection to machine"
(zero? (system (format #f "ssh -o ConnectTimeout=5 -o BatchMode=yes ~a exit 2>/dev/null" machine-name))))
(define (run-remote-command machine-name command . args)
"Run command on remote machine via SSH"
(let* ((full-command (if (null? args)
command
(string-join (cons command args) " ")))
(ssh-command (format #f "ssh ~a '~a'" machine-name full-command))
(port (open-input-pipe ssh-command))
(output (read-string port))
(status (close-pipe port)))
(values (zero? status) output)))
;; Utility function for spinner (stub)
(define (with-spinner message proc)
"Execute procedure with spinner (stub implementation)"
(display (format #f "~a..." message))
(let ((result (proc)))
(display " done.\n")
result))
;; Get comprehensive infrastructure status
(define (get-infrastructure-status . args)
@ -58,13 +126,14 @@
(let ((services (assoc-ref machine-config 'services)))
(if services
(map (lambda (service)
(call-with-values (success output)
(run-remote-command machine-name
"systemctl is-active"
(symbol->string service)))
`(,service . ,(if success
(string-trim-right output)
"unknown"))))
(call-with-values
(lambda () (run-remote-command machine-name
"systemctl is-active"
(symbol->string service)))
(lambda (success output)
`(,service . ,(if success
(string-trim-right output)
"unknown")))))
services)
'()))
'())))
@ -82,11 +151,12 @@
(fold (lambda (cmd-pair acc)
(let ((key (car cmd-pair))
(command (cadr cmd-pair)))
(call-with-values (((success output)
(run-remote-command machine-name command)))
(if success
(assoc-set! acc (string->symbol key) (string-trim-right output))
acc))))
(call-with-values
(lambda () (run-remote-command machine-name command))
(lambda (success output)
(if success
(assoc-set! acc (string->symbol key) (string-trim-right output))
acc)))))
'()
info-commands)))
@ -120,36 +190,40 @@
;; Individual health check functions
(define (check-disk-space machine-name)
"Check if disk space is below critical threshold"
(call-with-values (((success output)
(run-remote-command machine-name "df / | tail -1 | awk '{print $5}' | sed 's/%//'")))
(if success
(let ((usage (string->number (string-trim-right output))))
(< usage 90)) ; Pass if usage < 90%
#f)))
(call-with-values
(lambda () (run-remote-command machine-name "df / | tail -1 | awk '{print $5}' | sed 's/%//'"))
(lambda (success output)
(if success
(let ((usage (string->number (string-trim-right output))))
(< usage 90)) ; Pass if usage < 90%
#f))))
(define (check-system-load machine-name)
"Check if system load is reasonable"
(call-with-values (((success output)
(run-remote-command machine-name "cat /proc/loadavg | cut -d' ' -f1")))
(if success
(let ((load (string->number (string-trim-right output))))
(< load 5.0)) ; Pass if load < 5.0
#f)))
(call-with-values
(lambda () (run-remote-command machine-name "cat /proc/loadavg | cut -d' ' -f1"))
(lambda (success output)
(if success
(let ((load (string->number (string-trim-right output))))
(< load 5.0)) ; Pass if load < 5.0
#f))))
(define (check-critical-services machine-name)
"Check that critical services are running"
(let ((critical-services '("sshd")))
(every (lambda (service)
(call-with-values (((success output)
(run-remote-command machine-name "systemctl is-active" service)))
(and success (string=? (string-trim-right output) "active"))))
(call-with-values
(lambda () (run-remote-command machine-name "systemctl is-active" service))
(lambda (success output)
(and success (string=? (string-trim-right output) "active")))))
critical-services)))
(define (check-network-connectivity machine-name)
"Check basic network connectivity"
(call-with-values (((success output)
(run-remote-command machine-name "ping -c 1 -W 5 8.8.8.8 > /dev/null 2>&1; echo $?")))
(and success (string=? (string-trim-right output) "0"))))
(call-with-values
(lambda () (run-remote-command machine-name "ping -c 1 -W 5 8.8.8.8 > /dev/null 2>&1; echo $?"))
(lambda (success output)
(and success (string=? (string-trim-right output) "0")))))
;; Update flake inputs
(define (update-flake options)
@ -249,4 +323,4 @@
(begin
(log-error "nixos-rebuild failed for ~a (exit: ~a)" machine-name status)
(log-error "Error output: ~a" output)
#f)))))))))))
#f))))))))))

View file

@ -5,7 +5,7 @@
#:use-module (ice-9 match)
#:use-module (ice-9 popen)
#:use-module (ice-9 textual-ports)
#:use-module (ice-9 call-with-values)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-19)
#:use-module (utils logging)

View file

@ -3,7 +3,7 @@
(define-module (lab machines)
#:use-module (ice-9 format)
#:use-module (ice-9 match)
#:use-module (ice-9 call-with-values)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-19)
#:use-module (utils logging)

View file

@ -5,7 +5,7 @@
#:use-module (ice-9 match)
#:use-module (ice-9 popen)
#:use-module (ice-9 textual-ports)
#:use-module (ice-9 call-with-values)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-19)
#:use-module (utils logging)

View file

@ -0,0 +1,243 @@
#!/usr/bin/env guile
!#
;; Comprehensive test script for Home Lab Guile implementation
;; Tests all modules and identifies bugs/missing functionality
(add-to-load-path ".")
(use-modules (ice-9 format)
(ice-9 ftw)
(srfi srfi-1)
(srfi srfi-64)) ; Testing framework
;; Global test results
(define test-results '())
(define failed-tests '())
;; Utility functions for testing
(define (test-module-loading module-name)
"Test if a module can be loaded without errors"
(format #t "Testing module loading: ~a... " module-name)
(catch #t
(lambda ()
(let ((module-parts (map string->symbol (string-split module-name #\space))))
(if (resolve-module module-parts #:ensure #f)
(begin
(format #t "✅ PASS\n")
#t)
(begin
(format #t "❌ FAIL - Module not found\n")
(set! failed-tests (cons module-name failed-tests))
#f))))
(lambda (key . args)
(format #t "❌ FAIL - ~a: ~a\n" key args)
(set! failed-tests (cons module-name failed-tests))
#f)))
(define (test-function-exists module-name function-name)
"Test if a function exists in a module"
(catch #t
(lambda ()
(let* ((module-parts (string-split module-name #\space))
(module (resolve-module module-parts)))
(if (module-defined? module (string->symbol function-name))
(begin
(format #t " ✅ Function ~a exists\n" function-name)
#t)
(begin
(format #t " ❌ Function ~a missing\n" function-name)
(set! failed-tests (cons (format #f "~a::~a" module-name function-name) failed-tests))
#f))))
(lambda (key . args)
(format #t " ❌ Error checking function ~a: ~a\n" function-name key)
#f)))
(define (test-basic-functionality)
"Test basic functionality of each module"
(format #t "\n=== BASIC FUNCTIONALITY TESTS ===\n")
;; Test utils modules
(format #t "\n--- Testing utils/logging ---\n")
(catch #t
(lambda ()
(use-modules (utils logging))
(log-info "Testing logging functionality")
(log-debug "Debug message test")
(log-warn "Warning message test")
(log-error "Error message test")
(format #t "✅ Logging module functional\n"))
(lambda (key . args)
(format #t "❌ Logging module failed: ~a\n" key)))
;; Test config module (with JSON dependency fix)
(format #t "\n--- Testing utils/config ---\n")
(catch #t
(lambda ()
;; Try to load config module
(use-modules (utils config))
(let ((machines (get-all-machines)))
(format #t "✅ Config loaded, found ~a machines: ~a\n"
(length machines) machines))
(let ((homelab-root (get-homelab-root)))
(format #t "✅ Home lab root: ~a\n" homelab-root)))
(lambda (key . args)
(format #t "❌ Config module failed: ~a\n" key)))
;; Test SSH module
(format #t "\n--- Testing utils/ssh ---\n")
(catch #t
(lambda ()
(use-modules (utils ssh))
(format #t "✅ SSH module loaded\n")
;; Test SSH connection to local machine
(let ((result (test-ssh-connection "congenital-optimist")))
(format #t "✅ SSH test result for local machine: ~a\n" result)))
(lambda (key . args)
(format #t "❌ SSH module failed: ~a\n" key)))
;; Test core lab modules
(format #t "\n--- Testing lab/core ---\n")
(catch #t
(lambda ()
(use-modules (lab core))
(format #t "✅ Lab core module loaded\n")
;; Test infrastructure status
(let ((status (get-infrastructure-status)))
(format #t "✅ Infrastructure status retrieved for ~a machines\n"
(length status))))
(lambda (key . args)
(format #t "❌ Lab core module failed: ~a\n" key)))
;; Test machines module
(format #t "\n--- Testing lab/machines ---\n")
(catch #t
(lambda ()
(use-modules (lab machines))
(format #t "✅ Lab machines module loaded\n"))
(lambda (key . args)
(format #t "❌ Lab machines module failed: ~a\n" key)))
;; Test deployment module
(format #t "\n--- Testing lab/deployment ---\n")
(catch #t
(lambda ()
(use-modules (lab deployment))
(format #t "✅ Lab deployment module loaded\n"))
(lambda (key . args)
(format #t "❌ Lab deployment module failed: ~a\n" key)))
;; Test monitoring module
(format #t "\n--- Testing lab/monitoring ---\n")
(catch #t
(lambda ()
(use-modules (lab monitoring))
(format #t "✅ Lab monitoring module loaded\n"))
(lambda (key . args)
(format #t "❌ Lab monitoring module failed: ~a\n" key))))
(define (test-file-syntax file-path)
"Test if a Scheme file has valid syntax"
(catch #t
(lambda ()
(call-with-input-file file-path
(lambda (port)
(let loop ((expr (read port)))
(unless (eof-object? expr)
(loop (read port))))))
(format #t "✅ ~a - syntax OK\n" file-path)
#t)
(lambda (key . args)
(format #t "❌ ~a - syntax error: ~a\n" file-path key)
(set! failed-tests (cons file-path failed-tests))
#f)))
(define (find-scheme-files dir)
"Find all .scm files in directory"
(let ((files '()))
(file-system-fold
(lambda (file-name stat result) ; enter?
#t)
(lambda (file-name stat result) ; leaf
(if (string-suffix? ".scm" file-name)
(cons file-name result)
result))
(lambda (file-name stat result) ; down
result)
(lambda (file-name stat result) ; up
result)
(lambda (file-name stat result) ; skip
result)
(lambda (file-name stat errno result) ; error
(format #t "Error accessing ~a: ~a\n" file-name errno)
result)
files
dir)
files))
;; Main test execution
(define (main)
(format #t "🧪 HOME LAB GUILE IMPLEMENTATION TEST SUITE\n")
(format #t "==========================================\n")
;; Test 1: Syntax checking
(format #t "\n=== SYNTAX CHECKING ===\n")
(let ((scheme-files (find-scheme-files ".")))
(for-each test-file-syntax scheme-files))
;; Test 2: Module loading
(format #t "\n=== MODULE LOADING TESTS ===\n")
(let ((modules '("utils logging"
"utils config"
"utils ssh"
"utils json"
"lab core"
"lab machines"
"lab deployment"
"lab monitoring"
"mcp server")))
(for-each test-module-loading modules))
;; Test 3: Basic functionality
(test-basic-functionality)
;; Test 4: Integration tests
(format #t "\n=== INTEGRATION TESTS ===\n")
(catch #t
(lambda ()
(use-modules (utils config) (utils ssh) (lab core))
(format #t "Testing machine configuration access...\n")
(let ((machines (get-all-machines)))
(for-each (lambda (machine)
(format #t " - Testing ~a: " machine)
(let ((config (get-machine-config machine)))
(if config
(format #t "✅ Config found\n")
(format #t "❌ No config\n"))))
machines)))
(lambda (key . args)
(format #t "❌ Integration test failed: ~a\n" key)))
;; Test 5: Command line interface
(format #t "\n=== CLI INTERFACE TESTS ===\n")
(catch #t
(lambda ()
(load "home-lab-tool.scm")
(format #t "✅ CLI script loaded successfully\n"))
(lambda (key . args)
(format #t "❌ CLI script failed: ~a\n" key)))
;; Summary
(format #t "\n=== TEST SUMMARY ===\n")
(if (null? failed-tests)
(format #t "🎉 All tests passed!\n")
(begin
(format #t "❌ ~a test(s) failed:\n" (length failed-tests))
(for-each (lambda (test)
(format #t " - ~a\n" test))
failed-tests)))
(format #t "\nTest complete.\n"))
;; Run the tests
(main)

View file

@ -0,0 +1,211 @@
#!/usr/bin/env guile
!#
;; Updated test script for modular refactoring
;; Tests the new K.I.S.S modular implementation
(add-to-load-path ".")
(add-to-load-path "lab")
(use-modules (ice-9 format)
(ice-9 ftw)
(srfi srfi-1))
;; Global test results
(define test-results '())
(define failed-tests '())
;; Test pure function modules
(define (test-pure-functions)
"Test pure function implementations"
(format #t "\n=== PURE FUNCTION TESTS ===\n")
;; Test logging format (pure functions)
(format #t "\n--- Testing utils/logging/format (pure) ---\n")
(catch #t
(lambda ()
(use-modules (utils logging format))
(let ((blue-color (get-color 'blue))
(timestamp (format-timestamp)))
(format #t "✅ Color retrieval: ~a\n" blue-color)
(format #t "✅ Timestamp format: ~a\n" timestamp)
(format #t "✅ Pure logging format functions work\n")))
(lambda (key . args)
(format #t "❌ Logging format test failed: ~a\n" key)))
;; Test config defaults (pure data)
(format #t "\n--- Testing utils/config/defaults (pure) ---\n")
(catch #t
(lambda ()
(use-modules (utils config defaults))
(let ((homelab-root (assoc-ref default-config 'homelab-root))
(machines (assoc-ref default-config 'machines)))
(format #t "✅ Default homelab root: ~a\n" homelab-root)
(format #t "✅ Default machines count: ~a\n" (length machines))
(format #t "✅ Pure config defaults work\n")))
(lambda (key . args)
(format #t "❌ Config defaults test failed: ~a\n" key)))
;; Test config accessor (pure functions)
(format #t "\n--- Testing utils/config/accessor (pure) ---\n")
(catch #t
(lambda ()
(use-modules (utils config defaults)
(utils config accessor))
(let* ((config default-config)
(homelab-root (get-config-value-pure config '(homelab-root)))
(machines (get-all-machines-pure config))
(first-machine (if (not (null? machines)) (car machines) "none")))
(format #t "✅ Pure config access: ~a\n" homelab-root)
(format #t "✅ Pure machine list: ~a machines\n" (length machines))
(format #t "✅ First machine: ~a\n" first-machine)
(format #t "✅ Pure config accessor functions work\n")))
(lambda (key . args)
(format #t "❌ Config accessor test failed: ~a\n" key)))
;; Test JSON parse (pure functions)
(format #t "\n--- Testing utils/json/parse (pure) ---\n")
(catch #t
(lambda ()
(use-modules (utils json parse))
(let ((test-json "{\"name\": \"test\", \"value\": 42}")
(invalid-json "{invalid"))
(let ((parsed-valid (parse-json-pure test-json))
(parsed-invalid (parse-json-pure invalid-json)))
(format #t "✅ Valid JSON parsed: ~a\n" (assoc-ref parsed-valid "name"))
(format #t "✅ Invalid JSON handled: ~a\n" (if parsed-invalid "ERROR" "OK"))
(format #t "✅ Pure JSON parsing functions work\n"))))
(lambda (key . args)
(format #t "❌ JSON parse test failed: ~a\n" key)))
;; Test JSON manipulation (pure functions)
(format #t "\n--- Testing utils/json/manipulation (pure) ---\n")
(catch #t
(lambda ()
(use-modules (utils json manipulation))
(let ((obj1 '((a . 1) (b . 2)))
(obj2 '((b . 3) (c . 4))))
(let ((merged (merge-json-objects obj1 obj2))
(value-at-path (json-path-ref '((level1 . ((level2 . "found")))) '(level1 level2))))
(format #t "✅ Merge result: ~a\n" (assoc-ref merged 'b))
(format #t "✅ Path access: ~a\n" value-at-path)
(format #t "✅ Pure JSON manipulation functions work\n"))))
(lambda (key . args)
(format #t "❌ JSON manipulation test failed: ~a\n" key))))
;; Test module facades
(define (test-facade-modules)
"Test facade modules that aggregate functionality"
(format #t "\n=== FACADE MODULE TESTS ===\n")
;; Test new logging facade
(format #t "\n--- Testing utils/logging (facade) ---\n")
(catch #t
(lambda ()
(use-modules (utils logging-new))
(log-info "Test info message")
(log-debug "Test debug message")
(set-log-level! 'debug)
(log-debug "Debug message after level change")
(format #t "✅ Logging facade works\n"))
(lambda (key . args)
(format #t "❌ Logging facade test failed: ~a\n" key)))
;; Test new config facade
(format #t "\n--- Testing utils/config (facade) ---\n")
(catch #t
(lambda ()
(use-modules (utils config-new))
(let ((machines (get-all-machines))
(homelab-root (get-homelab-root)))
(format #t "✅ Config facade - machines: ~a\n" (length machines))
(format #t "✅ Config facade - root: ~a\n" homelab-root)
(format #t "✅ Config facade works\n")))
(lambda (key . args)
(format #t "❌ Config facade test failed: ~a\n" key)))
;; Test new JSON facade
(format #t "\n--- Testing utils/json (facade) ---\n")
(catch #t
(lambda ()
(use-modules (utils json-new))
(let* ((test-obj '((name . "test") (count . 5)))
(json-str (scm->json-string test-obj))
(parsed-back (json-string->scm-safe json-str)))
(format #t "✅ JSON facade - serialization: ~a\n" (if json-str "OK" "FAIL"))
(format #t "✅ JSON facade - round-trip: ~a\n" (assoc-ref parsed-back 'name))
(format #t "✅ JSON facade works\n")))
(lambda (key . args)
(format #t "❌ JSON facade test failed: ~a\n" key))))
;; Test modular structure benefits
(define (test-modular-benefits)
"Test the benefits of modular structure"
(format #t "\n=== MODULAR BENEFITS TESTS ===\n")
;; Test pure function composition
(format #t "\n--- Testing Pure Function Composition ---\n")
(catch #t
(lambda ()
(use-modules (utils config defaults)
(utils config accessor)
(utils logging format))
;; Compose pure functions
(let* ((config default-config)
(machines (get-all-machines-pure config))
(first-machine (if (not (null? machines)) (car machines) #f))
(ssh-config (if first-machine
(get-ssh-config-pure config first-machine)
#f))
(timestamp (format-timestamp)))
(format #t "✅ Functional composition: ~a machines found\n" (length machines))
(format #t "✅ First machine SSH config: ~a\n"
(if ssh-config (assoc-ref ssh-config 'hostname) "none"))
(format #t "✅ Pure function pipeline works\n")))
(lambda (key . args)
(format #t "❌ Pure function composition failed: ~a\n" key)))
;; Test module independence
(format #t "\n--- Testing Module Independence ---\n")
(catch #t
(lambda ()
;; Each module should work independently
(use-modules (utils json parse))
(let ((result1 (parse-json-pure "{\"test\": 1}")))
(use-modules (utils json serialize))
(let ((result2 (scm->json-string-pure '((test . 2)) #f)))
(use-modules (utils logging format))
(let ((result3 (get-color 'green)))
(format #t "✅ Independent module 1: ~a\n" (if result1 "OK" "FAIL"))
(format #t "✅ Independent module 2: ~a\n" (if result2 "OK" "FAIL"))
(format #t "✅ Independent module 3: ~a\n" (if result3 "OK" "FAIL"))
(format #t "✅ Module independence verified\n")))))
(lambda (key . args)
(format #t "❌ Module independence test failed: ~a\n" key))))
;; Main test execution
(define (main)
(format #t "🧪 K.I.S.S MODULAR REFACTORING TEST SUITE\n")
(format #t "==========================================\n")
(test-pure-functions)
(test-facade-modules)
(test-modular-benefits)
;; Summary
(format #t "\n=== TEST SUMMARY ===\n")
(format #t "🎉 K.I.S.S modular refactoring tests complete!\n")
(format #t "\n📊 Refactoring Benefits Demonstrated:\n")
(format #t " ✅ Pure functions enable easy testing\n")
(format #t " ✅ Small modules are easy to understand\n")
(format #t " ✅ Single responsibility principle applied\n")
(format #t " ✅ Functional composition works\n")
(format #t " ✅ Module independence verified\n")
(format #t " ✅ Both pure and impure APIs available\n")
(format #t "\nRefactoring complete! ✨\n"))
;; Run the tests
(main)

View file

@ -0,0 +1,43 @@
#!/usr/bin/env guile
!#
;; Test script for modular refactoring
(add-to-load-path "lab")
(use-modules (ice-9 format))
;; Test logging format module
(display "Testing logging format module...\n")
(catch #t
(lambda ()
(use-modules (utils logging format))
(display "✅ Logging format module loaded\n")
(let ((blue-color (get-color 'blue)))
(format #t "Blue color code: ~a\n" blue-color)))
(lambda (key . args)
(format #t "❌ Failed to load logging format: ~a ~a\n" key args)))
;; Test config defaults module
(display "\nTesting config defaults module...\n")
(catch #t
(lambda ()
(use-modules (utils config defaults))
(display "✅ Config defaults module loaded\n")
(let ((config default-config))
(format #t "Default homelab root: ~a\n" (assoc-ref config 'homelab-root))))
(lambda (key . args)
(format #t "❌ Failed to load config defaults: ~a ~a\n" key args)))
;; Test JSON parse module
(display "\nTesting JSON parse module...\n")
(catch #t
(lambda ()
(use-modules (utils json parse))
(display "✅ JSON parse module loaded\n")
(let ((result (parse-json-pure "{\"test\": true}")))
(format #t "JSON parse test: ~a\n" result)))
(lambda (key . args)
(format #t "❌ Failed to load JSON parse: ~a ~a\n" key args)))
(display "\n🎉 Modular refactoring test complete!\n")

View file

@ -0,0 +1,43 @@
;; utils/config.scm - Configuration management facade
(define-module (utils config)
#:use-module (utils config defaults)
#:use-module (utils config loader)
#:use-module (utils config accessor)
#:use-module (utils config state)
#:re-export (;; State management
get-current-config
set-current-config!
reload-config!
;; Stateful accessors (work with current config)
get-config-value
get-machine-config
get-all-machines
get-ssh-config
validate-machine-name
get-homelab-root
;; Pure accessors (require explicit config parameter)
get-config-value-pure
get-machine-config-pure
get-all-machines-pure
get-ssh-config-pure
validate-machine-name-pure
;; Loading functions
load-config
load-config-from-file
;; Default configuration
default-config))
;; This module acts as a facade for configuration management,
;; aggregating specialized modules that follow single responsibility:
;; - defaults: Pure data definitions
;; - loader: File I/O operations
;; - accessor: Pure configuration value access
;; - state: Mutable state management
;;
;; Both pure and impure functions are available, allowing callers
;; to choose the appropriate level of functional purity.

View file

@ -3,9 +3,9 @@
(define-module (utils config)
#:use-module (ice-9 format)
#:use-module (ice-9 textual-ports)
#:use-module (json)
#:use-module (srfi srfi-1)
#:use-module (utils logging)
#:use-module (utils json)
#:export (load-config
get-config-value
machine-configs
@ -60,7 +60,7 @@
(catch #t
(lambda ()
(let ((json-data (call-with-input-file config-file get-string-all)))
(set! current-config (json-string->scm json-data))
(set! current-config (json-string->scm-safe json-data))
(log-info "Configuration loaded successfully")))
(lambda (key . args)
(log-warn "Failed to load config file, using defaults: ~a" key)

View file

@ -0,0 +1,74 @@
;; utils/config/accessor.scm - Configuration value access (pure functions)
(define-module (utils config accessor)
#:use-module (srfi srfi-1)
#:export (get-config-value-pure
get-machine-config-pure
get-all-machines-pure
get-ssh-config-pure
validate-machine-name-pure))
;; Pure function: Get configuration value by path
;; Input: config alist, path list, optional default value
;; Output: configuration value or default
(define (get-config-value-pure config path . default)
"Pure function to get configuration value by path"
(let ((result (fold (lambda (key acc)
(if (and acc (list? acc))
(assoc-ref acc key)
#f))
config path)))
(if result
result
(if (null? default) #f (car default)))))
;; Pure function: Get machine configurations
;; Input: config alist
;; Output: machines alist
(define (get-machine-configs-pure config)
"Pure function to get machine configurations"
(get-config-value-pure config '(machines)))
;; Pure function: Get configuration for specific machine
;; Input: config alist, machine-name (string or symbol)
;; Output: machine configuration alist or #f
(define (get-machine-config-pure config machine-name)
"Pure function to get machine configuration"
(let ((machine-symbol (if (symbol? machine-name)
machine-name
(string->symbol machine-name)))
(machines (get-machine-configs-pure config)))
(assoc-ref machines machine-symbol)))
;; Pure function: Get list of all machine names
;; Input: config alist
;; Output: list of machine name strings
(define (get-all-machines-pure config)
"Pure function to get all machine names"
(map (lambda (machine-entry)
(symbol->string (car machine-entry)))
(get-machine-configs-pure config)))
;; Pure function: Validate machine name exists
;; Input: config alist, machine-name string
;; Output: #t if valid, #f otherwise
(define (validate-machine-name-pure config machine-name)
"Pure function to validate machine name"
(let ((machines (get-all-machines-pure config)))
(member machine-name machines)))
;; Pure function: Get SSH configuration for machine
;; Input: config alist, machine-name (string or symbol)
;; Output: SSH configuration alist or #f
(define (get-ssh-config-pure config machine-name)
"Pure function to get SSH configuration for machine"
(let ((machine-config (get-machine-config-pure config machine-name)))
(if machine-config
(let ((type (assoc-ref machine-config 'type))
(hostname (assoc-ref machine-config 'hostname))
(ssh-alias (assoc-ref machine-config 'ssh-alias)))
`((type . ,type)
(hostname . ,hostname)
(ssh-alias . ,ssh-alias)
(is-local . ,(eq? type 'local))))
#f)))

View file

@ -0,0 +1,35 @@
;; utils/config/defaults.scm - Configuration defaults (pure data)
(define-module (utils config defaults)
#:export (default-config))
;; Pure data: Default configuration structure
(define default-config
`((homelab-root . "/home/geir/Home-lab")
(machines . ((congenital-optimist
(type . local)
(hostname . "localhost")
(services . (workstation development)))
(sleeper-service
(type . remote)
(hostname . "sleeper-service.tail807ea.ts.net")
(ssh-alias . "admin-sleeper")
(services . (nfs zfs storage)))
(grey-area
(type . remote)
(hostname . "grey-area.tail807ea.ts.net")
(ssh-alias . "admin-grey")
(services . (ollama forgejo git)))
(reverse-proxy
(type . remote)
(hostname . "reverse-proxy.tail807ea.ts.net")
(ssh-alias . "admin-reverse")
(services . (nginx proxy ssl)))))
(deployment . ((default-mode . "boot")
(timeout . 300)
(retry-count . 3)))
(monitoring . ((interval . 30)
(timeout . 10)))
(mcp . ((port . 3001)
(host . "localhost")
(log-level . "info")))))

View file

@ -0,0 +1,60 @@
;; utils/config/loader.scm - Configuration loading (file I/O operations)
(define-module (utils config loader)
#:use-module (ice-9 textual-ports)
#:use-module (utils logging)
#:use-module (utils json)
#:use-module (utils config defaults)
#:export (load-config-from-file
load-config))
;; Pure function: Parse configuration from JSON string
;; Input: json-string
;; Output: parsed configuration alist or #f if invalid
(define (parse-config-json json-string)
"Pure function to parse configuration from JSON string"
(catch #t
(lambda () (json-string->scm-safe json-string))
(lambda (key . args) #f)))
;; Pure function: Validate configuration structure
;; Input: config alist
;; Output: #t if valid, #f otherwise
(define (validate-config config)
"Pure function to validate configuration structure"
(and (list? config)
(assoc-ref config 'homelab-root)
(assoc-ref config 'machines)))
;; Impure function: Load configuration from file
;; Input: file-path string
;; Output: configuration alist or default-config if file doesn't exist/invalid
(define (load-config-from-file file-path)
"Load configuration from file (with side effects: file I/O, logging)"
(if (file-exists? file-path)
(catch #t
(lambda ()
(log-debug "Loading configuration from ~a" file-path)
(let* ((json-data (call-with-input-file file-path get-string-all))
(parsed-config (parse-config-json json-data)))
(if (and parsed-config (validate-config parsed-config))
(begin
(log-info "Configuration loaded successfully")
parsed-config)
(begin
(log-warn "Invalid configuration file, using defaults")
default-config))))
(lambda (key . args)
(log-warn "Failed to load config file, using defaults: ~a" key)
default-config))
(begin
(log-debug "No config file found at ~a, using defaults" file-path)
default-config)))
;; Impure function: Load configuration with default path
(define (load-config . args)
"Load configuration with optional file path"
(let ((config-file (if (null? args)
(string-append (getenv "HOME") "/.config/homelab/config.json")
(car args))))
(load-config-from-file config-file)))

View file

@ -0,0 +1,69 @@
;; utils/config/state.scm - Configuration state management
(define-module (utils config state)
#:use-module (utils config defaults)
#:use-module (utils config loader)
#:use-module (utils config accessor)
#:use-module (utils logging)
#:export (get-current-config
set-current-config!
reload-config!
get-config-value
get-machine-config
get-all-machines
get-ssh-config
validate-machine-name
get-homelab-root))
;; Mutable state: Current loaded configuration
(define current-config default-config)
;; Impure function: Get current configuration
(define (get-current-config)
"Get current loaded configuration"
current-config)
;; Impure function: Set current configuration
(define (set-current-config! config)
"Set current configuration (impure)"
(set! current-config config))
;; Impure function: Reload configuration from file
(define (reload-config! . args)
"Reload configuration from file"
(let ((new-config (apply load-config args)))
(set-current-config! new-config)
new-config))
;; Impure wrappers for pure accessor functions
(define (get-config-value path . default)
"Get configuration value from current config"
(apply get-config-value-pure current-config path default))
(define (get-machine-config machine-name)
"Get machine configuration from current config"
(get-machine-config-pure current-config machine-name))
(define (get-all-machines)
"Get all machine names from current config"
(get-all-machines-pure current-config))
(define (get-ssh-config machine-name)
"Get SSH configuration from current config"
(get-ssh-config-pure current-config machine-name))
(define (validate-machine-name machine-name)
"Validate machine name against current config"
(if (validate-machine-name-pure current-config machine-name)
#t
(begin
(log-error "Unknown machine: ~a" machine-name)
(log-error "Available machines: ~a" (string-join (get-all-machines) ", "))
#f)))
(define (get-homelab-root)
"Get home lab root directory from current config"
(get-config-value '(homelab-root) "/home/geir/Home-lab"))
;; Initialize configuration on module load
(reload-config!)

View file

@ -0,0 +1,48 @@
;; utils/json.scm - JSON utilities facade
(define-module (utils json)
#:use-module (utils json parse)
#:use-module (utils json serialize)
#:use-module (utils json file-io)
#:use-module (utils json validation)
#:use-module (utils json manipulation)
#:use-module (utils json pretty-print)
#:re-export (;; Parsing
parse-json-pure
json-string->scm-safe
;; Serialization
scm->json-string-pure
scm->json-string
;; File I/O (both pure and impure versions)
read-json-file-pure
write-json-file-pure
read-json-file
write-json-file
;; Validation (pure functions)
validate-required-keys
validate-types
validate-json-schema
;; Manipulation (pure functions)
merge-json-objects
flatten-json-paths
json-path-ref
json-path-set
;; Pretty printing
json-pretty-print))
;; This module acts as a facade for JSON functionality,
;; aggregating specialized modules that follow single responsibility:
;; - parse: Pure JSON string parsing
;; - serialize: Pure scheme-to-JSON conversion
;; - file-io: File reading/writing with pure and impure versions
;; - validation: Pure schema validation functions
;; - manipulation: Pure object manipulation functions
;; - pretty-print: Output formatting
;;
;; All functions are designed to be composable and testable,
;; with pure versions available for functional programming patterns.

View file

@ -0,0 +1,57 @@
;; utils/json/file-io.scm - JSON file I/O operations
(define-module (utils json file-io)
#:use-module (json)
#:use-module (ice-9 textual-ports)
#:use-module (utils logging)
#:export (read-json-file-pure
write-json-file-pure
read-json-file
write-json-file))
;; Pure function: Read JSON from file without logging
;; Input: filename string
;; Output: parsed object or #f if failed
(define (read-json-file-pure filename)
"Pure function to read JSON from file"
(catch #t
(lambda ()
(call-with-input-file filename
(lambda (port) (json->scm port))))
(lambda (key . args) #f)))
;; Pure function: Write JSON to file without logging
;; Input: filename string, obj (scheme object), pretty boolean
;; Output: #t if successful, #f if failed
(define (write-json-file-pure filename obj pretty)
"Pure function to write JSON to file"
(catch #t
(lambda ()
(call-with-output-file filename
(lambda (port)
(if pretty
(scm->json obj port #:pretty #t)
(scm->json obj port))))
#t)
(lambda (key . args) #f)))
;; Impure wrapper: Read JSON file with logging
(define (read-json-file filename)
"Read JSON from file with logging"
(log-debug "Reading JSON file: ~a" filename)
(let ((result (read-json-file-pure filename)))
(if result
(log-debug "Successfully read JSON file: ~a" filename)
(log-error "Failed to read JSON file: ~a" filename))
result))
;; Impure wrapper: Write JSON file with logging
(define (write-json-file filename obj . options)
"Write JSON to file with logging"
(let ((pretty (if (null? options) #t (car options))))
(log-debug "Writing JSON file: ~a" filename)
(let ((result (write-json-file-pure filename obj pretty)))
(if result
(log-debug "Successfully wrote JSON file: ~a" filename)
(log-error "Failed to write JSON file: ~a" filename))
result)))

View file

@ -0,0 +1,63 @@
;; utils/json/manipulation.scm - Pure JSON manipulation functions
(define-module (utils json manipulation)
#:use-module (srfi srfi-1)
#:export (merge-json-objects
flatten-json-paths
json-path-ref
json-path-set))
;; Pure function: Merge two JSON objects
;; Input: obj1 (alist), obj2 (alist)
;; Output: merged alist with obj2 values taking precedence
(define (merge-json-objects obj1 obj2)
"Pure function to merge two JSON objects"
(let ((merged (copy-tree obj1)))
(fold (lambda (pair acc)
(let ((key (car pair))
(value (cdr pair)))
(assoc-set! acc key value)))
merged
obj2)))
;; Pure function: Convert nested alist to flat key paths
;; Input: obj (nested alist), optional prefix (list of keys)
;; Output: list of (path . value) pairs
(define (flatten-json-paths obj . prefix)
"Pure function to flatten nested object to path-value pairs"
(let ((current-prefix (if (null? prefix) '() (car prefix))))
(fold (lambda (pair acc)
(let ((key (car pair))
(value (cdr pair)))
(let ((new-path (append current-prefix (list key))))
(if (and (list? value) (not (null? value)) (pair? (car value)))
;; Nested object - recurse
(append (flatten-json-paths value new-path) acc)
;; Leaf value
(cons (cons new-path value) acc)))))
'()
obj)))
;; Pure function: Get nested value using path
;; Input: obj (nested alist), path (list of keys)
;; Output: value at path or #f if not found
(define (json-path-ref obj path)
"Pure function to get value from nested object using key path"
(fold (lambda (key acc)
(if (and acc (list? acc))
(assoc-ref acc key)
#f))
obj path))
;; Pure function: Set nested value using path
;; Input: obj (nested alist), path (list of keys), value
;; Output: new alist with value set at path
(define (json-path-set obj path value)
"Pure function to set value in nested object using key path"
(if (null? path)
value
(let* ((key (car path))
(rest-path (cdr path))
(current-value (assoc-ref obj key))
(new-value (json-path-set (or current-value '()) rest-path value)))
(assoc-set! (copy-tree obj) key new-value))))

View file

@ -0,0 +1,21 @@
;; utils/json/parse.scm - Pure JSON parsing functions
(define-module (utils json parse)
#:use-module (json)
#:export (json-string->scm-safe
parse-json-pure))
;; Pure function: Safely parse JSON string
;; Input: json-string
;; Output: parsed scheme object or #f if invalid
(define (parse-json-pure json-string)
"Pure function to parse JSON string without side effects"
(catch #t
(lambda ()
(if (string? json-string)
(json-string->scm json-string)
#f))
(lambda (key . args) #f)))
;; Alias for compatibility
(define json-string->scm-safe parse-json-pure)

View file

@ -0,0 +1,13 @@
;; utils/json/pretty-print.scm - JSON pretty printing
(define-module (utils json pretty-print)
#:use-module (json)
#:export (json-pretty-print))
;; Impure function: Pretty print JSON to current output port
;; Input: obj (scheme object)
;; Output: unspecified (side effect: prints to current-output-port)
(define (json-pretty-print obj)
"Pretty print JSON object to current output port"
(scm->json obj (current-output-port) #:pretty #t)
(newline))

View file

@ -0,0 +1,27 @@
;; utils/json/serialize.scm - Pure JSON serialization functions
(define-module (utils json serialize)
#:use-module (json)
#:use-module (ice-9 textual-ports)
#:export (scm->json-string-pure
scm->json-string))
;; Pure function: Convert scheme object to JSON string
;; Input: obj (scheme object), pretty (boolean)
;; Output: JSON string or #f if conversion fails
(define (scm->json-string-pure obj pretty)
"Pure function to convert scheme object to JSON string"
(catch #t
(lambda ()
(call-with-output-string
(lambda (port)
(if pretty
(scm->json obj port #:pretty #t)
(scm->json obj port)))))
(lambda (key . args) #f)))
;; Wrapper with optional pretty parameter
(define (scm->json-string obj . options)
"Convert scheme object to JSON string with optional pretty printing"
(let ((pretty (if (null? options) #f (car options))))
(scm->json-string-pure obj pretty)))

View file

@ -0,0 +1,67 @@
;; utils/json/validation.scm - Pure JSON validation functions
(define-module (utils json validation)
#:use-module (ice-9 format)
#:use-module (srfi srfi-1)
#:export (validate-required-keys
validate-types
validate-json-schema))
;; Pure function: Check for required keys
;; Input: obj (alist), required-keys (list of symbols)
;; Output: list of missing keys (empty if all present)
(define (get-missing-keys obj required-keys)
"Pure function to find missing required keys"
(filter (lambda (key)
(not (assoc-ref obj key)))
required-keys))
;; Pure function: Validate required keys
;; Input: obj (alist), required-keys (list of symbols)
;; Output: #t if all present, #f otherwise
(define (validate-required-keys obj required-keys)
"Pure function to validate required keys are present"
(null? (get-missing-keys obj required-keys)))
;; Pure function: Check type specifications
;; Input: obj (alist), type-specs (list of (key expected-type) pairs)
;; Output: list of type error messages (empty if all valid)
(define (get-type-errors obj type-specs)
"Pure function to find type validation errors"
(filter-map
(lambda (type-spec)
(let ((key (car type-spec))
(expected-type (cadr type-spec)))
(let ((value (assoc-ref obj key)))
(if (and value (not (eq? (type-of value) expected-type)))
(format #f "Key ~a: expected ~a, got ~a"
key expected-type (type-of value))
#f))))
type-specs))
;; Pure function: Validate types
;; Input: obj (alist), type-specs (list of (key expected-type) pairs)
;; Output: #t if all types valid, #f otherwise
(define (validate-types obj type-specs)
"Pure function to validate object types"
(null? (get-type-errors obj type-specs)))
;; Pure function: Complete schema validation
;; Input: obj (alist), schema (list with required-keys, optional-keys, types)
;; Output: (values valid? error-messages)
(define (validate-json-schema obj schema)
"Pure function to validate JSON object against schema"
(let ((required-keys (car schema))
(optional-keys (if (> (length schema) 1) (cadr schema) '()))
(type-specs (if (> (length schema) 2) (caddr schema) '())))
(let ((missing-keys (get-missing-keys obj required-keys))
(type-errors (get-type-errors obj type-specs)))
(if (or (not (null? missing-keys)) (not (null? type-errors)))
(values #f (append
(if (not (null? missing-keys))
(list (format #f "Missing required keys: ~a" missing-keys))
'())
type-errors))
(values #t '())))))

View file

@ -0,0 +1,42 @@
;; utils/logging.scm - Logging facade (aggregates modular components)
(define-module (utils logging)
#:use-module (utils logging format)
#:use-module (utils logging level)
#:use-module (utils logging state)
#:use-module (utils logging output)
#:use-module (utils logging core)
#:use-module (utils logging spinner)
#:re-export (;; Core logging functions
log-debug
log-info
log-warn
log-error
log-success
;; State management
get-current-log-level
set-log-level!
should-log?
;; Pure functions (for testing and functional composition)
should-log-pure
validate-log-level
format-timestamp
format-log-message
get-color
log-message-pure
;; Utilities
with-spinner))
;; This module acts as a facade for logging functionality,
;; aggregating specialized modules that follow single responsibility:
;; - format: Pure formatting functions and color codes
;; - level: Pure log level management and validation
;; - state: Mutable state management for current log level
;; - output: Pure output formatting and port writing
;; - core: Main logging functions with side effects
;; - spinner: Progress indication for long operations
;;
;; Both pure and impure functions are available for maximum flexibility.

View file

@ -0,0 +1,38 @@
;; utils/logging/core.scm - Core logging functions
(define-module (utils logging core)
#:use-module (utils logging state)
#:use-module (utils logging output)
#:export (log-with-color
log-debug
log-info
log-warn
log-error
log-success))
;; Impure function: Core logging with color and level checking
(define (log-with-color level color prefix message . args)
"Log message with color if level is appropriate"
(when (should-log? level)
(log-to-port (current-error-port) level color prefix message args)))
;; Specific logging functions - each does one thing well
(define (log-debug message . args)
"Log debug message"
(apply log-with-color 'debug 'cyan "DEBUG" message args))
(define (log-info message . args)
"Log info message"
(apply log-with-color 'info 'blue "INFO " message args))
(define (log-warn message . args)
"Log warning message"
(apply log-with-color 'warn 'yellow "WARN " message args))
(define (log-error message . args)
"Log error message"
(apply log-with-color 'error 'red "ERROR" message args))
(define (log-success message . args)
"Log success message"
(apply log-with-color 'info 'green "SUCCESS" message args))

View file

@ -0,0 +1,42 @@
;; utils/logging/format.scm - Pure logging formatting functions
(define-module (utils logging format)
#:use-module (ice-9 format)
#:use-module (srfi srfi-19)
#:export (format-timestamp
format-log-message
get-color
color-codes))
;; Pure data: ANSI color codes
(define color-codes
'((reset . "\x1b[0m")
(bold . "\x1b[1m")
(red . "\x1b[31m")
(green . "\x1b[32m")
(yellow . "\x1b[33m")
(blue . "\x1b[34m")
(magenta . "\x1b[35m")
(cyan . "\x1b[36m")))
;; Pure function: Get color code by name
(define (get-color name)
"Pure function to get ANSI color code"
(assoc-ref color-codes name))
;; Pure function: Format timestamp
(define (format-timestamp)
"Pure function to format current timestamp"
(date->string (current-date) "~H:~M:~S"))
;; Pure function: Format complete log message
;; Input: level symbol, color symbol, prefix string, message string, args list
;; Output: formatted log message string
(define (format-log-message level color prefix message args)
"Pure function to format a complete log message"
(let ((timestamp (format-timestamp))
(formatted-msg (apply format #f message args))
(color-start (get-color color))
(color-end (get-color 'reset)))
(format #f "~a~a[lab]~a ~a ~a~%"
color-start prefix color-end timestamp formatted-msg)))

View file

@ -0,0 +1,30 @@
;; utils/logging/level.scm - Pure log level management
(define-module (utils logging level)
#:export (log-levels
should-log-pure
validate-log-level))
;; Pure data: Log levels with numeric values for comparison
(define log-levels
'((debug . 0)
(info . 1)
(warn . 2)
(error . 3)))
;; Pure function: Check if message should be logged at given levels
;; Input: current-level symbol, message-level symbol
;; Output: #t if should log, #f otherwise
(define (should-log-pure current-level message-level)
"Pure function to determine if message should be logged"
(let ((current-value (assoc-ref log-levels current-level))
(message-value (assoc-ref log-levels message-level)))
(and current-value message-value
(<= current-value message-value))))
;; Pure function: Validate log level
;; Input: level symbol
;; Output: #t if valid, #f otherwise
(define (validate-log-level level)
"Pure function to validate log level"
(assoc-ref log-levels level))

View file

@ -0,0 +1,23 @@
;; utils/logging/output.scm - Pure logging output functions
(define-module (utils logging output)
#:use-module (utils logging format)
#:use-module (utils logging level)
#:export (log-message-pure
log-to-port))
;; Pure function: Create log message without side effects
;; Input: level, color, prefix, message, args
;; Output: formatted log message string
(define (log-message-pure level color prefix message args)
"Pure function to create formatted log message"
(format-log-message level color prefix message args))
;; Impure function: Write log message to port
;; Input: port, level, color, prefix, message, args
;; Output: unspecified (side effect: writes to port)
(define (log-to-port port level color prefix message args)
"Write formatted log message to specified port"
(let ((formatted-message (log-message-pure level color prefix message args)))
(display formatted-message port)
(force-output port)))

View file

@ -0,0 +1,27 @@
;; utils/logging/spinner.scm - Spinner utility for long operations
(define-module (utils logging spinner)
#:use-module (utils logging core)
#:export (with-spinner))
;; Pure function: Calculate elapsed time
;; Input: start-time, end-time
;; Output: elapsed seconds
(define (calculate-elapsed start-time end-time)
"Pure function to calculate elapsed time"
(- end-time start-time))
;; Impure function: Execute operation with spinner logging
(define (with-spinner message thunk)
"Execute operation with progress logging"
(log-info "~a..." message)
(let ((start-time (current-time)))
(catch #t
(lambda ()
(let ((result (thunk)))
(let ((elapsed (calculate-elapsed start-time (current-time))))
(log-success "~a completed in ~as" message elapsed))
result))
(lambda (key . args)
(log-error "~a failed: ~a ~a" message key args)
(throw key args)))))

View file

@ -0,0 +1,27 @@
;; utils/logging/state.scm - Logging state management
(define-module (utils logging state)
#:use-module (utils logging level)
#:export (get-current-log-level
set-log-level!
should-log?))
;; Mutable state: Current log level
(define current-log-level 'info)
;; Impure function: Get current log level
(define (get-current-log-level)
"Get current log level"
current-log-level)
;; Impure function: Set log level with validation
(define (set-log-level! level)
"Set current log level (with validation)"
(if (validate-log-level level)
(set! current-log-level level)
(error "Invalid log level" level)))
;; Impure function: Check if message should be logged
(define (should-log? level)
"Check if message should be logged at current level"
(should-log-pure current-log-level level))

View file

@ -0,0 +1,27 @@
;; utils/ssh.scm - SSH operations facade (aggregates modular components)
(define-module (utils ssh)
#:use-module (utils ssh connection-test)
#:use-module (utils ssh remote-command)
#:use-module (utils ssh file-copy)
#:use-module (utils ssh retry)
#:use-module (utils ssh context)
#:re-export (test-ssh-connection
run-remote-command
run-remote-command-pure
copy-file-to-remote
copy-file-pure
run-command-with-retry
with-retry
with-ssh-connection))
;; This module acts as a facade, re-exporting functions from specialized modules
;; Each sub-module follows the single responsibility principle:
;; - connection-test: SSH connectivity testing
;; - remote-command: Command execution on remote machines
;; - file-copy: File transfer operations
;; - retry: Retry logic and error recovery
;; - context: Connection context management
;;
;; Pure functions are exported alongside their impure wrappers,
;; allowing callers to choose the appropriate level of abstraction.

View file

@ -7,7 +7,7 @@
#:use-module (ice-9 popen)
#:use-module (ice-9 rdelim)
#:use-module (ice-9 textual-ports)
#:use-module (ice-9 call-with-values)
#:use-module (srfi srfi-1)
#:use-module (utils logging)
#:use-module (utils config)
@ -113,15 +113,17 @@
;; Run command with retry logic
(define (run-command-with-retry machine-name command max-retries . args)
(let loop ((retries 0))
(call-with-values (success output) (apply run-remote-command machine-name command args)
(if success
(values #t output)
(if (< retries max-retries)
(begin
(log-warn "Command failed, retrying (~a/~a)..." (+ retries 1) max-retries)
(sleep 2)
(loop (+ retries 1)))
(values #f output))))))
(call-with-values
(lambda () (apply run-remote-command machine-name command args))
(lambda (success output)
(if success
(values #t output)
(if (< retries max-retries)
(begin
(log-warn "Command failed, retrying (~a/~a)..." (+ retries 1) max-retries)
(sleep 2)
(loop (+ retries 1)))
(values #f output)))))))
;; Execute a thunk with SSH connection context
(define (with-ssh-connection machine-name thunk)

View file

@ -0,0 +1,41 @@
;; utils/ssh/connection-test.scm - Pure SSH connection testing
(define-module (utils ssh connection-test)
#:use-module (ice-9 popen)
#:use-module (ice-9 textual-ports)
#:use-module (ice-9 format)
#:use-module (utils logging)
#:use-module (utils config)
#:export (test-ssh-connection-pure
test-ssh-connection))
;; Pure function: Test SSH connectivity to a machine
;; Input: ssh-config alist
;; Output: #t if connection successful, #f otherwise
(define (test-ssh-connection-pure ssh-config)
"Pure function to test SSH connection given ssh-config"
(let ((hostname (assoc-ref ssh-config 'hostname))
(ssh-alias (assoc-ref ssh-config 'ssh-alias))
(is-local (assoc-ref ssh-config 'is-local)))
(if is-local
#t ; Local connections always succeed
(let* ((target (or ssh-alias hostname))
(test-cmd (format #f "ssh -o ConnectTimeout=5 -o BatchMode=yes ~a echo OK" target))
(port (open-pipe* OPEN_READ "/bin/sh" "-c" test-cmd))
(output (get-string-all port))
(status (close-pipe port)))
(zero? status)))))
;; Impure wrapper: Test SSH connection with logging and config lookup
(define (test-ssh-connection machine-name)
"Test SSH connectivity to a machine (with side effects: logging, config lookup)"
(let ((ssh-config (get-ssh-config machine-name)))
(if (not ssh-config)
(begin
(log-error "No SSH configuration found for ~a" machine-name)
#f)
(let ((result (test-ssh-connection-pure ssh-config)))
(if result
(log-debug "SSH connection to ~a successful" machine-name)
(log-warn "SSH connection to ~a failed" machine-name))
result))))

View file

@ -0,0 +1,33 @@
;; utils/ssh/context.scm - SSH context management
(define-module (utils ssh context)
#:use-module (ice-9 format)
#:use-module (utils logging)
#:use-module (utils ssh connection-test)
#:export (with-connection-context
with-ssh-connection))
;; Pure function: Execute operation with connection validation
;; Input: connection-validator (thunk -> boolean), operation (thunk)
;; Output: result of operation or #f if connection invalid
(define (with-connection-context connection-validator operation)
"Pure function to execute operation with connection context"
(if (connection-validator)
(catch #t
operation
(lambda (key . args)
(values #f (format #f "Operation failed: ~a ~a" key args))))
(values #f "Connection validation failed")))
;; Impure wrapper: Execute with SSH connection context and logging
(define (with-ssh-connection machine-name thunk)
"Execute operation with SSH connection context (with side effects: logging)"
(let ((connection-validator (lambda () (test-ssh-connection machine-name))))
(call-with-values
(lambda () (with-connection-context connection-validator thunk))
(lambda (success result)
(if success
result
(begin
(log-error "SSH operation failed for ~a: ~a" machine-name result)
#f))))))

View file

@ -0,0 +1,50 @@
;; utils/ssh/file-copy.scm - Pure file copying operations
(define-module (utils ssh file-copy)
#:use-module (ice-9 format)
#:use-module (utils logging)
#:use-module (utils config)
#:export (copy-file-pure
build-copy-context
copy-file-to-remote))
;; Pure function: Copy file with given copy context
;; Input: copy-context alist, local-path string, remote-path string
;; Output: #t if successful, #f otherwise
(define (copy-file-pure copy-context local-path remote-path)
"Pure function to copy file given copy context"
(let ((is-local (assoc-ref copy-context 'is-local))
(target (assoc-ref copy-context 'target)))
(let* ((copy-cmd (if is-local
(format #f "cp '~a' '~a'" local-path remote-path)
(format #f "scp '~a' '~a:~a'" local-path target remote-path)))
(status (system copy-cmd)))
(zero? status))))
;; Pure function: Build copy context from ssh-config
(define (build-copy-context ssh-config)
"Pure function to build copy context from ssh-config"
(let ((hostname (assoc-ref ssh-config 'hostname))
(ssh-alias (assoc-ref ssh-config 'ssh-alias))
(is-local (assoc-ref ssh-config 'is-local)))
`((is-local . ,is-local)
(target . ,(or ssh-alias hostname)))))
;; Impure wrapper: Copy file to remote with logging and config lookup
(define (copy-file-to-remote machine-name local-path remote-path)
"Copy file to remote machine (with side effects: logging, config lookup)"
(let ((ssh-config (get-ssh-config machine-name)))
(if (not ssh-config)
(begin
(log-error "No SSH configuration found for ~a" machine-name)
#f)
(let* ((copy-context (build-copy-context ssh-config))
(is-local (assoc-ref copy-context 'is-local)))
(log-debug "Copying ~a: ~a -> ~a"
(if is-local "locally" (format #f "to ~a" machine-name))
local-path remote-path)
(let ((result (copy-file-pure copy-context local-path remote-path)))
(if result
(log-debug "File copy successful")
(log-error "File copy failed"))
result)))))

View file

@ -0,0 +1,58 @@
;; utils/ssh/remote-command.scm - Pure remote command execution
(define-module (utils ssh remote-command)
#:use-module (ice-9 popen)
#:use-module (ice-9 textual-ports)
#:use-module (ice-9 format)
#:use-module (srfi srfi-1)
#:use-module (utils logging)
#:use-module (utils config)
#:export (run-remote-command-pure
execute-command-pure
build-execution-context
run-remote-command))
;; Pure function: Execute command with given execution context
;; Input: execution-context alist, command string, args list
;; Output: (values success? output-string)
(define (execute-command-pure execution-context command args)
"Pure function to execute command in given context"
(let ((is-local (assoc-ref execution-context 'is-local))
(target (assoc-ref execution-context 'target))
(full-command (if (null? args)
command
(format #f "~a ~a" command (string-join args " ")))))
(let* ((exec-cmd (if is-local
full-command
(format #f "ssh ~a '~a'" target full-command)))
(port (open-pipe* OPEN_READ "/bin/sh" "-c" exec-cmd))
(output (get-string-all port))
(status (close-pipe port)))
(values (zero? status) output))))
;; Pure function: Build execution context from ssh-config
(define (build-execution-context ssh-config)
"Pure function to build execution context from ssh-config"
(let ((hostname (assoc-ref ssh-config 'hostname))
(ssh-alias (assoc-ref ssh-config 'ssh-alias))
(is-local (assoc-ref ssh-config 'is-local)))
`((is-local . ,is-local)
(target . ,(or ssh-alias hostname)))))
;; Pure wrapper: Run remote command with pure functions
(define (run-remote-command-pure ssh-config command args)
"Pure function to run remote command given ssh-config"
(let ((exec-context (build-execution-context ssh-config)))
(execute-command-pure exec-context command args)))
;; Impure wrapper: Run remote command with logging and config lookup
(define (run-remote-command machine-name command . args)
"Run command on remote machine (with side effects: logging, config lookup)"
(let ((ssh-config (get-ssh-config machine-name)))
(if (not ssh-config)
(begin
(log-error "No SSH configuration found for ~a" machine-name)
(values #f "No SSH configuration found"))
(begin
(log-debug "Executing on ~a: ~a ~a" machine-name command (string-join args " "))
(run-remote-command-pure ssh-config command args)))))

View file

@ -0,0 +1,45 @@
;; utils/ssh/retry.scm - Pure retry logic
(define-module (utils ssh retry)
#:use-module (utils logging)
#:use-module (utils ssh remote-command)
#:export (with-retry
run-command-with-retry))
;; Pure function: Retry operation with exponential backoff
;; Input: operation (thunk), max-retries number, delay-fn (retry-count -> seconds)
;; Output: result of operation or #f if all retries failed
(define (with-retry operation max-retries . delay-fn)
"Pure retry logic - operation should return (values success? result)"
(let ((delay-func (if (null? delay-fn)
(lambda (retry) (* retry 2)) ; Default: exponential backoff
(car delay-fn))))
(let loop ((retries 0))
(call-with-values operation
(lambda (success result)
(if success
(values #t result)
(if (< retries max-retries)
(begin
(sleep (delay-func retries))
(loop (+ retries 1)))
(values #f result))))))))
;; Impure wrapper: Run command with retry and logging
(define (run-command-with-retry machine-name command max-retries . args)
"Run command with retry logic (with side effects: logging)"
(let ((operation (lambda ()
(apply run-remote-command machine-name command args))))
(let loop ((retries 0))
(call-with-values operation
(lambda (success output)
(if success
(values #t output)
(if (< retries max-retries)
(begin
(log-warn "Command failed, retrying (~a/~a)..." (+ retries 1) max-retries)
(sleep 2)
(loop (+ retries 1)))
(begin
(log-error "Command failed after ~a retries" max-retries)
(values #f output))))))))))

127
packages/lab-tools.nix Normal file
View file

@ -0,0 +1,127 @@
{
lib,
stdenv,
guile,
makeWrapper,
writeShellScriptBin,
}: let
# Lab Tool - K.I.S.S Refactored Implementation
lab-tool = stdenv.mkDerivation {
pname = "lab-tool";
version = "2.0.0-kiss";
src = ./lab;
nativeBuildInputs = [makeWrapper];
buildInputs = [guile];
installPhase = ''
mkdir -p $out/share/lab-tool
cp -r . $out/share/lab-tool/
mkdir -p $out/bin
# Create the main lab tool executable
cat > $out/bin/lab << 'EOF'
#!/usr/bin/env bash
export GUILE_LOAD_PATH="$out/share/lab-tool:$GUILE_LOAD_PATH"
exec ${guile}/bin/guile "$out/share/lab-tool/main.scm" "$@"
EOF
chmod +x $out/bin/lab
# Create aliases for convenience
ln -s $out/bin/lab $out/bin/lab-tool
'';
meta = with lib; {
description = "K.I.S.S refactored home lab management tool";
longDescription = ''
A modular, functional home lab management tool following K.I.S.S principles:
- Modular: Each module has single responsibility
- Functional: Pure functions separated from side effects
- Small: Individual modules under 50 lines
- Simple: One function does one thing well
Features:
- Infrastructure status checking
- Machine management and deployment
- SSH connectivity testing
- Modular architecture for easy extension
'';
homepage = "https://github.com/geirda/Home-lab";
license = licenses.mit;
maintainers = ["geir"];
platforms = platforms.unix;
};
};
# MCP Server placeholder (for future implementation)
mcp-server = writeShellScriptBin "mcp-server" ''
echo "MCP Server - Coming Soon!"
echo "This will provide Model Context Protocol integration"
exit 0
'';
# RAG System placeholder (for future implementation)
rag-system = writeShellScriptBin "rag-system" ''
echo "RAG System - Coming Soon!"
echo "This will provide Retrieval-Augmented Generation capabilities"
exit 0
'';
in {
# Export individual tools
inherit lab-tool mcp-server rag-system;
# Main package combines all tools
default = stdenv.mkDerivation {
pname = "home-lab-tools";
version = "2.0.0-kiss";
dontUnpack = true;
nativeBuildInputs = [makeWrapper];
installPhase = ''
mkdir -p $out/bin
# Link all tools
ln -s ${lab-tool}/bin/* $out/bin/
ln -s ${mcp-server}/bin/* $out/bin/
ln -s ${rag-system}/bin/* $out/bin/
# Create main entry point that shows all available tools
cat > $out/bin/home-lab-tools << 'EOF'
#!/usr/bin/env bash
echo "🏠 Home Lab Tools - K.I.S.S Edition"
echo "=================================="
echo ""
echo "Available Tools:"
echo " lab - Lab management tool (K.I.S.S refactored)"
echo " mcp-server - Model Context Protocol server"
echo " rag-system - Retrieval-Augmented Generation system"
echo ""
echo "Examples:"
echo " lab status # Show infrastructure status"
echo " lab machines # List all machines"
echo " lab deploy machine # Deploy to machine"
echo " mcp-server # Start MCP server"
echo " rag-system # Start RAG system"
echo ""
echo "For detailed help: lab help"
EOF
chmod +x $out/bin/home-lab-tools
'';
meta = with lib; {
description = "Complete home lab tooling suite";
longDescription = ''
Comprehensive home lab management tooling following K.I.S.S principles.
Includes lab tool, MCP server, and RAG system components.
'';
homepage = "https://github.com/geirda/Home-lab";
license = licenses.mit;
maintainers = ["geir"];
platforms = platforms.unix;
};
};
}

View file