Update configuration files, templates, and internal documentation for the provisioning repository system. Configuration Updates: - KMS configuration modernization - Plugin system settings - Service port mappings - Test cluster topologies - Installation configuration examples - VM configuration defaults - Cedar authorization policies Documentation Updates: - Library module documentation - Extension API guides - AI system documentation - Service management guides - Test environment setup - Plugin usage guides - Validator configuration documentation All changes are backward compatible.
739 lines
36 KiB
HTML
739 lines
36 KiB
HTML
<!DOCTYPE HTML>
|
||
<html lang="en" class="ayu sidebar-visible" dir="ltr">
|
||
<head>
|
||
<!-- Book generated using mdBook -->
|
||
<meta charset="UTF-8">
|
||
<title>Command Handler Guide - Provisioning Platform Documentation</title>
|
||
|
||
|
||
<!-- Custom HTML head -->
|
||
|
||
<meta name="description" content="Complete documentation for the Provisioning Platform - Infrastructure automation with Nushell, KCL, and Rust">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="theme-color" content="#ffffff">
|
||
|
||
<link rel="icon" href="../favicon.svg">
|
||
<link rel="shortcut icon" href="../favicon.png">
|
||
<link rel="stylesheet" href="../css/variables.css">
|
||
<link rel="stylesheet" href="../css/general.css">
|
||
<link rel="stylesheet" href="../css/chrome.css">
|
||
<link rel="stylesheet" href="../css/print.css" media="print">
|
||
|
||
<!-- Fonts -->
|
||
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
|
||
<link rel="stylesheet" href="../fonts/fonts.css">
|
||
|
||
<!-- Highlight.js Stylesheets -->
|
||
<link rel="stylesheet" id="highlight-css" href="../highlight.css">
|
||
<link rel="stylesheet" id="tomorrow-night-css" href="../tomorrow-night.css">
|
||
<link rel="stylesheet" id="ayu-highlight-css" href="../ayu-highlight.css">
|
||
|
||
<!-- Custom theme stylesheets -->
|
||
|
||
|
||
<!-- Provide site root and default themes to javascript -->
|
||
<script>
|
||
const path_to_root = "../";
|
||
const default_light_theme = "ayu";
|
||
const default_dark_theme = "navy";
|
||
</script>
|
||
<!-- Start loading toc.js asap -->
|
||
<script src="../toc.js"></script>
|
||
</head>
|
||
<body>
|
||
<div id="mdbook-help-container">
|
||
<div id="mdbook-help-popup">
|
||
<h2 class="mdbook-help-title">Keyboard shortcuts</h2>
|
||
<div>
|
||
<p>Press <kbd>←</kbd> or <kbd>→</kbd> to navigate between chapters</p>
|
||
<p>Press <kbd>S</kbd> or <kbd>/</kbd> to search in the book</p>
|
||
<p>Press <kbd>?</kbd> to show this help</p>
|
||
<p>Press <kbd>Esc</kbd> to hide this help</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="body-container">
|
||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||
<script>
|
||
try {
|
||
let theme = localStorage.getItem('mdbook-theme');
|
||
let sidebar = localStorage.getItem('mdbook-sidebar');
|
||
|
||
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||
}
|
||
|
||
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||
}
|
||
} catch (e) { }
|
||
</script>
|
||
|
||
<!-- Set the theme before any content is loaded, prevents flash -->
|
||
<script>
|
||
const default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? default_dark_theme : default_light_theme;
|
||
let theme;
|
||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||
if (theme === null || theme === undefined) { theme = default_theme; }
|
||
const html = document.documentElement;
|
||
html.classList.remove('ayu')
|
||
html.classList.add(theme);
|
||
html.classList.add("js");
|
||
</script>
|
||
|
||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
||
|
||
<!-- Hide / unhide sidebar before it is displayed -->
|
||
<script>
|
||
let sidebar = null;
|
||
const sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
|
||
if (document.body.clientWidth >= 1080) {
|
||
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||
sidebar = sidebar || 'visible';
|
||
} else {
|
||
sidebar = 'hidden';
|
||
}
|
||
sidebar_toggle.checked = sidebar === 'visible';
|
||
html.classList.remove('sidebar-visible');
|
||
html.classList.add("sidebar-" + sidebar);
|
||
</script>
|
||
|
||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||
<!-- populated by js -->
|
||
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
|
||
<noscript>
|
||
<iframe class="sidebar-iframe-outer" src="../toc.html"></iframe>
|
||
</noscript>
|
||
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
||
<div class="sidebar-resize-indicator"></div>
|
||
</div>
|
||
</nav>
|
||
|
||
<div id="page-wrapper" class="page-wrapper">
|
||
|
||
<div class="page">
|
||
<div id="menu-bar-hover-placeholder"></div>
|
||
<div id="menu-bar" class="menu-bar sticky">
|
||
<div class="left-buttons">
|
||
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||
<i class="fa fa-bars"></i>
|
||
</label>
|
||
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||
<i class="fa fa-paint-brush"></i>
|
||
</button>
|
||
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||
<li role="none"><button role="menuitem" class="theme" id="default_theme">Auto</button></li>
|
||
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
||
</ul>
|
||
<button id="search-toggle" class="icon-button" type="button" title="Search (`/`)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="/ s" aria-controls="searchbar">
|
||
<i class="fa fa-search"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<h1 class="menu-title">Provisioning Platform Documentation</h1>
|
||
|
||
<div class="right-buttons">
|
||
<a href="../print.html" title="Print this book" aria-label="Print this book">
|
||
<i id="print-button" class="fa fa-print"></i>
|
||
</a>
|
||
<a href="https://github.com/provisioning/provisioning-platform" title="Git repository" aria-label="Git repository">
|
||
<i id="git-repository-button" class="fa fa-github"></i>
|
||
</a>
|
||
<a href="https://github.com/provisioning/provisioning-platform/edit/main/provisioning/docs/src/development/COMMAND_HANDLER_GUIDE.md" title="Suggest an edit" aria-label="Suggest an edit">
|
||
<i id="git-edit-button" class="fa fa-edit"></i>
|
||
</a>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div id="search-wrapper" class="hidden">
|
||
<form id="searchbar-outer" class="searchbar-outer">
|
||
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||
</form>
|
||
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||
<div id="searchresults-header" class="searchresults-header"></div>
|
||
<ul id="searchresults">
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||
<script>
|
||
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||
});
|
||
</script>
|
||
|
||
<div id="content" class="content">
|
||
<main>
|
||
<h1 id="command-handler-developer-guide"><a class="header" href="#command-handler-developer-guide">Command Handler Developer Guide</a></h1>
|
||
<p><strong>Target Audience</strong>: Developers working on the provisioning CLI
|
||
<strong>Last Updated</strong>: 2025-09-30
|
||
<strong>Related</strong>: <a href="../architecture/adr/ADR-006-provisioning-cli-refactoring.html">ADR-006 CLI Refactoring</a></p>
|
||
<h2 id="overview"><a class="header" href="#overview">Overview</a></h2>
|
||
<p>The provisioning CLI uses a <strong>modular, domain-driven architecture</strong> that separates concerns into focused command handlers. This guide shows you how to work with this architecture.</p>
|
||
<h3 id="key-architecture-principles"><a class="header" href="#key-architecture-principles">Key Architecture Principles</a></h3>
|
||
<ol>
|
||
<li><strong>Separation of Concerns</strong>: Routing, flag parsing, and business logic are separated</li>
|
||
<li><strong>Domain-Driven Design</strong>: Commands organized by domain (infrastructure, orchestration, etc.)</li>
|
||
<li><strong>DRY (Don’t Repeat Yourself)</strong>: Centralized flag handling eliminates code duplication</li>
|
||
<li><strong>Single Responsibility</strong>: Each module has one clear purpose</li>
|
||
<li><strong>Open/Closed Principle</strong>: Easy to extend, no need to modify core routing</li>
|
||
</ol>
|
||
<h3 id="architecture-components"><a class="header" href="#architecture-components">Architecture Components</a></h3>
|
||
<pre><code>provisioning/core/nulib/
|
||
├── provisioning (211 lines) - Main entry point
|
||
├── main_provisioning/
|
||
│ ├── flags.nu (139 lines) - Centralized flag handling
|
||
│ ├── dispatcher.nu (264 lines) - Command routing
|
||
│ ├── help_system.nu - Categorized help system
|
||
│ └── commands/ - Domain-focused handlers
|
||
│ ├── infrastructure.nu (117 lines) - Server, taskserv, cluster, infra
|
||
│ ├── orchestration.nu (64 lines) - Workflow, batch, orchestrator
|
||
│ ├── development.nu (72 lines) - Module, layer, version, pack
|
||
│ ├── workspace.nu (56 lines) - Workspace, template
|
||
│ ├── generation.nu (78 lines) - Generate commands
|
||
│ ├── utilities.nu (157 lines) - SSH, SOPS, cache, providers
|
||
│ └── configuration.nu (316 lines) - Env, show, init, validate
|
||
</code></pre>
|
||
<h2 id="adding-new-commands"><a class="header" href="#adding-new-commands">Adding New Commands</a></h2>
|
||
<h3 id="step-1-choose-the-right-domain-handler"><a class="header" href="#step-1-choose-the-right-domain-handler">Step 1: Choose the Right Domain Handler</a></h3>
|
||
<p>Commands are organized by domain. Choose the appropriate handler:</p>
|
||
<div class="table-wrapper"><table><thead><tr><th>Domain</th><th>Handler</th><th>Responsibility</th></tr></thead><tbody>
|
||
<tr><td><code>infrastructure.nu</code></td><td>Server/taskserv/cluster/infra lifecycle</td><td></td></tr>
|
||
<tr><td><code>orchestration.nu</code></td><td>Workflow/batch operations, orchestrator control</td><td></td></tr>
|
||
<tr><td><code>development.nu</code></td><td>Module discovery, layers, versions, packaging</td><td></td></tr>
|
||
<tr><td><code>workspace.nu</code></td><td>Workspace and template management</td><td></td></tr>
|
||
<tr><td><code>configuration.nu</code></td><td>Environment, settings, initialization</td><td></td></tr>
|
||
<tr><td><code>utilities.nu</code></td><td>SSH, SOPS, cache, providers, utilities</td><td></td></tr>
|
||
<tr><td><code>generation.nu</code></td><td>Generate commands (server, taskserv, etc.)</td><td></td></tr>
|
||
</tbody></table>
|
||
</div>
|
||
<h3 id="step-2-add-command-to-handler"><a class="header" href="#step-2-add-command-to-handler">Step 2: Add Command to Handler</a></h3>
|
||
<p><strong>Example: Adding a new server command <code>server status</code></strong></p>
|
||
<p>Edit <code>provisioning/core/nulib/main_provisioning/commands/infrastructure.nu</code>:</p>
|
||
<pre><code class="language-nushell"># Add to the handle_infrastructure_command match statement
|
||
export def handle_infrastructure_command [
|
||
command: string
|
||
ops: string
|
||
flags: record
|
||
] {
|
||
set_debug_env $flags
|
||
|
||
match $command {
|
||
"server" => { handle_server $ops $flags }
|
||
"taskserv" | "task" => { handle_taskserv $ops $flags }
|
||
"cluster" => { handle_cluster $ops $flags }
|
||
"infra" | "infras" => { handle_infra $ops $flags }
|
||
_ => {
|
||
print $"❌ Unknown infrastructure command: ($command)"
|
||
print ""
|
||
print "Available infrastructure commands:"
|
||
print " server - Server operations (create, delete, list, ssh, status)" # Updated
|
||
print " taskserv - Task service management"
|
||
print " cluster - Cluster operations"
|
||
print " infra - Infrastructure management"
|
||
print ""
|
||
print "Use 'provisioning help infrastructure' for more details"
|
||
exit 1
|
||
}
|
||
}
|
||
}
|
||
|
||
# Add the new command handler
|
||
def handle_server [ops: string, flags: record] {
|
||
let args = build_module_args $flags $ops
|
||
run_module $args "server" --exec
|
||
}
|
||
</code></pre>
|
||
<p><strong>That’s it!</strong> The command is now available as <code>provisioning server status</code>.</p>
|
||
<h3 id="step-3-add-shortcuts-optional"><a class="header" href="#step-3-add-shortcuts-optional">Step 3: Add Shortcuts (Optional)</a></h3>
|
||
<p>If you want shortcuts like <code>provisioning s status</code>:</p>
|
||
<p>Edit <code>provisioning/core/nulib/main_provisioning/dispatcher.nu</code>:</p>
|
||
<pre><code class="language-nushell">export def get_command_registry []: nothing -> record {
|
||
{
|
||
# Infrastructure commands
|
||
"s" => "infrastructure server" # Already exists
|
||
"server" => "infrastructure server" # Already exists
|
||
|
||
# Your new shortcut (if needed)
|
||
# Example: "srv-status" => "infrastructure server status"
|
||
|
||
# ... rest of registry
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><strong>Note</strong>: Most shortcuts are already configured. You only need to add new shortcuts if you’re creating completely new command categories.</p>
|
||
<h2 id="modifying-existing-handlers"><a class="header" href="#modifying-existing-handlers">Modifying Existing Handlers</a></h2>
|
||
<h3 id="example-enhancing-the-taskserv-command"><a class="header" href="#example-enhancing-the-taskserv-command">Example: Enhancing the <code>taskserv</code> Command</a></h3>
|
||
<p>Let’s say you want to add better error handling to the taskserv command:</p>
|
||
<p><strong>Before:</strong></p>
|
||
<pre><code class="language-nushell">def handle_taskserv [ops: string, flags: record] {
|
||
let args = build_module_args $flags $ops
|
||
run_module $args "taskserv" --exec
|
||
}
|
||
</code></pre>
|
||
<p><strong>After:</strong></p>
|
||
<pre><code class="language-nushell">def handle_taskserv [ops: string, flags: record] {
|
||
# Validate taskserv name if provided
|
||
let first_arg = ($ops | split row " " | get -o 0)
|
||
if ($first_arg | is-not-empty) and $first_arg not-in ["create", "delete", "list", "generate", "check-updates", "help"] {
|
||
# Check if taskserv exists
|
||
let available_taskservs = (^$env.PROVISIONING_NAME module discover taskservs | from json)
|
||
if $first_arg not-in $available_taskservs {
|
||
print $"❌ Unknown taskserv: ($first_arg)"
|
||
print ""
|
||
print "Available taskservs:"
|
||
$available_taskservs | each { |ts| print $" • ($ts)" }
|
||
exit 1
|
||
}
|
||
}
|
||
|
||
let args = build_module_args $flags $ops
|
||
run_module $args "taskserv" --exec
|
||
}
|
||
</code></pre>
|
||
<h2 id="working-with-flags"><a class="header" href="#working-with-flags">Working with Flags</a></h2>
|
||
<h3 id="using-centralized-flag-handling"><a class="header" href="#using-centralized-flag-handling">Using Centralized Flag Handling</a></h3>
|
||
<p>The <code>flags.nu</code> module provides centralized flag handling:</p>
|
||
<pre><code class="language-nushell"># Parse all flags into normalized record
|
||
let parsed_flags = (parse_common_flags {
|
||
version: $version, v: $v, info: $info,
|
||
debug: $debug, check: $check, yes: $yes,
|
||
wait: $wait, infra: $infra, # ... etc
|
||
})
|
||
|
||
# Build argument string for module execution
|
||
let args = build_module_args $parsed_flags $ops
|
||
|
||
# Set environment variables based on flags
|
||
set_debug_env $parsed_flags
|
||
</code></pre>
|
||
<h3 id="available-flag-parsing"><a class="header" href="#available-flag-parsing">Available Flag Parsing</a></h3>
|
||
<p>The <code>parse_common_flags</code> function normalizes these flags:</p>
|
||
<div class="table-wrapper"><table><thead><tr><th>Flag Record Field</th><th>Description</th></tr></thead><tbody>
|
||
<tr><td><code>show_version</code></td><td>Version display (<code>--version</code>, <code>-v</code>)</td></tr>
|
||
<tr><td><code>show_info</code></td><td>Info display (<code>--info</code>, <code>-i</code>)</td></tr>
|
||
<tr><td><code>show_about</code></td><td>About display (<code>--about</code>, <code>-a</code>)</td></tr>
|
||
<tr><td><code>debug_mode</code></td><td>Debug mode (<code>--debug</code>, <code>-x</code>)</td></tr>
|
||
<tr><td><code>check_mode</code></td><td>Check mode (<code>--check</code>, <code>-c</code>)</td></tr>
|
||
<tr><td><code>auto_confirm</code></td><td>Auto-confirm (<code>--yes</code>, <code>-y</code>)</td></tr>
|
||
<tr><td><code>wait</code></td><td>Wait for completion (<code>--wait</code>, <code>-w</code>)</td></tr>
|
||
<tr><td><code>keep_storage</code></td><td>Keep storage (<code>--keepstorage</code>)</td></tr>
|
||
<tr><td><code>infra</code></td><td>Infrastructure name (<code>--infra</code>)</td></tr>
|
||
<tr><td><code>outfile</code></td><td>Output file (<code>--outfile</code>)</td></tr>
|
||
<tr><td><code>output_format</code></td><td>Output format (<code>--out</code>)</td></tr>
|
||
<tr><td><code>template</code></td><td>Template name (<code>--template</code>)</td></tr>
|
||
<tr><td><code>select</code></td><td>Selection (<code>--select</code>)</td></tr>
|
||
<tr><td><code>settings</code></td><td>Settings file (<code>--settings</code>)</td></tr>
|
||
<tr><td><code>new_infra</code></td><td>New infra name (<code>--new</code>)</td></tr>
|
||
</tbody></table>
|
||
</div>
|
||
<h3 id="adding-new-flags"><a class="header" href="#adding-new-flags">Adding New Flags</a></h3>
|
||
<p>If you need to add a new flag:</p>
|
||
<ol>
|
||
<li><strong>Update main <code>provisioning</code> file</strong> to accept the flag</li>
|
||
<li><strong>Update <code>flags.nu:parse_common_flags</code></strong> to normalize it</li>
|
||
<li><strong>Update <code>flags.nu:build_module_args</code></strong> to pass it to modules</li>
|
||
</ol>
|
||
<p><strong>Example: Adding <code>--timeout</code> flag</strong></p>
|
||
<pre><code class="language-nushell"># 1. In provisioning main file (parameter list)
|
||
def main [
|
||
# ... existing parameters
|
||
--timeout: int = 300 # Timeout in seconds
|
||
# ... rest of parameters
|
||
] {
|
||
# ... existing code
|
||
let parsed_flags = (parse_common_flags {
|
||
# ... existing flags
|
||
timeout: $timeout
|
||
})
|
||
}
|
||
|
||
# 2. In flags.nu:parse_common_flags
|
||
export def parse_common_flags [flags: record]: nothing -> record {
|
||
{
|
||
# ... existing normalizations
|
||
timeout: ($flags.timeout? | default 300)
|
||
}
|
||
}
|
||
|
||
# 3. In flags.nu:build_module_args
|
||
export def build_module_args [flags: record, extra: string = ""]: nothing -> string {
|
||
# ... existing code
|
||
let str_timeout = if ($flags.timeout != 300) { $"--timeout ($flags.timeout) " } else { "" }
|
||
# ... rest of function
|
||
$"($extra) ($use_check)($use_yes)($use_wait)($str_timeout)..."
|
||
}
|
||
</code></pre>
|
||
<h2 id="adding-new-shortcuts"><a class="header" href="#adding-new-shortcuts">Adding New Shortcuts</a></h2>
|
||
<h3 id="shortcut-naming-conventions"><a class="header" href="#shortcut-naming-conventions">Shortcut Naming Conventions</a></h3>
|
||
<ul>
|
||
<li><strong>1-2 letters</strong>: Ultra-short for common commands (<code>s</code> for server, <code>ws</code> for workspace)</li>
|
||
<li><strong>3-4 letters</strong>: Abbreviations (<code>orch</code> for orchestrator, <code>tmpl</code> for template)</li>
|
||
<li><strong>Aliases</strong>: Alternative names (<code>task</code> for taskserv, <code>flow</code> for workflow)</li>
|
||
</ul>
|
||
<h3 id="example-adding-a-new-shortcut"><a class="header" href="#example-adding-a-new-shortcut">Example: Adding a New Shortcut</a></h3>
|
||
<p>Edit <code>provisioning/core/nulib/main_provisioning/dispatcher.nu</code>:</p>
|
||
<pre><code class="language-nushell">export def get_command_registry []: nothing -> record {
|
||
{
|
||
# ... existing shortcuts
|
||
|
||
# Add your new shortcut
|
||
"db" => "infrastructure database" # New: db command
|
||
"database" => "infrastructure database" # Full name
|
||
|
||
# ... rest of registry
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><strong>Important</strong>: After adding a shortcut, update the help system in <code>help_system.nu</code> to document it.</p>
|
||
<h2 id="testing-your-changes"><a class="header" href="#testing-your-changes">Testing Your Changes</a></h2>
|
||
<h3 id="running-the-test-suite"><a class="header" href="#running-the-test-suite">Running the Test Suite</a></h3>
|
||
<pre><code class="language-bash"># Run comprehensive test suite
|
||
nu tests/test_provisioning_refactor.nu
|
||
</code></pre>
|
||
<h3 id="test-coverage"><a class="header" href="#test-coverage">Test Coverage</a></h3>
|
||
<p>The test suite validates:</p>
|
||
<ul>
|
||
<li>✅ Main help display</li>
|
||
<li>✅ Category help (infrastructure, orchestration, development, workspace)</li>
|
||
<li>✅ Bi-directional help routing</li>
|
||
<li>✅ All command shortcuts</li>
|
||
<li>✅ Category shortcut help</li>
|
||
<li>✅ Command routing to correct handlers</li>
|
||
</ul>
|
||
<h3 id="adding-tests-for-your-changes"><a class="header" href="#adding-tests-for-your-changes">Adding Tests for Your Changes</a></h3>
|
||
<p>Edit <code>tests/test_provisioning_refactor.nu</code>:</p>
|
||
<pre><code class="language-nushell"># Add your test function
|
||
export def test_my_new_feature [] {
|
||
print "\n🧪 Testing my new feature..."
|
||
|
||
let output = (run_provisioning "my-command" "test")
|
||
assert_contains $output "Expected Output" "My command works"
|
||
}
|
||
|
||
# Add to main test runner
|
||
export def main [] {
|
||
# ... existing tests
|
||
|
||
let results = [
|
||
# ... existing test calls
|
||
(try { test_my_new_feature; "passed" } catch { "failed" })
|
||
]
|
||
|
||
# ... rest of main
|
||
}
|
||
</code></pre>
|
||
<h3 id="manual-testing"><a class="header" href="#manual-testing">Manual Testing</a></h3>
|
||
<pre><code class="language-bash"># Test command execution
|
||
provisioning/core/cli/provisioning my-command test --check
|
||
|
||
# Test with debug mode
|
||
provisioning/core/cli/provisioning --debug my-command test
|
||
|
||
# Test help
|
||
provisioning/core/cli/provisioning my-command help
|
||
provisioning/core/cli/provisioning help my-command # Bi-directional
|
||
</code></pre>
|
||
<h2 id="common-patterns"><a class="header" href="#common-patterns">Common Patterns</a></h2>
|
||
<h3 id="pattern-1-simple-command-handler"><a class="header" href="#pattern-1-simple-command-handler">Pattern 1: Simple Command Handler</a></h3>
|
||
<p><strong>Use Case</strong>: Command just needs to execute a module with standard flags</p>
|
||
<pre><code class="language-nushell">def handle_simple_command [ops: string, flags: record] {
|
||
let args = build_module_args $flags $ops
|
||
run_module $args "module_name" --exec
|
||
}
|
||
</code></pre>
|
||
<h3 id="pattern-2-command-with-validation"><a class="header" href="#pattern-2-command-with-validation">Pattern 2: Command with Validation</a></h3>
|
||
<p><strong>Use Case</strong>: Need to validate input before execution</p>
|
||
<pre><code class="language-nushell">def handle_validated_command [ops: string, flags: record] {
|
||
# Validate
|
||
let first_arg = ($ops | split row " " | get -o 0)
|
||
if ($first_arg | is-empty) {
|
||
print "❌ Missing required argument"
|
||
print "Usage: provisioning command <arg>"
|
||
exit 1
|
||
}
|
||
|
||
# Execute
|
||
let args = build_module_args $flags $ops
|
||
run_module $args "module_name" --exec
|
||
}
|
||
</code></pre>
|
||
<h3 id="pattern-3-command-with-subcommands"><a class="header" href="#pattern-3-command-with-subcommands">Pattern 3: Command with Subcommands</a></h3>
|
||
<p><strong>Use Case</strong>: Command has multiple subcommands (like <code>server create</code>, <code>server delete</code>)</p>
|
||
<pre><code class="language-nushell">def handle_complex_command [ops: string, flags: record] {
|
||
let subcommand = ($ops | split row " " | get -o 0)
|
||
let rest_ops = ($ops | split row " " | skip 1 | str join " ")
|
||
|
||
match $subcommand {
|
||
"create" => { handle_create $rest_ops $flags }
|
||
"delete" => { handle_delete $rest_ops $flags }
|
||
"list" => { handle_list $rest_ops $flags }
|
||
_ => {
|
||
print "❌ Unknown subcommand: $subcommand"
|
||
print "Available: create, delete, list"
|
||
exit 1
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<h3 id="pattern-4-command-with-flag-based-routing"><a class="header" href="#pattern-4-command-with-flag-based-routing">Pattern 4: Command with Flag-Based Routing</a></h3>
|
||
<p><strong>Use Case</strong>: Command behavior changes based on flags</p>
|
||
<pre><code class="language-nushell">def handle_flag_routed_command [ops: string, flags: record] {
|
||
if $flags.check_mode {
|
||
# Dry-run mode
|
||
print "🔍 Check mode: simulating command..."
|
||
let args = build_module_args $flags $ops
|
||
run_module $args "module_name" # No --exec, returns output
|
||
} else {
|
||
# Normal execution
|
||
let args = build_module_args $flags $ops
|
||
run_module $args "module_name" --exec
|
||
}
|
||
}
|
||
</code></pre>
|
||
<h2 id="best-practices"><a class="header" href="#best-practices">Best Practices</a></h2>
|
||
<h3 id="1-keep-handlers-focused"><a class="header" href="#1-keep-handlers-focused">1. Keep Handlers Focused</a></h3>
|
||
<p>Each handler should do <strong>one thing well</strong>:</p>
|
||
<ul>
|
||
<li>✅ Good: <code>handle_server</code> manages all server operations</li>
|
||
<li>❌ Bad: <code>handle_server</code> also manages clusters and taskservs</li>
|
||
</ul>
|
||
<h3 id="2-use-descriptive-error-messages"><a class="header" href="#2-use-descriptive-error-messages">2. Use Descriptive Error Messages</a></h3>
|
||
<pre><code class="language-nushell"># ❌ Bad
|
||
print "Error"
|
||
|
||
# ✅ Good
|
||
print "❌ Unknown taskserv: kubernetes-invalid"
|
||
print ""
|
||
print "Available taskservs:"
|
||
print " • kubernetes"
|
||
print " • containerd"
|
||
print " • cilium"
|
||
print ""
|
||
print "Use 'provisioning taskserv list' to see all available taskservs"
|
||
</code></pre>
|
||
<h3 id="3-leverage-centralized-functions"><a class="header" href="#3-leverage-centralized-functions">3. Leverage Centralized Functions</a></h3>
|
||
<p>Don’t repeat code - use centralized functions:</p>
|
||
<pre><code class="language-nushell"># ❌ Bad: Repeating flag handling
|
||
def handle_bad [ops: string, flags: record] {
|
||
let use_check = if $flags.check_mode { "--check " } else { "" }
|
||
let use_yes = if $flags.auto_confirm { "--yes " } else { "" }
|
||
let str_infra = if ($flags.infra | is-not-empty) { $"--infra ($flags.infra) " } else { "" }
|
||
# ... 10 more lines of flag handling
|
||
run_module $"($ops) ($use_check)($use_yes)($str_infra)..." "module" --exec
|
||
}
|
||
|
||
# ✅ Good: Using centralized function
|
||
def handle_good [ops: string, flags: record] {
|
||
let args = build_module_args $flags $ops
|
||
run_module $args "module" --exec
|
||
}
|
||
</code></pre>
|
||
<h3 id="4-document-your-changes"><a class="header" href="#4-document-your-changes">4. Document Your Changes</a></h3>
|
||
<p>Update relevant documentation:</p>
|
||
<ul>
|
||
<li><strong>ADR-006</strong>: If architectural changes</li>
|
||
<li><strong>CLAUDE.md</strong>: If new commands or shortcuts</li>
|
||
<li><strong>help_system.nu</strong>: If new categories or commands</li>
|
||
<li><strong>This guide</strong>: If new patterns or conventions</li>
|
||
</ul>
|
||
<h3 id="5-test-thoroughly"><a class="header" href="#5-test-thoroughly">5. Test Thoroughly</a></h3>
|
||
<p>Before committing:</p>
|
||
<ul>
|
||
<li><input disabled="" type="checkbox"/>
|
||
Run test suite: <code>nu tests/test_provisioning_refactor.nu</code></li>
|
||
<li><input disabled="" type="checkbox"/>
|
||
Test manual execution</li>
|
||
<li><input disabled="" type="checkbox"/>
|
||
Test with <code>--check</code> flag</li>
|
||
<li><input disabled="" type="checkbox"/>
|
||
Test with <code>--debug</code> flag</li>
|
||
<li><input disabled="" type="checkbox"/>
|
||
Test help: both <code>provisioning cmd help</code> and <code>provisioning help cmd</code></li>
|
||
<li><input disabled="" type="checkbox"/>
|
||
Test shortcuts</li>
|
||
</ul>
|
||
<h2 id="troubleshooting"><a class="header" href="#troubleshooting">Troubleshooting</a></h2>
|
||
<h3 id="issue-module-not-found"><a class="header" href="#issue-module-not-found">Issue: “Module not found”</a></h3>
|
||
<p><strong>Cause</strong>: Incorrect import path in handler</p>
|
||
<p><strong>Fix</strong>: Use relative imports with <code>.nu</code> extension:</p>
|
||
<pre><code class="language-nushell"># ✅ Correct
|
||
use ../flags.nu *
|
||
use ../../lib_provisioning *
|
||
|
||
# ❌ Wrong
|
||
use ../main_provisioning/flags *
|
||
use lib_provisioning *
|
||
</code></pre>
|
||
<h3 id="issue-parse-mismatch-expected-colon"><a class="header" href="#issue-parse-mismatch-expected-colon">Issue: “Parse mismatch: expected colon”</a></h3>
|
||
<p><strong>Cause</strong>: Missing type signature format</p>
|
||
<p><strong>Fix</strong>: Use proper Nushell 0.107 type signature:</p>
|
||
<pre><code class="language-nushell"># ✅ Correct
|
||
export def my_function [param: string]: nothing -> string {
|
||
"result"
|
||
}
|
||
|
||
# ❌ Wrong
|
||
export def my_function [param: string] -> string {
|
||
"result"
|
||
}
|
||
</code></pre>
|
||
<h3 id="issue-command-not-routing-correctly"><a class="header" href="#issue-command-not-routing-correctly">Issue: “Command not routing correctly”</a></h3>
|
||
<p><strong>Cause</strong>: Shortcut not in command registry</p>
|
||
<p><strong>Fix</strong>: Add to <code>dispatcher.nu:get_command_registry</code>:</p>
|
||
<pre><code class="language-nushell">"myshortcut" => "domain command"
|
||
</code></pre>
|
||
<h3 id="issue-flags-not-being-passed"><a class="header" href="#issue-flags-not-being-passed">Issue: “Flags not being passed”</a></h3>
|
||
<p><strong>Cause</strong>: Not using <code>build_module_args</code></p>
|
||
<p><strong>Fix</strong>: Use centralized flag builder:</p>
|
||
<pre><code class="language-nushell">let args = build_module_args $flags $ops
|
||
run_module $args "module" --exec
|
||
</code></pre>
|
||
<h2 id="quick-reference"><a class="header" href="#quick-reference">Quick Reference</a></h2>
|
||
<h3 id="file-locations"><a class="header" href="#file-locations">File Locations</a></h3>
|
||
<pre><code>provisioning/core/nulib/
|
||
├── provisioning - Main entry, flag definitions
|
||
├── main_provisioning/
|
||
│ ├── flags.nu - Flag parsing (parse_common_flags, build_module_args)
|
||
│ ├── dispatcher.nu - Routing (get_command_registry, dispatch_command)
|
||
│ ├── help_system.nu - Help (provisioning-help, help-*)
|
||
│ └── commands/ - Domain handlers (handle_*_command)
|
||
tests/
|
||
└── test_provisioning_refactor.nu - Test suite
|
||
docs/
|
||
├── architecture/
|
||
│ └── ADR-006-provisioning-cli-refactoring.md - Architecture docs
|
||
└── development/
|
||
└── COMMAND_HANDLER_GUIDE.md - This guide
|
||
</code></pre>
|
||
<h3 id="key-functions"><a class="header" href="#key-functions">Key Functions</a></h3>
|
||
<pre><code class="language-nushell"># In flags.nu
|
||
parse_common_flags [flags: record]: nothing -> record
|
||
build_module_args [flags: record, extra: string = ""]: nothing -> string
|
||
set_debug_env [flags: record]
|
||
get_debug_flag [flags: record]: nothing -> string
|
||
|
||
# In dispatcher.nu
|
||
get_command_registry []: nothing -> record
|
||
dispatch_command [args: list, flags: record]
|
||
|
||
# In help_system.nu
|
||
provisioning-help [category?: string]: nothing -> string
|
||
help-infrastructure []: nothing -> string
|
||
help-orchestration []: nothing -> string
|
||
# ... (one for each category)
|
||
|
||
# In commands/*.nu
|
||
handle_*_command [command: string, ops: string, flags: record]
|
||
# Example: handle_infrastructure_command, handle_workspace_command
|
||
</code></pre>
|
||
<h3 id="testing-commands"><a class="header" href="#testing-commands">Testing Commands</a></h3>
|
||
<pre><code class="language-bash"># Run full test suite
|
||
nu tests/test_provisioning_refactor.nu
|
||
|
||
# Test specific command
|
||
provisioning/core/cli/provisioning my-command test --check
|
||
|
||
# Test with debug
|
||
provisioning/core/cli/provisioning --debug my-command test
|
||
|
||
# Test help
|
||
provisioning/core/cli/provisioning help my-command
|
||
provisioning/core/cli/provisioning my-command help # Bi-directional
|
||
</code></pre>
|
||
<h2 id="further-reading"><a class="header" href="#further-reading">Further Reading</a></h2>
|
||
<ul>
|
||
<li><strong><a href="../architecture/adr/ADR-006-provisioning-cli-refactoring.html">ADR-006: CLI Refactoring</a></strong> - Complete architectural decision record</li>
|
||
<li><strong><a href="project-structure.html">Project Structure</a></strong> - Overall project organization</li>
|
||
<li><strong><a href="workflow.html">Workflow Development</a></strong> - Workflow system architecture</li>
|
||
<li><strong><a href="integration.html">Development Integration</a></strong> - Integration patterns</li>
|
||
</ul>
|
||
<h2 id="contributing"><a class="header" href="#contributing">Contributing</a></h2>
|
||
<p>When contributing command handler changes:</p>
|
||
<ol>
|
||
<li><strong>Follow existing patterns</strong> - Use the patterns in this guide</li>
|
||
<li><strong>Update documentation</strong> - Keep docs in sync with code</li>
|
||
<li><strong>Add tests</strong> - Cover your new functionality</li>
|
||
<li><strong>Run test suite</strong> - Ensure nothing breaks</li>
|
||
<li><strong>Update CLAUDE.md</strong> - Document new commands/shortcuts</li>
|
||
</ol>
|
||
<p>For questions or issues, refer to ADR-006 or ask the team.</p>
|
||
<hr />
|
||
<p><em>This guide is part of the provisioning project documentation. Last updated: 2025-09-30</em></p>
|
||
|
||
</main>
|
||
|
||
<nav class="nav-wrapper" aria-label="Page navigation">
|
||
<!-- Mobile navigation buttons -->
|
||
<a rel="prev" href="../development/TASKSERV_QUICK_GUIDE.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||
<i class="fa fa-angle-left"></i>
|
||
</a>
|
||
|
||
<a rel="next prefetch" href="../development/configuration.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||
<i class="fa fa-angle-right"></i>
|
||
</a>
|
||
|
||
<div style="clear: both"></div>
|
||
</nav>
|
||
</div>
|
||
</div>
|
||
|
||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||
<a rel="prev" href="../development/TASKSERV_QUICK_GUIDE.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||
<i class="fa fa-angle-left"></i>
|
||
</a>
|
||
|
||
<a rel="next prefetch" href="../development/configuration.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||
<i class="fa fa-angle-right"></i>
|
||
</a>
|
||
</nav>
|
||
|
||
</div>
|
||
|
||
<!-- Livereload script (if served using the cli tool) -->
|
||
<script>
|
||
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||
const wsAddress = wsProtocol + "//" + location.host + "/" + "__livereload";
|
||
const socket = new WebSocket(wsAddress);
|
||
socket.onmessage = function (event) {
|
||
if (event.data === "reload") {
|
||
socket.close();
|
||
location.reload();
|
||
}
|
||
};
|
||
|
||
window.onbeforeunload = function() {
|
||
socket.close();
|
||
}
|
||
</script>
|
||
|
||
|
||
|
||
<script>
|
||
window.playground_copyable = true;
|
||
</script>
|
||
|
||
|
||
<script src="../elasticlunr.min.js"></script>
|
||
<script src="../mark.min.js"></script>
|
||
<script src="../searcher.js"></script>
|
||
|
||
<script src="../clipboard.min.js"></script>
|
||
<script src="../highlight.js"></script>
|
||
<script src="../book.js"></script>
|
||
|
||
<!-- Custom JS scripts -->
|
||
|
||
|
||
</div>
|
||
</body>
|
||
</html>
|