merge(refactor/lazy-loading): ADR-025 lazy-loading complete

# Conflicts:
#	CHANGELOG.md
#	versions.ncl
This commit is contained in:
Jesús Pérez 2026-04-17 23:14:02 +01:00
commit 85ab055ccb
Signed by: jesus
GPG key ID: 9F243E355E0BC939
557 changed files with 51964 additions and 32767 deletions

View file

@ -3,3 +3,35 @@
use toolkit.nu fmt use toolkit.nu fmt
fmt # --check --verbose fmt # --check --verbose
# ADR-025: Block root star-imports of lib_provisioning / main_provisioning.
# A line matching `use lib_provisioning *` or `use main_provisioning *` at the
# start of a file (top-level) reintroduces the transitive parse cost this
# refactor was designed to eliminate. All imports must be selective.
let staged = (git diff --cached --name-only | lines | where { str ends-with ".nu" })
if ($staged | length) > 0 {
let violations = (
$staged
| each {|f|
let hits = (
do { git show $":($f)" } | complete
| if $in.exit_code == 0 { $in.stdout } else { "" }
| lines
| enumerate
| where { $it.item | str starts-with "use lib_provisioning *" or $it.item | str starts-with "use main_provisioning *" }
| each {|row| $" ($f):($row.index + 1): ($row.item | str trim)"}
)
$hits
}
| flatten
)
if ($violations | length) > 0 {
print "❌ ADR-025 star-import violation — selective imports required:"
for v in $violations { print $v }
print ""
print "Replace `use lib_provisioning *` with explicit `use lib_provisioning/path/to/module.nu [sym1 sym2]`"
exit 1
}
}

View file

@ -18,9 +18,8 @@ export def fmt [
} }
if $check { if $check {
try { let result = (do { ^cargo fmt --all -- --check } | complete)
^cargo fmt --all -- --check if $result.exit_code != 0 {
} catch {
error make --unspanned { error make --unspanned {
msg: $"\nplease run ('toolkit fmt' | pretty-format-command) to fix formatting!" msg: $"\nplease run ('toolkit fmt' | pretty-format-command) to fix formatting!"
} }
@ -42,7 +41,7 @@ export def clippy [
} }
# If changing these settings also change CI settings in .github/workflows/ci.yml # If changing these settings also change CI settings in .github/workflows/ci.yml
try {( let result1 = (do {
^cargo clippy ^cargo clippy
--workspace --workspace
--exclude nu_plugin_* --exclude nu_plugin_*
@ -51,13 +50,19 @@ export def clippy [
-D warnings -D warnings
-D clippy::unwrap_used -D clippy::unwrap_used
-D clippy::unchecked_duration_subtraction -D clippy::unchecked_duration_subtraction
) } | complete)
if $result1.exit_code != 0 {
error make --unspanned {
msg: $"\nplease fix the above ('clippy' | pretty-format-command) errors before continuing!"
}
}
if $verbose { if $verbose {
print $"running ('toolkit clippy' | pretty-format-command) on tests" print $"running ('toolkit clippy' | pretty-format-command) on tests"
} }
# In tests we don't have to deny unwrap # In tests we don't have to deny unwrap
( let result2 = (do {
^cargo clippy ^cargo clippy
--tests --tests
--workspace --workspace
@ -65,21 +70,27 @@ export def clippy [
--features ($features | default [] | str join ",") --features ($features | default [] | str join ",")
-- --
-D warnings -D warnings
) } | complete)
if $result2.exit_code != 0 {
error make --unspanned {
msg: $"\nplease fix the above ('clippy' | pretty-format-command) errors before continuing!"
}
}
if $verbose { if $verbose {
print $"running ('toolkit clippy' | pretty-format-command) on plugins" print $"running ('toolkit clippy' | pretty-format-command) on plugins"
} }
( let result3 = (do {
^cargo clippy ^cargo clippy
--package nu_plugin_* --package nu_plugin_*
-- --
-D warnings -D warnings
-D clippy::unwrap_used -D clippy::unwrap_used
-D clippy::unchecked_duration_subtraction -D clippy::unchecked_duration_subtraction
) } | complete)
} catch { if $result3.exit_code != 0 {
error make --unspanned { error make --unspanned {
msg: $"\nplease fix the above ('clippy' | pretty-format-command) errors before continuing!" msg: $"\nplease fix the above ('clippy' | pretty-format-command) errors before continuing!"
} }
@ -262,20 +273,18 @@ export def "check pr" [
$env.LANG = 'en_US.UTF-8' $env.LANG = 'en_US.UTF-8'
$env.LANGUAGE = 'en' $env.LANGUAGE = 'en'
try { let fmt_result = (do { fmt --check --verbose } | complete)
fmt --check --verbose if $fmt_result.exit_code != 0 {
} catch {
return (report --fail-fmt) return (report --fail-fmt)
} }
try { let clippy_result = (do { clippy --features $features --verbose } | complete)
clippy --features $features --verbose if $clippy_result.exit_code != 0 {
} catch {
return (report --fail-clippy) return (report --fail-clippy)
} }
print $"running ('toolkit test' | pretty-format-command)" print $"running ('toolkit test' | pretty-format-command)"
try { let test_result = (do {
if $fast { if $fast {
if ($features | is-empty) { if ($features | is-empty) {
test --workspace --fast test --workspace --fast
@ -289,14 +298,15 @@ export def "check pr" [
test --features $features test --features $features
} }
} }
} catch { } | complete)
if $test_result.exit_code != 0 {
return (report --fail-test) return (report --fail-test)
} }
print $"running ('toolkit test stdlib' | pretty-format-command)" print $"running ('toolkit test stdlib' | pretty-format-command)"
try { let stdlib_result = (do { test stdlib } | complete)
test stdlib if $stdlib_result.exit_code != 0 {
} catch {
return (report --fail-test-stdlib) return (report --fail-test-stdlib)
} }
@ -425,11 +435,12 @@ export def "add plugins" [] {
} }
for plugin in $plugins { for plugin in $plugins {
try { let plugin_result = (do {
print $"> plugin add ($plugin)" print $"> plugin add ($plugin)"
plugin add $plugin plugin add $plugin
} catch { |err| } | complete)
print -e $"(ansi rb)Failed to add ($plugin):\n($err.msg)(ansi reset)" if $plugin_result.exit_code != 0 {
print -e $"(ansi rb)Failed to add ($plugin):\n($plugin_result.stderr)(ansi reset)"
} }
} }

7
.gitignore vendored
View file

@ -1,17 +1,18 @@
.p .p
.claude .claude
.vscode .vscode
.shellcheckrc .shellcheckrc
.coder .coder
.migration .migration
.zed .zed
ai_demo.nu # ai_demo.nu
CLAUDE.md CLAUDE.md
.cache .cache
.coder .coder
wrks .wrks
ROOT ROOT
OLD OLD
old-config
plugins/nushell-plugins plugins/nushell-plugins
# Generated by Cargo # Generated by Cargo
# will have compiled files and executables # will have compiled files and executables

96
.markdownlint-cli2.jsonc Normal file
View file

@ -0,0 +1,96 @@
// Markdownlint-cli2 Configuration
// Documentation quality enforcement aligned with CLAUDE.md guidelines
// See: https://github.com/igorshubovych/markdownlint-cli2
{
"config": {
"default": true,
// Headings - enforce proper hierarchy
"MD001": false, // heading-increment (relaxed - allow flexibility)
"MD026": { "punctuation": ".,;:!?" }, // heading-punctuation
// Lists - enforce consistency
"MD004": { "style": "consistent" }, // ul-style (consistent list markers)
"MD005": false, // inconsistent-indentation (relaxed)
"MD007": { "indent": 2 }, // ul-indent
"MD029": false, // ol-prefix (allow flexible list numbering)
"MD030": { "ul_single": 1, "ol_single": 1, "ul_multi": 1, "ol_multi": 1 },
// Code blocks - fenced only
"MD046": { "style": "fenced" }, // code-block-style
// Formatting - strict whitespace
"MD009": true, // no-hard-tabs
"MD010": true, // hard-tabs
"MD011": true, // reversed-link-syntax
"MD018": true, // no-missing-space-atx
"MD019": true, // no-multiple-space-atx
"MD020": true, // no-missing-space-closed-atx
"MD021": true, // no-multiple-space-closed-atx
"MD023": true, // heading-starts-line
"MD027": true, // no-multiple-spaces-blockquote
"MD037": true, // no-space-in-emphasis
"MD039": true, // no-space-in-links
// Trailing content
"MD012": false, // no-multiple-blanks (relaxed - allow formatting space)
"MD024": false, // no-duplicate-heading (too strict for docs)
"MD028": false, // no-blanks-blockquote (relaxed)
"MD047": true, // single-trailing-newline
// Links and references
"MD034": true, // no-bare-urls (links must be formatted)
"MD040": true, // fenced-code-language (code blocks need language)
"MD042": true, // no-empty-links
// HTML - allow for documentation formatting and images
"MD033": { "allowed_elements": ["br", "hr", "details", "summary", "p", "img"] },
// Line length - relaxed for technical documentation
"MD013": {
"line_length": 150,
"heading_line_length": 150,
"code_block_line_length": 150,
"code_blocks": true,
"tables": true,
"headers": true,
"headers_line_length": 150,
"strict": false,
"stern": false
},
// Images
"MD045": true, // image-alt-text
// Disable rules that conflict with relaxed style
"MD003": false, // consistent-indentation
"MD041": false, // first-line-heading
"MD025": false, // single-h1 / multiple-top-level-headings
"MD022": false, // blanks-around-headings (flexible spacing)
"MD032": false, // blanks-around-lists (flexible spacing)
"MD035": false, // hr-style (consistent)
"MD036": false, // no-emphasis-as-heading
"MD044": false // proper-names
},
// Documentation patterns
"globs": [
"docs/**/*.md",
"!docs/node_modules/**",
"!docs/build/**"
],
// Ignore build artifacts, external content, and operational directories
"ignores": [
"node_modules/**",
"target/**",
".git/**",
"build/**",
"dist/**",
".coder/**",
".claude/**",
".wrks/**",
".vale/**"
]
}

143
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,143 @@
# Pre-commit Framework Configuration
# Generated by dev-system/ci
# Configures git pre-commit hooks for Rust projects
repos:
# ============================================================================
# Rust Hooks (COMMENTED OUT - Not used in this repo)
# ============================================================================
# - repo: local
# hooks:
# - id: rust-fmt
# name: Rust formatting (cargo +nightly fmt)
# entry: bash -c 'cargo +nightly fmt --all -- --check'
# language: system
# types: [rust]
# pass_filenames: false
# stages: [pre-commit]
#
# - id: rust-clippy
# name: Rust linting (cargo clippy)
# entry: bash -c 'cargo clippy --all-targets -- -D warnings'
# language: system
# types: [rust]
# pass_filenames: false
# stages: [pre-commit]
#
# - id: rust-test
# name: Rust tests
# entry: bash -c 'cargo test --workspace'
# language: system
# types: [rust]
# pass_filenames: false
# stages: [pre-push]
#
# - id: cargo-deny
# name: Cargo deny (licenses & advisories)
# entry: bash -c 'cargo deny check licenses advisories'
# language: system
# pass_filenames: false
# stages: [pre-push]
# ============================================================================
# Nushell Hooks (ACTIVE)
# ============================================================================
- repo: local
hooks:
- id: nushell-check
name: Nushell validation (nu --ide-check)
entry: >-
bash -c 'for f in $(git diff --cached --name-only --diff-filter=ACM | grep "\.nu$"); do
echo "Checking: $f"; nu --ide-check 100 "$f" || exit 1; done'
language: system
types: [file]
files: \.nu$
pass_filenames: false
stages: [pre-commit]
# ============================================================================
# Nickel Hooks (ACTIVE)
# ============================================================================
- repo: local
hooks:
- id: nickel-typecheck
name: Nickel type checking
entry: >-
bash -c 'export NICKEL_IMPORT_PATH="../:."; for f in $(git diff --cached --name-only --diff-filter=ACM | grep "\.ncl$"); do
echo "Checking: $f"; nickel typecheck "$f" || exit 1; done'
language: system
types: [file]
files: \.ncl$
pass_filenames: false
stages: [pre-commit]
# ============================================================================
# Bash Hooks (optional - enable if using Bash)
# ============================================================================
# - repo: local
# hooks:
# - id: shellcheck
# name: Shellcheck (bash linting)
# entry: shellcheck
# language: system
# types: [shell]
# stages: [commit]
#
# - id: shfmt
# name: Shell script formatting
# entry: bash -c 'shfmt -i 2 -d'
# language: system
# types: [shell]
# stages: [commit]
# ============================================================================
# Markdown Hooks (ACTIVE)
# ============================================================================
- repo: local
hooks:
- id: markdownlint
name: Markdown linting (markdownlint-cli2)
entry: markdownlint-cli2
language: system
types: [markdown]
stages: [pre-commit]
# CRITICAL: markdownlint-cli2 MD040 only checks opening fences for language.
# It does NOT catch malformed closing fences (e.g., ```plaintext) - CommonMark violation.
# This hook is ESSENTIAL to prevent malformed closing fences from entering the repo.
# See: .markdownlint-cli2.jsonc line 22-24 for details.
- id: check-malformed-fences
name: Check malformed closing fences (CommonMark)
entry: bash -c 'cd .. && nu scripts/check-malformed-fences.nu $(git diff --cached --name-only --diff-filter=ACM | grep "\.md$" | grep -v ".coder/" | grep -v ".claude/" | grep -v "old_config/" | tr "\n" " ")'
language: system
types: [markdown]
pass_filenames: false
stages: [pre-commit]
exclude: ^\.coder/|^\.claude/|^old_config/
# ============================================================================
# General Pre-commit Hooks
# ============================================================================
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-added-large-files
args: ['--maxkb=1000']
- id: check-case-conflict
- id: check-merge-conflict
- id: check-toml
# - id: check-yaml
# exclude: ^\.woodpecker/
- id: end-of-file-fixer
exclude: ^(\.coder/|\.wrks/|\.claude/)
- id: trailing-whitespace
exclude: \.md$|^(\.coder/|\.wrks/|\.claude/)
- id: mixed-line-ending
exclude: ^(\.coder/|\.wrks/|\.claude/)

View file

@ -1,71 +1,278 @@
# Provisioning Core - Changes # Provisioning Core - Changelog
**Date**: 2025-12-11 **Date**: 2026-04-17
**Repository**: provisioning/core **Repository**: provisioning/core
**Changes**: CLI, libraries, plugins, and utilities updates **Status**: Nickel IaC (PRIMARY)
--- ---
## 📋 Summary ## 📋 Summary
Updates to core CLI, Nushell libraries, plugins system, and utility scripts for the provisioning core system. Major refactor: three-layer DAG architecture with workspace composition, Unified
Component Architecture (components + workflows + capabilities), Nickel-backed
commands-registry with JSON cache for fast CLI startup, consolidated platform
service manager, and completed Nushell 0.110/0.112 migration (no try/catch, no
bash redirections). TTY stack moved from `shlib/` into `cli/tty-*`. Numerous new
domain modules: `dag`, `components`, `workflow` engine, `images` lifecycle,
workspace state/sync, ontoref queries, FIP handler.
---
## 🔄 Latest Release (2026-04-17)
### Three-Layer DAG Architecture
**Scope**: Workspace composition as a DAG with formula_id::task_name namespacing,
health gates, conditions, and NATS subject emission.
**New files**:
- `nulib/main_provisioning/dag.nu``dag show/validate/export` (DOT/JSON/Mermaid)
- `nulib/lib_provisioning/config/loader/dag.nu` — DAG config loader
- `nulib/taskservs/dag-executor.nu` — taskserv-level DAG execution helper
**Related**: ADR-020 (extension capability declarations), ADR-021 (workspace
composition DAG). Orchestrator consumes composition via
`WorkspaceComposition::into_workflow` and emits NATS events.
### Unified Component Architecture
**Scope**: Components + workflows + capabilities as first-class citizens
(libre-daoshi plan, blocks A-H complete).
**New files**:
- `nulib/components/mod.nu` — component dispatch module
- `nulib/main_provisioning/components.nu``validate capabilities/components`,
`component list/info`
- `nulib/main_provisioning/workflow.nu` — full workflow engine: run/list/status/
validate, topological sort, NATS event emission (+605 lines)
- `nulib/main_provisioning/extensions.nu``extensions capabilities/graph`
- `nulib/main_provisioning/ontoref-queries.nu` — on+re-aware CLI queries
(describe component/databases/namespace/storage/workflow)
### Commands-Registry & Fast-Path Dispatch
**Scope**: Eliminate Nu startup cost on every `prvng` invocation.
**New files**:
- `nulib/commands-registry.ncl` — Nickel command catalog (314 lines)
- `nulib/lib_provisioning/utils/command-registry.nu` — registry accessor
- `nulib/scripts/validate-command.nu` — cache-aware command validator
**Behavior**: `cli/provisioning` reads the JSON cache at
`~/.cache/provisioning/commands-registry.json`, rebuilt automatically via
`nickel export` when the `.ncl` source is newer. Single-char aliases
(`s`, `t`, `c`, `e`, `w`, `j`, `b`, `o`, `a`) are expanded in bash before
dispatch. `nulib/main_provisioning/ADDING_COMMANDS.md` documents the four-step
procedure for new commands.
### Platform Service Manager
**New files**:
- `nulib/lib_provisioning/platform/service-manager.nu` (+573 lines)
- `nulib/lib_provisioning/platform/startup.nu` (+611 lines)
- `nulib/lib_provisioning/utils/service-check.nu` (+255 lines)
**Refactored**: `platform/autostart.nu`, `platform/bootstrap.nu`,
`platform/health.nu`, `platform/target.nu` — unified lifecycle, health probes,
and autostart logic.
### Nushell 0.112.2 Migration
**Scope**: Project-wide refactor driven by `scripts/refactor-try-catch.nu` and
`scripts/refactor-try-catch-simplified.nu` to reach Nushell 0.112.2 compliance.
**Enforced**:
- No `try/catch` — all use `do { } | complete`
- No bash redirections (`2>&1`, `2>/dev/null`)
- External commands prefixed with `^`
- Parenthesized pipelines in `if`
- Type signatures: `def f [x: string]: nothing -> record { }`
### TTY Stack Replacement
**Removed**: `shlib/README.md`, `shlib/auth-login-tty.sh`,
`shlib/mfa-enroll-tty.sh`, `shlib/setup-wizard-tty.sh`.
**Replaced by**:
- `cli/tty-dispatch.sh` (+86 lines) — TTY-safe command dispatcher
- `cli/tty-filter.sh` (+137 lines) — command filter
- `cli/tty-commands.conf` — TTY command manifest
### New Domain Modules
- `nulib/images/` — golden image lifecycle (`create`, `delete`, `list`, `state`,
`update`, `watch`)
- `nulib/workspace/state.nu` (+641 lines) — workspace state model
- `nulib/workspace/sync.nu` (+148 lines) — workspace synchronization
- `nulib/main_provisioning/bootstrap.nu` — platform bootstrap
- `nulib/main_provisioning/cluster-deploy.nu` — component/taskserv dispatch
- `nulib/main_provisioning/fip.nu` (+421 lines) — floating IP handler
- `nulib/main_provisioning/state.nu` — state command
- `nulib/main_provisioning/commands/state.nu`, `commands/build.nu`,
`commands/integrations/auth.nu`, `commands/utilities/alias.nu`
- `nulib/main_provisioning/commands/platform.nu` — major expansion (+874 lines)
### Config Loader Overhaul
- `lib_provisioning/config/loader/core.nu` — slimmed (759 lines of legacy paths)
- `lib_provisioning/config/cache/core.nu` — refactored (454 lines of dead paths)
- `lib_provisioning/config/cache/nickel.nu` — simplified
- Removed: `lib_provisioning/config/loaders/file_loader.nu` (330 lines)
- Added: `config/accessor-minimal.nu`, `config/accessor/functions.nu` helpers
### Scripts & Tooling
- `nulib/scripts/` — query-* family (clusters/infra/providers/servers/taskservs/
workspace-info), validate-command, validate-config
- `scripts/auto-refactor-priority.nu`, `scripts/batch-refactor.sh`
- `scripts/build-nixos-image-remote.sh`, `scripts/deploy-cp-server.sh`
### CLI Modular Subcommands
New top-level Nu modules referenced by the bash dispatcher:
`provisioning-batch.nu`, `provisioning-bootstrap.nu`, `provisioning-cluster.nu`,
`provisioning-component.nu`, `provisioning-extension.nu`, `provisioning-job.nu`,
`provisioning-platform.nu`, `provisioning-server.nu`, `provisioning-state.nu`,
`provisioning-status.nu`, `provisioning-taskserv.nu`, `provisioning-volume.nu`,
`provisioning-workflow.nu`.
### Tests
- `nulib/tests/test_workspace_state.nu` (+351 lines)
- Updates to `test_oci_registry.nu`, `test_services.nu`
### Statistics
| Area | Files | Lines +/ |
| ---- | ----- | --------- |
| DAG + Components + Workflows | 8 | +1800 / 50 |
| Commands-registry + dispatch | 6 | +900 / 200 |
| Platform service manager | 5 | +1700 / 300 |
| Config loader/cache refactor | 10 | +400 / 1500 |
| TTY replacement | 4 | +250 / 515 |
| New subcommand modules | 13 | +1700 / 0 |
| **Total staged** | **242 files** | **+21949 / 6012** |
---
## 🔄 Previous Release (2026-01-14)
### Terminology Migration: Cluster → Taskserv
**Scope**: Complete refactoring across nulib/ modules to standardize on taskserv nomenclature
**Files Updated**:
- `nulib/clusters/handlers.nu` - Handler signature updates, ANSI formatting improvements
- `nulib/clusters/run.nu` - Function parameter and path updates (+326 lines modified)
- `nulib/clusters/utils.nu` - Utility function updates (+144 lines modified)
- `nulib/clusters/discover.nu` - Discovery module refactoring
- `nulib/clusters/load.nu` - Configuration loader updates
- `nulib/ai/query_processor.nu` - AI integration updates
- `nulib/api/routes.nu` - API routing adjustments
- `nulib/api/server.nu` - Server module updates
- `.pre-commit-config.yaml` - Pre-commit hook updates
**Changes**:
- Updated function parameters: `server_cluster_path``server_taskserv_path`
- Updated record fields: `defs.cluster.name``defs.taskserv.name`
- Enhanced output formatting with consistent ANSI styling (yellow_bold, default_dimmed, purple_bold)
- Improved function documentation and import organization
- Pre-commit configuration refinements
**Rationale**: Taskserv better reflects the service-oriented nature of infrastructure components and improves semantic clarity throughout the codebase.
### i18n/Localization System
**New Feature**: Fluent i18n integration for internationalized help system
**Implementation**:
- `nulib/main_provisioning/help_system_fluent.nu` - Fluent-based i18n framework
- Active locale detection from `LANG` environment variable
- Fallback to English (en-US) for missing translations
- Fluent catalog parsing: `locale/{locale}/help.ftl`
- Locale format conversion: `es_ES.UTF-8``es-ES`
**Features**:
- Automatic locale detection from system LANG
- Fluent catalog format support for translations
- Graceful fallback mechanism
- Category-based color formatting (infrastructure, orchestration, development, etc.)
- Tab-separated help column formatting
---
## 📋 Version History
### v1.0.10 (Previous Release)
- Stable release with Nickel IaC support
- Base version with core CLI and library system
### v1.0.11 (Current - 2026-01-14)
- **Cluster → Taskserv** terminology migration
- **Fluent i18n** system documentation
- Enhanced ANSI output formatting
--- ---
## 📁 Changes by Directory ## 📁 Changes by Directory
### cli/ directory ### cli/ directory
Provisioning CLI implementation and commands
- Command implementations **Major Updates (586 lines added to provisioning)**
- CLI utilities
- Command routing and dispatching - Expanded CLI command implementations (+590 lines)
- Help system - Enhanced tools installation system (tools-install: +163 lines)
- Command validation - Improved install script for Nushell environment (install_nu.sh: +31 lines)
- Better CLI routing and command validation
- Help system enhancements for Nickel-aware commands
- Support for Nickel schema evaluation and validation
### nulib/ directory ### nulib/ directory
Nushell libraries and modules (core business logic)
**Key Modules:** **Nushell libraries - Nickel-first architecture**
- `lib_provisioning/` - Main library modules
- config/ - Configuration loading and management
- extensions/ - Extension system
- secrets/ - Secrets management
- infra_validator/ - Infrastructure validation
- ai/ - AI integration documentation
- user/ - User management
- workspace/ - Workspace operations
- cache/ - Caching system
- utils/ - Utility functions
**Workflows:** **Config System**
- Batch operations and orchestration - `config/loader.nu` - Nickel schema loading and evaluation
- Server management - `config/accessor.nu` - Accessor patterns for Nickel fields
- Task service management - `config/cache/` - Cache system optimized for Nickel evaluation
- Cluster operations
- Test environments
**Services:** **AI & Documentation**
- Service management scripts - `ai/README.md` - Nickel IaC patterns
- Task service utilities - `ai/info_about.md` - Nickel-focused documentation
- Infrastructure utilities - `ai/lib.nu` - AI integration for Nickel schema analysis
**Documentation:** **Extension System**
- Library module documentation - `extensions/QUICKSTART.md` - Nickel extension quickstart (+50 lines)
- Extension API quickstart - `extensions/README.md` - Extension system for Nickel (+63 lines)
- Secrets management guide - `extensions/loader_oci.nu` - OCI registry loader (minor updates)
- Service management summary
- Test environments guide **Infrastructure & Validation**
- `infra_validator/rules_engine.nu` - Validation rules for Nickel schemas
- `infra_validator/validator.nu` - Schema validation support
- `loader-minimal.nu` - Minimal loader for lightweight deployments
**Clusters & Workflows**
- `clusters/discover.nu`, `clusters/load.nu`, `clusters/run.nu` - Cluster operations updated
- Plugin definitions updated for Nickel integration (+28-38 lines)
**Documentation**
- `SERVICE_MANAGEMENT_SUMMARY.md` - Expanded service documentation (+90 lines)
- `gitea/IMPLEMENTATION_SUMMARY.md` - Gitea integration guide (+89 lines)
- Extension and validation quickstarts and README updates
### plugins/ directory ### plugins/ directory
Nushell plugins for performance optimization Nushell plugins for performance optimization
**Sub-repositories:** **Sub-repositories:**
- `nushell-plugins/` - Multiple Nushell plugins - `nushell-plugins/` - Multiple Nushell plugins
- `_nu_plugin_inquire/` - Interactive form plugin - `_nu_plugin_inquire/` - Interactive form plugin
- `api_nu_plugin_kcl/` - KCL integration plugin - `api_nu_plugin_nickel/` - Nickel integration plugin
- Additional plugin implementations - Additional plugin implementations
**Plugin Documentation:** **Plugin Documentation:**
- Build summaries - Build summaries
- Installation guides - Installation guides
- Configuration examples - Configuration examples
@ -73,7 +280,9 @@ Nushell plugins for performance optimization
- Fix and limitation reports - Fix and limitation reports
### scripts/ directory ### scripts/ directory
Utility scripts for system operations Utility scripts for system operations
- Build scripts - Build scripts
- Installation scripts - Installation scripts
- Testing scripts - Testing scripts
@ -81,83 +290,92 @@ Utility scripts for system operations
- Infrastructure scripts - Infrastructure scripts
### services/ directory ### services/ directory
Service definitions and configurations Service definitions and configurations
- Service descriptions - Service descriptions
- Service management - Service management
### forminquire/ directory ### forminquire/ directory (ARCHIVED)
Form inquiry interface
- Interactive form system **Status**: DEPRECATED - Archived to `.coder/archive/forminquire/`
- User input handling
**Replacement**: TypeDialog forms (`.typedialog/provisioning/`)
- Legacy: Jinja2-based form system
- Archived: 2025-01-09
- Replaced by: TypeDialog with bash wrappers for TTY-safe input
### Additional Files ### Additional Files
- `README.md` - Core system documentation - `README.md` - Core system documentation
- `versions.k` - Version definitions - `versions.ncl` - Version definitions
- `.gitignore` - Git ignore patterns - `.gitignore` - Git ignore patterns
- `kcl.mod` / `kcl.mod.lock` - KCL module definitions - `nickel.mod` / `nickel.mod.lock` - Nickel module definitions
- `.githooks/` - Git hooks for development - `.githooks/` - Git hooks for development
--- ---
## 📊 Change Statistics ## 📊 Change Statistics
| Category | Files | Status | | Category | Files | Lines Added | Lines Removed | Status |
|----------|-------|--------| | -------- | ----- | ----------- | ------------- | ------ |
| CLI | 8+ | Updated | | CLI | 3 | 780+ | 30+ | Major update |
| Libraries | 20+ | Updated | | Config System | 15+ | 300+ | 200+ | Refactored |
| Plugins | 10+ | Updated | | AI/Docs | 8+ | 350+ | 100+ | Enhanced |
| Scripts | 15+ | Updated | | Extensions | 5+ | 150+ | 50+ | Updated |
| Documentation | 20+ | Updated | | Infrastructure | 8+ | 100+ | 70+ | Updated |
| Clusters/Workflows | 5+ | 80+ | 30+ | Enhanced |
| **Total** | **60+ files** | **1700+ lines** | **500+ lines** | **Complete** |
--- ---
## ✨ Key Areas ## ✨ Key Areas
### CLI System ### CLI System
- Command implementations
- Flag handling and validation
- Help and documentation
- Error handling
### Nushell Libraries - Command implementations with Nickel support
- Configuration management - Tools installation system
- Infrastructure validation - Nushell environment setup
- Extension system - Nickel schema evaluation commands
- Secrets management - Error messages and help text
- Workspace operations - Nickel type checking and validation
- Cache management
### Plugin System ### Config System
- Interactive forms (inquire)
- KCL integration
- Performance optimization
- Plugin registration
### Scripts & Utilities - **Nickel-first loader**: Schema evaluation via config/loader.nu
- Build and distribution - **Optimized caching**: Nickel evaluation cache patterns
- Installation procedures - **Field accessors**: Nickel record manipulation
- Testing utilities - **Schema validation**: Type-safe configuration loading
- Development tools
### AI & Documentation
- AI integration for Nickel IaC
- Extension development guides
- Service management documentation
### Extensions & Infrastructure
- OCI registry loader optimization
- Schema-aware extension system
- Infrastructure validation for Nickel definitions
- Cluster discovery and operations enhanced
--- ---
## 🔄 Backward Compatibility ## 🎯 Current Features
**✅ 100% Backward Compatible** - **Nickel IaC**: Type-safe infrastructure definitions
- **CLI System**: Unified command interface with 80+ shortcuts
All changes are additive or maintain existing interfaces. - **Provider Abstraction**: Cloud-agnostic operations
- **Config System**: Hierarchical configuration with 476+ accessors
- **Workflow Engine**: Batch operations with dependency resolution
- **Validation**: Schema-aware infrastructure validation
- **AI Integration**: Schema-driven configuration generation
--- ---
## 🚀 No Breaking Changes **Status**: Production
**Date**: 2026-01-14
- Existing commands work unchanged
- Library APIs remain compatible
- Plugin system compatible
- Configuration remains compatible
---
**Status**: Core system updates complete
**Date**: 2025-12-11
**Repository**: provisioning/core **Repository**: provisioning/core
**Version**: 1.0.11

109
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,109 @@
# Code of Conduct
## Our Pledge
We, as members, contributors, and leaders, pledge to make participation in our project and community
a harassment-free experience for everyone, regardless of:
- Age
- Body size
- Visible or invisible disability
- Ethnicity
- Sex characteristics
- Gender identity and expression
- Level of experience
- Education
- Socioeconomic status
- Nationality
- Personal appearance
- Race
- Caste
- Color
- Religion
- Sexual identity and orientation
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by mistakes
- Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery
- Trolling, insulting, or derogatory comments
- Personal or political attacks
- Public or private harassment
- Publishing others' private information (doxing)
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Enforcement Responsibilities
Project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior
and will take appropriate corrective action in response to unacceptable behavior.
Maintainers have the right and responsibility to:
- Remove, edit, or reject comments, commits, code, and other contributions
- Ban contributors for behavior they deem inappropriate, threatening, or harmful
## Scope
This Code of Conduct applies to:
- All community spaces (GitHub, forums, chat, events, etc.)
- Official project channels and representations
- Interactions between community members related to the project
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to project maintainers:
- Email: [project contact]
- GitHub: Private security advisory
- Issues: Report with `conduct` label (public discussions only)
All complaints will be reviewed and investigated promptly and fairly.
### Enforcement Guidelines
**1. Correction**
- Community impact: Use of inappropriate language or unwelcoming behavior
- Action: Private written warning with explanation and clarity on impact
- Consequence: Warning and no further violations
**2. Warning**
- Community impact: Violation through single incident or series of actions
- Action: Written warning with severity consequences for continued behavior
- Consequence: Suspension from community interaction
**3. Temporary Ban**
- Community impact: Serious violation of standards
- Action: Temporary ban from community interaction
- Consequence: Revocation of ban after reflection period
**4. Permanent Ban**
- Community impact: Pattern of violating community standards
- Action: Permanent ban from community interaction
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1.
For answers to common questions about this code of conduct, see the FAQ at <https://www.contributor-covenant.org/faq>.
---
**Thank you for being part of our community!**
We believe in creating a welcoming and inclusive space where everyone can contribute their best work. Together, we make this project better.

131
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,131 @@
# Contributing to provisioning
Thank you for your interest in contributing! This document provides guidelines and instructions for contributing to this project.
## Code of Conduct
This project adheres to a Code of Conduct. By participating, you are expected to uphold this code.
Please see [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for details.
## Getting Started
### Prerequisites
- Rust 1.70+ (if project uses Rust)
- NuShell (if project uses Nushell scripts)
- Git
### Development Setup
1. Fork the repository
2. Clone your fork: `git clone https://repo.jesusperez.pro/jesus/provisioning`
3. Add upstream: `git remote add upstream https://repo.jesusperez.pro/jesus/provisioning`
4. Create a branch: `git checkout -b feature/your-feature`
## Development Workflow
### Before You Code
- Check existing issues and pull requests to avoid duplication
- Create an issue to discuss major changes before implementing
- Assign yourself to let others know you're working on it
### Code Standards
#### Rust
- Run `cargo fmt --all` before committing
- All code must pass `cargo clippy -- -D warnings`
- Write tests for new functionality
- Maintain 100% documentation coverage for public APIs
#### Nushell
- Validate scripts with `nu --ide-check 100 script.nu`
- Follow consistent naming conventions
- Use type hints where applicable
#### Nickel
- Type check schemas with `nickel typecheck`
- Document schema fields with comments
- Test schema validation
### Commit Guidelines
- Write clear, descriptive commit messages
- Reference issues with `Fixes #123` or `Related to #123`
- Keep commits focused on a single concern
- Use imperative mood: "Add feature" not "Added feature"
### Testing
All changes must include tests:
```bash
# Run all tests
cargo test --workspace
# Run with coverage
cargo llvm-cov --all-features --lcov
# Run locally before pushing
just ci-full
```
### Pull Request Process
1. Update documentation for any changed functionality
2. Add tests for new code
3. Ensure all CI checks pass
4. Request review from maintainers
5. Be responsive to feedback and iterate quickly
## Review Process
- Maintainers will review your PR within 3-5 business days
- Feedback is constructive and meant to improve the code
- All discussions should be respectful and professional
- Once approved, maintainers will merge the PR
## Reporting Bugs
Found a bug? Please file an issue with:
- **Title**: Clear, descriptive title
- **Description**: What happened and what you expected
- **Steps to reproduce**: Minimal reproducible example
- **Environment**: OS, Rust version, etc.
- **Screenshots**: If applicable
## Suggesting Enhancements
Have an idea? Please file an issue with:
- **Title**: Clear feature title
- **Description**: What, why, and how
- **Use cases**: Real-world scenarios where this would help
- **Alternative approaches**: If you've considered any
## Documentation
- Keep README.md up to date
- Document public APIs with rustdoc comments
- Add examples for non-obvious functionality
- Update CHANGELOG.md with your changes
## Release Process
Maintainers handle releases following semantic versioning:
- MAJOR: Breaking changes
- MINOR: New features (backward compatible)
- PATCH: Bug fixes
## Questions
- Check existing documentation and issues
- Ask in discussions or open an issue
- Join our community channels
Thank you for contributing!

215
README.md
View file

@ -9,7 +9,9 @@
# Core Engine # Core Engine
The **Core Engine** is the foundational component of the [Provisioning project](https://repo.jesusperez.pro/jesus/provisioning), providing the unified CLI interface, core Nushell libraries, and essential utility scripts. Built on **Nushell** and **KCL**, it serves as the primary entry point for all infrastructure operations. The **Core Engine** is the foundational component of the [Provisioning project](https://repo.jesusperez.pro/jesus/provisioning),
providing the unified CLI interface, core Nushell libraries, and essential utility scripts.
Built on **Nushell** and **Nickel**, it serves as the primary entry point for all infrastructure operations.
## Overview ## Overview
@ -23,58 +25,71 @@ The Core Engine provides:
## Project Structure ## Project Structure
``` ```text
provisioning/core/ provisioning/core/
├── cli/ # Command-line interface ├── cli/ # Command-line interface
│ └── provisioning # Main CLI entry point (211 lines, 84% reduction) │ ├── provisioning # Main bash wrapper (command-registry cache aware)
│ ├── tty-dispatch.sh # TTY-safe dispatcher (replaces shlib)
│ ├── tty-filter.sh # TTY command filter
│ └── tty-commands.conf # TTY command manifest
├── nulib/ # Core Nushell libraries ├── nulib/ # Core Nushell libraries
│ ├── lib_provisioning/ # Core provisioning libraries │ ├── commands-registry.ncl # Command catalog (Nickel → JSON cache)
│ │ ├── config/ # Configuration loading and management │ ├── lib_provisioning/ # Core provisioning libraries
│ │ ├── utils/ # Utility functions (SSH, validation, logging) │ │ ├── config/ # Hierarchical loader, cache, DAG loader
│ │ ├── providers/ # Provider abstraction layer │ │ ├── platform/ # Service manager, startup, bootstrap, health
│ │ ├── secrets/ # Secrets management (SOPS integration) │ │ ├── utils/ # SSH, logging, nickel_processor, path-utils
│ │ ├── workspace/ # Workspace management │ │ ├── plugins/ # auth, kms, orchestrator, secretumvault
│ │ └── infra_validator/ # Infrastructure validation engine │ │ ├── providers/ # Provider registry and loader
│ ├── main_provisioning/ # CLI command handlers │ │ ├── workspace/ # Workspace config, verification, enforcement
│ │ ├── flags.nu # Centralized flag handling │ │ └── infra_validator/ # Schema-aware validation engine
│ │ ├── dispatcher.nu # Command routing (80+ shortcuts) │ ├── main_provisioning/ # CLI command handlers
│ │ ├── help_system.nu # Categorized help system │ │ ├── dispatcher.nu # Command routing (80+ shortcuts)
│ │ └── commands/ # Domain-focused command modules │ │ ├── dag.nu # `dag show/validate/export`
│ │ ├── components.nu # Components + capabilities queries
│ │ ├── workflow.nu # Workflow engine (topo sort, NATS events)
│ │ ├── bootstrap.nu # Platform bootstrap
│ │ ├── cluster-deploy.nu # Component/taskserv dispatch
│ │ ├── ontoref-queries.nu # on+re-aware CLI queries
│ │ └── commands/ # Domain-focused command modules
│ ├── components/ # Component dispatch module (NEW)
│ ├── images/ # Golden image lifecycle (create/list/update/watch)
│ ├── servers/ # Server management modules │ ├── servers/ # Server management modules
│ ├── taskservs/ # Task service modules │ ├── taskservs/ # Task service modules (+ dag-executor)
│ ├── clusters/ # Cluster management modules │ ├── clusters/ # Cluster management modules
│ └── workflows/ # Workflow orchestration modules │ ├── workflows/ # Workflow orchestration modules
├── scripts/ # Utility scripts │ ├── workspace/ # Workspace state + sync
│ └── test/ # Test automation │ └── scripts/ # In-repo nushell scripts (query-*, validate-*)
└── resources/ # Images and logos ├── scripts/ # Utility scripts (refactor, deploy, manage-ports)
└── services/ # Service definitions
``` ```
## Installation ## Installation
### Prerequisites ### Prerequisites
- **Nushell 0.107.1+** - Primary shell and scripting environment - **Nushell 0.112.2** - Primary shell and scripting environment
- **KCL 0.11.2+** - Configuration language for infrastructure definitions - **Nickel 1.15.1+** - Configuration language for infrastructure definitions
- **SOPS 3.10.2+** - Secrets management (optional but recommended) - **SOPS 3.10.2+** - Secrets management (optional but recommended)
- **Age 1.2.1+** - Encryption tool for secrets (optional) - **Age 1.2.1+** - Encryption tool for secrets (optional)
### Adding to PATH ### Adding to PATH
To use the CLI globally, add it to your PATH: Recommended installation uses a symlink plus the `prvng` shell alias:
```bash ```bash
# Create symbolic link # Symlink the bash wrapper into ~/.local/bin
ln -sf "$(pwd)/provisioning/core/cli/provisioning" /usr/local/bin/provisioning ln -sf "$(pwd)/provisioning/core/cli/provisioning" "$HOME/.local/bin/provisioning"
# Or add to PATH in your shell config (~/.bashrc, ~/.zshrc, etc.) # Optional shell alias (add to ~/.bashrc / ~/.zshrc)
export PATH="$PATH:/path/to/project-provisioning/provisioning/core/cli" alias prvng='provisioning'
``` ```
Verify installation: Verify installation:
```bash ```text
provisioning version provisioning version
provisioning help provisioning help
prvng s list # alias + single-char shortcut
``` ```
## Quick Start ## Quick Start
@ -118,17 +133,45 @@ provisioning cluster create my-cluster
provisioning server ssh hostname-01 provisioning server ssh hostname-01
``` ```
### DAG, Components & Workflows
```bash
# Inspect workspace DAG composition (nodes, edges, health gates)
provisioning dag show --infra wuji
provisioning dag validate --infra wuji
provisioning dag export --infra wuji --format dot
# Components and extension capabilities
provisioning component list
provisioning component info postgresql
provisioning extensions capabilities
provisioning extensions graph
# Workflows (topological scheduling + NATS events)
provisioning workflow list
provisioning workflow run deploy-services --infra libre-daoshi
provisioning workflow status <id>
```
### Command Registry & Fast Path
Every `prvng`/`provisioning` invocation validates the command against a JSON cache
rebuilt from `nulib/commands-registry.ncl` whenever the source is newer. Single-char
aliases (`s`, `t`, `c`, `e`, `w`, `j`, `b`, `o`, `a`) are expanded in the bash wrapper
before dispatch. Adding a new top-level command requires a registry entry **plus** a
dispatch case in `cli/provisioning` — see `nulib/main_provisioning/ADDING_COMMANDS.md`.
### Quick Reference ### Quick Reference
For fastest command reference: For fastest command reference:
```bash ```text
provisioning sc provisioning sc
``` ```
For complete guides: For complete guides:
```bash ```text
provisioning guide from-scratch # Complete deployment guide provisioning guide from-scratch # Complete deployment guide
provisioning guide quickstart # Command shortcuts reference provisioning guide quickstart # Command shortcuts reference
provisioning guide customize # Customization patterns provisioning guide customize # Customization patterns
@ -185,7 +228,7 @@ Batch operations with dependency resolution:
```bash ```bash
# Submit batch workflow # Submit batch workflow
provisioning batch submit workflows/example.k provisioning batch submit workflows/example.ncl
# Monitor workflow progress # Monitor workflow progress
provisioning batch monitor <workflow-id> provisioning batch monitor <workflow-id>
@ -197,6 +240,38 @@ provisioning workflow list
provisioning workflow status <id> provisioning workflow status <id>
``` ```
## Internationalization (i18n)
### Fluent-based Localization
The help system supports multiple languages using the Fluent catalog format:
```bash
# Automatic locale detection from LANG environment variable
export LANG=es_ES.UTF-8
provisioning help # Shows Spanish help if es-ES catalog exists
# Falls back to en-US if translation not available
export LANG=fr_FR.UTF-8
provisioning help # Shows French help if fr-FR exists, otherwise English
```
**Catalog Structure**:
```text
provisioning/locales/
├── en-US/
│ └── help.ftl # English help strings
├── es-ES/
│ └── help.ftl # Spanish help strings
└── de-DE/
└── help.ftl # German help strings
```
**Supported Locales**: en-US (default), with framework ready for es-ES, fr-FR, de-DE, etc.
---
## CLI Architecture ## CLI Architecture
### Modular Design ### Modular Design
@ -215,7 +290,7 @@ The CLI uses a domain-driven architecture:
80+ shortcuts for improved productivity: 80+ shortcuts for improved productivity:
| Full Command | Shortcuts | Description | | Full Command | Shortcuts | Description |
|--------------|-----------|-------------| | ------------ | --------- | ----------- |
| `server` | `s` | Server operations | | `server` | `s` | Server operations |
| `taskserv` | `t`, `task` | Task service operations | | `taskserv` | `t`, `task` | Task service operations |
| `cluster` | `cl` | Cluster operations | | `cluster` | `cl` | Cluster operations |
@ -232,7 +307,7 @@ See complete reference: `provisioning sc` or `provisioning guide quickstart`
Help works in both directions: Help works in both directions:
```bash ```text
provisioning help workspace # ✅ provisioning help workspace # ✅
provisioning workspace help # ✅ Same result provisioning workspace help # ✅ Same result
provisioning ws help # ✅ Shortcut also works provisioning ws help # ✅ Shortcut also works
@ -329,8 +404,8 @@ The project follows a three-phase migration:
### Required ### Required
- **Nushell 0.107.1+** - Shell and scripting language - **Nushell 0.112.2** - Shell and scripting language
- **KCL 0.11.2+** - Configuration language - **Nickel 1.15.1+** - Configuration language
### Recommended ### Recommended
@ -341,7 +416,7 @@ The project follows a three-phase migration:
### Optional ### Optional
- **nu_plugin_tera** - Template rendering - **nu_plugin_tera** - Template rendering
- **nu_plugin_kcl** - KCL integration (CLI `kcl` is required, plugin optional) - **Nickel Language** - Native Nickel support via CLI (no plugin required)
## Documentation ## Documentation
@ -354,14 +429,14 @@ The project follows a three-phase migration:
### Architecture Documentation ### Architecture Documentation
- **CLI Architecture**: `docs/architecture/ADR-006-provisioning-cli-refactoring.md` - **CLI Architecture**: `../docs/src/architecture/adr/ADR-006-provisioning-cli-refactoring.md`
- **Configuration System**: See `.claude/features/configuration-system.md` - **Configuration System**: `../docs/src/infrastructure/configuration-system.md`
- **Batch Workflows**: See `.claude/features/batch-workflow-system.md` - **Batch Workflows**: `../docs/src/infrastructure/batch-workflow-system.md`
- **Orchestrator**: See `.claude/features/orchestrator-architecture.md` - **Orchestrator**: `../docs/src/operations/orchestrator-system.md`
### API Documentation ### API Documentation
- **REST API**: See `docs/api/` (when orchestrator is running) - **REST API**: See `../docs/src/api-reference/` (when orchestrator is running)
- **Nushell Modules**: See inline documentation in `nulib/` modules - **Nushell Modules**: See inline documentation in `nulib/` modules
## Testing ## Testing
@ -402,19 +477,23 @@ When contributing to the Core Engine:
### Common Issues ### Common Issues
**Missing environment variables:** **Missing environment variables:**
```bash
```text
provisioning env # Check current configuration provisioning env # Check current configuration
provisioning validate config # Validate configuration files provisioning validate config # Validate configuration files
``` ```
**KCL compilation errors:** **Nickel schema errors:**
```bash
kcl fmt <file>.k # Format KCL file ```text
kcl run <file>.k # Test KCL file nickel fmt <file>.ncl # Format Nickel file
nickel eval <file>.ncl # Evaluate Nickel schema
nickel typecheck <file>.ncl # Type check schema
``` ```
**Provider authentication:** **Provider authentication:**
```bash
```text
provisioning providers # List available providers provisioning providers # List available providers
provisioning show settings # View provider configuration provisioning show settings # View provider configuration
``` ```
@ -423,13 +502,13 @@ provisioning show settings # View provider configuration
Enable verbose logging: Enable verbose logging:
```bash ```text
provisioning --debug <command> provisioning --debug <command>
``` ```
### Getting Help ### Getting Help
```bash ```text
provisioning help # Show main help provisioning help # Show main help
provisioning help <category> # Category-specific help provisioning help <category> # Category-specific help
provisioning <command> help # Command-specific help provisioning <command> help # Command-specific help
@ -440,7 +519,7 @@ provisioning guide list # List all guides
Check system versions: Check system versions:
```bash ```text
provisioning version # Show all versions provisioning version # Show all versions
provisioning nuinfo # Nushell information provisioning nuinfo # Nushell information
``` ```
@ -451,5 +530,37 @@ See project root LICENSE file.
--- ---
**Maintained By**: Architecture Team ## Recent Updates
**Last Updated**: 2025-10-07
### 2026-04-17 - DAG architecture, commands-registry, Nushell 0.110/0.112 refactor
- **Unified Component Architecture**: `components/`, `workflow.nu`, and `components.nu`
implement the libre-daoshi unified model (ComponentDef, WorkflowDef, capabilities).
See `memory/unified_component_arch.md` and ADRs 020/021.
- **Three-layer DAG**: `dag.nu` + `lib_provisioning/config/loader/dag.nu` add
`dag show/validate/export` backed by `schemas/lib/dag/*.ncl`; orchestrator emits
NATS events via `WorkspaceComposition::into_workflow`.
- **Commands-registry cache**: `nulib/commands-registry.ncl` feeds the bash wrapper's
`_validate_command`; fast-path single-char alias expansion avoids cold Nu startup.
- **Platform service manager**: new `platform/service-manager.nu`, `platform/startup.nu`,
and `bootstrap.nu` consolidate autostart, health checks, and lifecycle.
- **Nushell 0.112.2 compliance**: `scripts/refactor-try-catch*.nu` drove the
migration — no `try/catch`, no bash redirections, all external commands prefixed.
- **TTY stack**: `shlib/*-tty.sh` removed; replaced by `cli/tty-dispatch.sh`,
`cli/tty-filter.sh`, and `cli/tty-commands.conf`.
- **New domain modules**: `images/` (golden image lifecycle), `workspace/state.nu` +
`workspace/sync.nu`, `main_provisioning/ontoref-queries.nu`, `main_provisioning/fip.nu`,
`main_provisioning/state.nu`, `main_provisioning/extensions.nu`.
- **Config loader overhaul**: `loader/core.nu` slimmed (759 lines of legacy paths),
`cache/core.nu` refactored, `loaders/file_loader.nu` removed.
### 2026-01-14 - Terminology Migration & i18n
- **Cluster → Taskserv**: Complete refactoring across nulib/ modules
- **Fluent i18n System**: Automatic locale detection with en-US fallback
- Enhanced ANSI output formatting for improved CLI readability
---
**Maintained By**: Core Team
**Last Updated**: 2026-04-17

467
cli/README.md Normal file
View file

@ -0,0 +1,467 @@
# Provisioning CLI - Flow-Aware TTY Command Management
## Architecture Overview
The provisioning wrapper (`provisioning/core/cli/provisioning`) is a **flow controller** that manages three execution paths for command handling:
1. **Standalone TTY** - Interactive commands that exit after execution
2. **Pipeline TTY** - Interactive commands that output for piping to other commands
3. **Regular** - Standard Nushell command processing
This design enables:
- Interactive commands (TTY input) without blocking Nushell
- Inter-command piping of TTY output to subsequent commands
- Same-command flow (TTY input → Nushell processing in one execution)
- Daemon optimization for non-interactive commands
## How Flow Management Works
### Execution Flow
```text
User Command: provisioning <cmd> <args>
Bash wrapper (provisioning)
┌──────────────────────────────────────┐
│ Phase 1: TTY Command Detection │
│ - Read tty-commands.conf registry │
│ - Match command pattern │
└──────────────────────────────────────┘
├─→ Not a TTY command → Continue to Nushell (normal processing)
└─→ TTY command found → Check flow type
├─→ flow=exit → Execute wrapper, exit immediately
├─→ flow=pipe → Execute wrapper, output to stdout, exit (allows piping)
└─→ flow=continue → Execute wrapper, capture output, continue to Nushell
($env.TTY_OUTPUT available in Nushell)
```
### Flow Types Explained
#### 1. Standalone TTY Commands (flow=exit)
**Use case**: Interactive forms, setup wizards, authentication dialogs
**Example**: `provisioning setup wizard`
**Flow**:
```bash
Bash wrapper → TTY filter detects "setup wizard" → flow=exit
Execute wrapper: core/shlib/setup-wizard-tty.sh
User interaction (TypeDialog form)
Exit wrapper → Exit bash wrapper
Never reaches Nushell
```
**Registry entry**:
```bash
"setup wizard" "core/shlib/setup-wizard-tty.sh" "exit"
```
#### 2. Pipeline TTY Commands (flow=pipe)
**Use case**: Getting user input to pipe to another command
**Example**: `provisioning auth get-key | provisioning deploy --api-key-stdin`
**Flow**:
```bash
Bash wrapper → TTY filter detects "auth get-key" → flow=pipe
Execute wrapper: core/shlib/auth-get-key-tty.sh
User provides API key via TTY prompt
Wrapper outputs API key to stdout
Exit wrapper (process exits, pipe has captured output)
Next command receives API key from stdin
```
**Registry entry**:
```bash
"auth get-key" "core/shlib/auth-get-key-tty.sh" "pipe"
```
**Wrapper requirements** (flow=pipe):
- Must output result to stdout
- Output must be newline-terminated
- Exit with proper code (0=success, non-zero=error)
#### 3. Continue-to-Nushell TTY Commands (flow=continue)
**Use case**: TTY input that needs further processing in Nushell
**Example**: `provisioning auth integrate --provider azure`
**Flow**:
```bash
Bash wrapper → TTY filter detects "auth integrate" → flow=continue
Execute wrapper: core/shlib/auth-integrate-tty.sh
User provides credentials via TTY prompt
Wrapper outputs credentials (usually JSON) to stdout
Filter CAPTURES output to $TTY_OUTPUT environment variable
Set $env.PROVISIONING_BYPASS_DAEMON=true (skip daemon)
Return 0 WITHOUT EXITING (continue to Nushell)
Nushell dispatcher receives both:
- CLI args: --provider azure
- TTY output: $env.TTY_OUTPUT (credentials JSON)
Nushell script processes both, completes integration
```
**Registry entry**:
```bash
"auth integrate" "core/shlib/auth-integrate-tty.sh" "continue"
```
**Wrapper requirements** (flow=continue):
- Must output result to stdout (usually JSON for structured data)
- Exit with proper code (0=success, non-zero=error)
**Nushell script requirements** (receives flow=continue output):
```nushell
export def "provisioning auth integrate" [--provider: string] {
# Check if TTY output exists (guard pattern)
let tty_output = ($env.TTY_OUTPUT? | default "")
if ($tty_output | is-empty) {
error make {msg: "No credentials provided via TTY"}
}
# Parse TTY output (credentials)
let credentials = ($tty_output | from json)
# Use both TTY input ($credentials) and CLI args ($provider)
# Complete integration logic...
# Clear sensitive data after use
hide-env TTY_OUTPUT
}
```
#### 4. Regular Commands
**Use case**: Standard provisioning operations
**Example**: `provisioning server list`
**Flow**:
```bash
Bash wrapper → TTY filter checks registry → Not found → Return 1
Continue to normal processing:
- Fast-path checks (help, workspace, env, etc.)
- Daemon check (if applicable)
- Nushell dispatcher
```
## Registry Format
**File**: `provisioning/core/cli/tty-commands.conf`
**Three-field format**: `"PATTERN" "WRAPPER_PATH" "FLOW_TYPE"`
```bash
# Exact command match (e.g., "setup wizard" matches "provisioning setup wizard")
"setup wizard" "core/shlib/setup-wizard-tty.sh" "exit"
# Paths are relative to $PROVISIONING
"auth get-key" "core/shlib/auth-get-key-tty.sh" "pipe"
# Flow types: exit | pipe | continue
"auth integrate" "core/shlib/auth-integrate-tty.sh" "continue"
```
### Flow Type Decision Matrix
| Interaction | Flow Type | Example |
| ----------- | --------- | ------- |
| Interactive form, no output needed | `exit` | Setup wizard, auth login |
| User input → pipe to next command | `pipe` | API key for piping to deploy |
| User input → same-command Nushell processing | `continue` | Credentials for integration |
## Adding New TTY Commands
### Step 1: Create Wrapper Script
Create wrapper in `provisioning/core/shlib/`:
```bash
#!/bin/bash
set -euo pipefail
main() {
local input
# Get input from user
read -rsp "Prompt: " input
echo # Newline
# For flow=pipe: output to stdout
# For flow=continue: output to stdout (will be captured by filter)
echo "$input"
return 0
}
main "$@"
```
Make it executable:
```bash
chmod +x provisioning/core/shlib/your-wrapper-tty.sh
```
### Step 2: Add Registry Entry
Edit `provisioning/core/cli/tty-commands.conf`:
```bash
# Standalone TTY
"your command" "core/shlib/your-wrapper-tty.sh" "exit"
# Pipeline TTY
"get something" "core/shlib/get-something-tty.sh" "pipe"
# Continue-to-Nushell TTY
"setup something" "core/shlib/setup-something-tty.sh" "continue"
```
### Step 3: No Wrapper Modifications Required
The provisioning wrapper automatically:
- Reads registry
- Matches command pattern
- Routes based on flow type
- Handles all three flows
**No need to modify provisioning wrapper for new commands!**
## Wrapper Script Requirements
### For All Wrappers
- **Shebang**: `#!/bin/bash`
- **Safety**: `set -euo pipefail`
- **Arguments**: Accept `"${@}"` from wrapper
- **Exit codes**: 0=success, non-zero=error
- **Validation**: `shellcheck` passes without warnings
### For flow=exit Wrappers
- Complete all interaction in wrapper
- Exit with proper code (0=success, non-zero=error)
- Output shown directly to user (from wrapper)
### For flow=pipe Wrappers
- Get input from user (TTY)
- Output result to stdout
- Output must be newline-terminated
- Exit with proper code (0=success, non-zero=error)
### For flow=continue Wrappers
- Get input from user (TTY)
- Output result to stdout (usually JSON)
- Exit with proper code (0=success, non-zero=error)
- Filter captures output → $TTY_OUTPUT
- Nushell script reads $env.TTY_OUTPUT
## Environment Variables
### Exported by Filter (flow=continue only)
- **`$TTY_OUTPUT`**: Captured output from wrapper (available in Nushell as `$env.TTY_OUTPUT`)
- **`$PROVISIONING_BYPASS_DAEMON`**: Set to "true" to skip daemon (flow=continue automatically sets this)
- **`$TTY_WRAPPER_EXECUTED`**: Set to "true" when TTY wrapper was executed
### Usage in Nushell
```nushell
# Access TTY output in Nushell script
export def "provisioning auth integrate" [--provider: string] {
let tty_output = ($env.TTY_OUTPUT? | default "")
# Parse if JSON
let creds = ($tty_output | from json)
# Use both TTY output and CLI args
integration-logic $provider $creds
# Clear after use (security)
hide-env TTY_OUTPUT
}
```
## Daemon Interaction
The flow filter intelligently manages daemon usage:
### For flow=exit and flow=pipe
- ✅ **Daemon can be used** - No stdin required
- No output needs to be captured and passed to Nushell
- Daemon optimization available (~100ms startup improvement)
### For flow=continue
- ❌ **Daemon MUST be bypassed** - stdin required for TTY interaction
- `PROVISIONING_BYPASS_DAEMON=true` automatically set by filter
- Direct Nushell execution (preserves stdin for TTY)
- Zero overhead (same as non-daemon path)
## Testing TTY Commands
### Test Standalone (flow=exit)
```bash
provisioning setup wizard
# Expected: TypeDialog form, user interaction, exits
```
### Test Pipeline (flow=pipe)
```bash
provisioning auth get-key | wc -c
# Expected: Prompts for API key, outputs to pipe
```
### Test Continue (flow=continue)
```bash
provisioning auth integrate --provider azure
# Expected: Prompts for credentials, passes to Nushell with $env.TTY_OUTPUT
```
### Test Regular Command
```bash
provisioning server list
# Expected: Normal Nushell processing
```
## Troubleshooting
### Command Not Executed
- **Check**: Is command in tty-commands.conf?
- **Check**: Does pattern exactly match command?
- **Check**: Is wrapper path correct and executable?
### Wrapper Not Found
- **Error message**: `Warning: TTY wrapper not found or not executable: /path/to/wrapper`
- **Check**: File exists at `$PROVISIONING/wrapper-path`
- **Check**: File is executable: `chmod +x wrapper-path`
### Output Not Piping (flow=pipe)
- **Check**: Wrapper outputs to stdout (not stderr)
- **Check**: Output is newline-terminated: `echo "output"`
- **Check**: No daemon interference (PROVISIONING_BYPASS_DAEMON not set)
### Nushell Not Receiving Output (flow=continue)
- **Check**: `$env.TTY_OUTPUT` accessible in Nushell: `echo $env.TTY_OUTPUT`
- **Check**: Output format (usually JSON): `echo $env.TTY_OUTPUT | from json`
- **Check**: Wrapper exits with 0: `echo $?`
## Implementation Details
### Filter Location and Function
**File**: `provisioning/core/cli/tty-filter.sh`
**Function**: `filter_tty_command()`
**Lines**: ~104 (includes documentation and three flow paths)
### Integration in Wrapper
**File**: `provisioning/core/cli/provisioning`
**Lines**: ~20 (sources filter, calls function, continues to Nushell)
### Registry Parsing
- **File**: `provisioning/core/cli/tty-commands.conf`
- **Method**: Line-by-line bash read (no jq dependency)
- **Format**: Three-field bash array (bash-compatible)
- **Sections**: Organized by flow type for clarity
## Performance Implications
### startup time
- **flow=exit/pipe**: Daemon available for startup optimization (~100ms improvement)
- **flow=continue**: Daemon bypassed (stdin needed), ~500ms traditional path
- **Regular commands**: Normal daemon/non-daemon path selection
### Memory
- **flow=continue**: Wrapper output stored in `$TTY_OUTPUT` environment variable
- Typical size: < 1KB (credentials, keys, etc.)
- Cleared after Nushell processing (or via `hide-env`)
## Security Considerations
### Sensitive Data in $TTY_OUTPUT
- **Credentials** captured in `$TTY_OUTPUT`
- **Nushell scripts should clear after use**: `hide-env TTY_OUTPUT`
- **Wrapper output may be logged**: Use standard Unix conventions (hide passwords from output)
### Wrapper Location Restriction
- Wrappers should be in `provisioning/core/shlib/` or `provisioning/scripts/`
- Registry reads only wrappers from these trusted locations
- Pattern validation prevents arbitrary script execution
### No Shell Injection
- All variables quoted: `"$variable"`
- No eval or command substitution with user input
- Pattern matching uses exact string match (no regex)
## Related Files
- **Filter**: `provisioning/core/cli/tty-filter.sh`
- **Registry**: `provisioning/core/cli/tty-commands.conf`
- **Wrapper**: `provisioning/core/cli/provisioning`
- **Example wrappers**: `provisioning/core/shlib/auth-get-key-tty.sh`, `provisioning/core/shlib/auth-integrate-tty.sh`
## Key Insights
The provisioning wrapper is not just a pass-through - it's a **flow controller** that:
1. **Detects TTY requirements** (registry matching)
2. **Manages execution paths** (three flows: exit, pipe, continue)
3. **Controls exit behavior** (standalone vs pipeline vs same-command)
4. **Enables inter-command piping** (TTY output to pipes)
5. **Supports Nushell integration** (TTY→Nushell continuation)
6. **Optimizes with daemon** (skip when stdin needed)
This solves:
- "el tema no es sólo un filter" → ✅ Flow controller with three execution paths
- "cómo gestionar el flow por medio del provisioning command" → ✅ Registry + flow types
- "usamos tty para input de una API key, se lo pasamos a un script de nushell" → ✅ Pipeline + continue flows
---
**Version**: 1.0.0
**Last Updated**: January 2026
**Status**: ✅ Production Ready

View file

@ -6,12 +6,12 @@ OS=$(uname | tr '[:upper:]' '[:lower:]')
ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')" ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')"
wget https://github.com/cloudflare/cfssl/releases/download/v${VERSION}/cfssl_${VERSION}_${OS}_${ARCH} wget https://github.com/cloudflare/cfssl/releases/download/v${VERSION}/cfssl_${VERSION}_${OS}_${ARCH}
if [ -r "cfssl_${VERSION}_${OS}_${ARCH}" ] ; then if [ -r "cfssl_${VERSION}_${OS}_${ARCH}" ] ; then
chmod +x "cfssl_${VERSION}_${OS}_${ARCH}" chmod +x "cfssl_${VERSION}_${OS}_${ARCH}"
sudo mv "cfssl_${VERSION}_${OS}_${ARCH}" /usr/local/bin/cfssl sudo mv "cfssl_${VERSION}_${OS}_${ARCH}" /usr/local/bin/cfssl
fi fi
wget https://github.com/cloudflare/cfssl/releases/download/v${VERSION}/cfssljson_${VERSION}_${OS}_${ARCH} wget https://github.com/cloudflare/cfssl/releases/download/v${VERSION}/cfssljson_${VERSION}_${OS}_${ARCH}
if [ -r "cfssljson_${VERSION}_${OS}_${ARCH}" ] ; then if [ -r "cfssljson_${VERSION}_${OS}_${ARCH}" ] ; then
chmod +x "cfssljson_${VERSION}_${OS}_${ARCH}" chmod +x "cfssljson_${VERSION}_${OS}_${ARCH}"
sudo mv "cfssljson_${VERSION}_${OS}_${ARCH}" /usr/local/bin/cfssljson sudo mv "cfssljson_${VERSION}_${OS}_${ARCH}" /usr/local/bin/cfssljson
fi fi

View file

@ -1,9 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Info: Script to instal NUSHELL for Provisioning # Info: Script to instal NUSHELL for Provisioning
# Author: JesusPerezLorenzo # Author: JesusPerezLorenzo
# Release: 1.0.5 # Release: 1.0.5
# Date: 8-03-2024 # Date: 8-03-2024
test_runner() { test_runner() {
echo -e "\nTest installation ... " echo -e "\nTest installation ... "
RUNNER_PATH=$(type -P $RUNNER) RUNNER_PATH=$(type -P $RUNNER)
@ -14,27 +14,27 @@ test_runner() {
echo -e "\n🛑 Error $RUNNER ! Review installation " && exit 1 echo -e "\n🛑 Error $RUNNER ! Review installation " && exit 1
fi fi
} }
register_plugins() { register_plugins() {
local source=$1 local source=$1
local warn=$2 local warn=$2
[ ! -d "$source" ] && echo "🛑 Error path $source is not a directory" && exit 1 [ ! -d "$source" ] && echo "🛑 Error path $source is not a directory" && exit 1
[ -z "$(ls $source/nu_plugin_* 2> /dev/null)" ] && echo "🛑 Error no 'nu_plugin_*' found in $source to register" && exit 1 [ -z "$(ls $source/nu_plugin_* 2> /dev/null)" ] && echo "🛑 Error no 'nu_plugin_*' found in $source to register" && exit 1
echo -e "Nushell $NU_VERSION plugins registration \n" echo -e "Nushell $NU_VERSION plugins registration \n"
if [ -n "$warn" ] ; then if [ -n "$warn" ] ; then
echo -e $"❗Warning: Be sure Nushell plugins are compiled for same Nushell version $NU_VERSION\n otherwise will probably not work and will break installation !\n" echo -e $"❗Warning: Be sure Nushell plugins are compiled for same Nushell version $NU_VERSION\n otherwise will probably not work and will break installation !\n"
fi fi
for plugin in ${source}/nu_plugin_* for plugin in ${source}/nu_plugin_*
do do
if $source/nu -c "register \"${plugin}\" " 2>/dev/null ; then if $source/nu -c "register \"${plugin}\" " 2>/dev/null ; then
echo -en "$(basename $plugin)" echo -en "$(basename $plugin)"
if [[ "$plugin" == *_notifications ]] ; then if [[ "$plugin" == *_notifications ]] ; then
echo -e " registred " echo -e " registred "
else else
echo -e "\t\t registred " echo -e "\t\t registred "
fi fi
fi fi
done done
# Install nu_plugin_tera if available # Install nu_plugin_tera if available
if command -v cargo >/dev/null 2>&1; then if command -v cargo >/dev/null 2>&1; then
echo -e "Installing nu_plugin_tera..." echo -e "Installing nu_plugin_tera..."
@ -47,22 +47,26 @@ register_plugins() {
else else
echo -e "❗ Failed to install nu_plugin_tera" echo -e "❗ Failed to install nu_plugin_tera"
fi fi
# Install nu_plugin_kcl if available
echo -e "Installing nu_plugin_kcl..."
if cargo install nu_plugin_kcl; then
if $source/nu -c "register ~/.cargo/bin/nu_plugin_kcl" 2>/dev/null; then
echo -e "nu_plugin_kcl\t\t registred"
else
echo -e "❗ Failed to register nu_plugin_kcl"
fi
else
echo -e "❗ Failed to install nu_plugin_kcl"
fi
else else
echo -e "❗ Cargo not found - nu_plugin_tera and nu_plugin_kcl not installed" echo -e "❗ Cargo not found - nu_plugin_tera not installed"
fi fi
} }
# Check Nickel configuration language installation
check_nickel_installation() {
if command -v nickel >/dev/null 2>&1; then
nickel_version=$(nickel --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
echo -e "Nickel\t\t\t already installed (version $nickel_version)"
return 0
else
echo -e "⚠️ Nickel not found - Optional but recommended for config rendering"
echo -e " Install via: \$PROVISIONING/core/cli/tools-install nickel"
echo -e " Recommended method: nix profile install nixpkgs#nickel"
echo -e " (Pre-built binaries have Nix library dependencies)"
echo -e " https://nickel-lang.org/getting-started"
return 1
fi
}
install_mode() { install_mode() {
local mode=$1 local mode=$1
@ -72,13 +76,13 @@ install_mode() {
echo "Mode $mode installed" echo "Mode $mode installed"
fi fi
;; ;;
*) *)
NC_PATH=$(type -P nc) NC_PATH=$(type -P nc)
if [ -z "$NC_PATH" ] ; then if [ -z "$NC_PATH" ] ; then
echo "'nc' command not found in PATH. Install 'nc' (netcat) command." echo "'nc' command not found in PATH. Install 'nc' (netcat) command."
exit 1 exit 1
fi fi
if cp $PROVISIONING_MODELS_SRC/no_plugins_defs.nu $PROVISIONING_MODELS_TARGET/plugins_defs.nu ; then if cp $PROVISIONING_MODELS_SRC/no_plugins_defs.nu $PROVISIONING_MODELS_TARGET/plugins_defs.nu ; then
echo "Mode 'no plugins' installed" echo "Mode 'no plugins' installed"
fi fi
esac esac
@ -95,7 +99,7 @@ install_from_url() {
lib_mode=$(grep NU_LIB $PROVISIONING/core/versions | cut -f2 -d"=" | sed 's/"//g') lib_mode=$(grep NU_LIB $PROVISIONING/core/versions | cut -f2 -d"=" | sed 's/"//g')
url_source=$(grep NU_SOURCE $PROVISIONING/core/versions | cut -f2 -d"=" | sed 's/"//g') url_source=$(grep NU_SOURCE $PROVISIONING/core/versions | cut -f2 -d"=" | sed 's/"//g')
download_path="nu-${NU_VERSION}-${ARCH_ORG}-${OS}" download_path="nu-${NU_VERSION}-${ARCH_ORG}-${OS}"
case "$OS" in case "$OS" in
linux) download_path="nu-${NU_VERSION}-${ARCH_ORG}-unknown-${OS}-gnu" linux) download_path="nu-${NU_VERSION}-${ARCH_ORG}-unknown-${OS}-gnu"
;; ;;
esac esac
@ -107,7 +111,7 @@ install_from_url() {
return 1 return 1
fi fi
echo -e "Nushell $NU_VERSION extracting ..." echo -e "Nushell $NU_VERSION extracting ..."
if ! tar xzf $tar_file ; then if ! tar xzf $tar_file ; then
echo "🛑 Error download $download_url " && exit 1 echo "🛑 Error download $download_url " && exit 1
return 1 return 1
fi fi
@ -117,9 +121,9 @@ install_from_url() {
return 1 return 1
fi fi
echo -e "Nushell $NU_VERSION installing ..." echo -e "Nushell $NU_VERSION installing ..."
if [ -r "$download_path/nu" ] ; then if [ -r "$download_path/nu" ] ; then
chmod +x $download_path/nu chmod +x $download_path/nu
if ! sudo cp $download_path/nu $target_path ; then if ! sudo cp $download_path/nu $target_path ; then
echo "🛑 Error installing \"nu\" in $target_path" echo "🛑 Error installing \"nu\" in $target_path"
rm -rf $download_path rm -rf $download_path
return 1 return 1
@ -127,14 +131,14 @@ install_from_url() {
fi fi
rm -rf $download_path rm -rf $download_path
echo "✅ Nushell and installed in $target_path" echo "✅ Nushell and installed in $target_path"
[[ ! "$PATH" =~ $target_path ]] && echo "❗ Warning: \"$target_path\" is not in your PATH for $(basename $SHELL) ! Fix your PATH settings " [[ ! "$PATH" =~ $target_path ]] && echo "❗ Warning: \"$target_path\" is not in your PATH for $(basename $SHELL) ! Fix your PATH settings "
echo "" echo ""
# TDOO install plguins via cargo ?? # TDOO install plguins via cargo ??
# TODO a NU version without PLUGINS # TODO a NU version without PLUGINS
# register_plugins $target_path # register_plugins $target_path
} }
install_from_local() { install_from_local() {
local source=$1 local source=$1
local target=$2 local target=$2
local tmpdir local tmpdir
@ -146,44 +150,47 @@ install_from_local() {
tmpdir=$(mktemp -d) tmpdir=$(mktemp -d)
cp $source/*gz $tmpdir cp $source/*gz $tmpdir
for file in $tmpdir/*gz ; do gunzip $file ; done for file in $tmpdir/*gz ; do gunzip $file ; done
if ! sudo mv $tmpdir/* $target ; then if ! sudo mv $tmpdir/* $target ; then
echo -e "🛑 Errors to install Nushell and plugins in \"${target}\"" echo -e "🛑 Errors to install Nushell and plugins in \"${target}\""
rm -rf $tmpdir rm -rf $tmpdir
return 1 return 1
fi fi
rm -rf $tmpdir rm -rf $tmpdir
echo "✅ Nushell and plugins installed in $target" echo "✅ Nushell and plugins installed in $target"
[[ ! "$PATH" =~ $target ]] && echo "❗ Warning: \"$target\" is not in your PATH for $(basename $SHELL) ! Fix your PATH settings " [[ ! "$PATH" =~ $target ]] && echo "❗ Warning: \"$target\" is not in your PATH for $(basename $SHELL) ! Fix your PATH settings "
echo "" echo ""
register_plugins $target register_plugins $target
} }
message_install() { message_install() {
local ask=$1 local ask=$1
local msg local msg
local answer local answer
[ -r "$PROVISIONING/resources/ascii.txt" ] && cat "$PROVISIONING/resources/ascii.txt" && echo "" [ -r "$PROVISIONING/resources/ascii.txt" ] && cat "$PROVISIONING/resources/ascii.txt" && echo ""
if [ -z "$NU" ] ; then if [ -z "$NU" ] ; then
echo -e "🛑 Nushell $NU_VERSION not installed is mandatory for \"${RUNNER}\"" echo -e "🛑 Nushell $NU_VERSION not installed is mandatory for \"${RUNNER}\""
echo -e "Check PATH or https://www.nushell.sh/book/installation.html with version $NU_VERSION" echo -e "Check PATH or https://www.nushell.sh/book/installation.html with version $NU_VERSION"
else else
echo -e "Nushell $NU_VERSION update for \"${RUNNER}\"" echo -e "Nushell $NU_VERSION update for \"${RUNNER}\""
fi fi
echo "" echo ""
if [ -n "$ask" ] && [ -d "$(dirname $0)/nu/${ARCH}-${OS}" ] ; then if [ -n "$ask" ] && [ -d "$(dirname $0)/nu/${ARCH}-${OS}" ] ; then
echo -en "Install Nushell $(uname -m) $(uname) in \"$INSTALL_PATH\" now (yes/no) ? : " echo -en "Install Nushell $(uname -m) $(uname) in \"$INSTALL_PATH\" now (yes/no) ? : "
read -r answer read -r answer
if [ "$answer" != "yes" ] && [ "$answer" != "y" ] ; then if [ "$answer" != "yes" ] && [ "$answer" != "y" ] ; then
return 1 return 1
fi fi
fi fi
if [ -d "$(dirname $0)/nu/${ARCH}-${OS}" ] ; then if [ -d "$(dirname $0)/nu/${ARCH}-${OS}" ] ; then
install_from_local $(dirname $0)/nu/${ARCH}-${OS} $INSTALL_PATH install_from_local $(dirname $0)/nu/${ARCH}-${OS} $INSTALL_PATH
install_mode "ui" install_mode "ui"
else else
install_from_url $INSTALL_PATH install_from_url $INSTALL_PATH
install_mode "" install_mode ""
fi fi
echo ""
echo -e "Checking optional configuration languages..."
check_nickel_installation
} }
set +o errexit set +o errexit
@ -195,21 +202,21 @@ export NU=$(type -P nu)
[ -n "$PROVISIONING_ENV" ] && [ -r "$PROVISIONING_ENV" ] && source "$PROVISIONING_ENV" [ -n "$PROVISIONING_ENV" ] && [ -r "$PROVISIONING_ENV" ] && source "$PROVISIONING_ENV"
[ -r "../env-provisioning" ] && source ../env-provisioning [ -r "../env-provisioning" ] && source ../env-provisioning
[ -r "env-provisioning" ] && source ./env-provisioning [ -r "env-provisioning" ] && source ./env-provisioning
#[ -r ".env" ] && source .env set #[ -r ".env" ] && source .env set
set +o allexport set +o allexport
if [ -n "$1" ] && [ -d "$1" ] && [ -d "$1/core" ] ; then if [ -n "$1" ] && [ -d "$1" ] && [ -d "$1/core" ] ; then
export PROVISIONING=$1 export PROVISIONING=$1
else else
export PROVISIONING=${PROVISIONING:-/usr/local/provisioning} export PROVISIONING=${PROVISIONING:-/usr/local/provisioning}
fi fi
TASK=${1:-check} TASK=${1:-check}
shift shift
if [ "$TASK" == "mode" ] && [ -n "$1" ] ; then if [ "$TASK" == "mode" ] && [ -n "$1" ] ; then
INSTALL_MODE=$1 INSTALL_MODE=$1
shift shift
else else
INSTALL_MODE="ui" INSTALL_MODE="ui"
fi fi
@ -230,21 +237,21 @@ PROVISIONING_MODELS_SRC=$PROVISIONING/core/nulib/models
PROVISIONING_MODELS_TARGET=$PROVISIONING/core/nulib/lib_provisioning PROVISIONING_MODELS_TARGET=$PROVISIONING/core/nulib/lib_provisioning
USAGE="$(basename $0) [install | reinstall | mode | check] no-ask mode-?? " USAGE="$(basename $0) [install | reinstall | mode | check] no-ask mode-?? "
case $TASK in case $TASK in
install) install)
message_install $ASK_MESSAGE message_install $ASK_MESSAGE
;; ;;
reinstall | update) reinstall | update)
INSTALL_PATH=$(dirname $NU) INSTALL_PATH=$(dirname $NU)
if message_install ; then if message_install ; then
test_runner test_runner
fi fi
;; ;;
mode) mode)
install_mode $INSTALL_MODE install_mode $INSTALL_MODE
;; ;;
check) check)
$PROVISIONING/core/bin/tools-install check nu $PROVISIONING/core/bin/tools-install check nu
;; ;;
help|-h) help|-h)
echo "$USAGE" echo "$USAGE"

View file

@ -10,7 +10,7 @@ use ../nulib/providers/discover.nu *
use ../nulib/providers/load.nu * use ../nulib/providers/load.nu *
use ../nulib/clusters/discover.nu * use ../nulib/clusters/discover.nu *
use ../nulib/clusters/load.nu * use ../nulib/clusters/load.nu *
use ../nulib/lib_provisioning/kcl_module_loader.nu * use ../nulib/lib_provisioning/module_loader.nu *
use ../nulib/lib_provisioning/config/accessor.nu config-get use ../nulib/lib_provisioning/config/accessor.nu config-get
# Main module loader command with enhanced features # Main module loader command with enhanced features
@ -82,11 +82,11 @@ export def "main discover" [
} }
} }
# Sync KCL dependencies for infrastructure workspace # Sync Nickel dependencies for infrastructure workspace
export def "main sync-kcl" [ export def "main sync" [
infra: string, # Infrastructure name or path infra: string, # Infrastructure name or path
--manifest: string = "providers.manifest.yaml", # Manifest file name --manifest: string = "providers.manifest.yaml", # Manifest file name
--kcl # Show KCL module info after sync --show-modules # Show module info after sync
] { ] {
# Resolve infrastructure path # Resolve infrastructure path
let infra_path = if ($infra | path exists) { let infra_path = if ($infra | path exists) {
@ -102,14 +102,14 @@ export def "main sync-kcl" [
} }
} }
# Sync KCL dependencies using library function # Sync Nickel dependencies using library function
sync-kcl-dependencies $infra_path --manifest $manifest sync-nickel-dependencies $infra_path --manifest $manifest
# Show KCL module info if requested # Show Nickel module info if requested
if $kcl { if $show_modules {
print "" print ""
print "📋 KCL Modules:" print "📋 Nickel Modules:"
let modules_dir = (get-config-value "kcl" "modules_dir") let modules_dir = (get-config-value "nickel" "modules_dir")
let modules_path = ($infra_path | path join $modules_dir) let modules_path = ($infra_path | path join $modules_dir)
if ($modules_path | path exists) { if ($modules_path | path exists) {
@ -382,7 +382,7 @@ export def "main override create" [
$"# Override for ($module) in ($infra) $"# Override for ($module) in ($infra)
# Based on template: ($from) # Based on template: ($from)
import ($type).*.($module).kcl.($module) as base import ($type).*.($module).ncl.($module) as base
import provisioning.workspace.templates.($type).($from) as template import provisioning.workspace.templates.($type).($from) as template
# Infrastructure-specific overrides # Infrastructure-specific overrides
@ -396,7 +396,7 @@ import provisioning.workspace.templates.($type).($from) as template
} else { } else {
$"# Override for ($module) in ($infra) $"# Override for ($module) in ($infra)
import ($type).*.($module).kcl.($module) as base import ($type).*.($module).ncl.($module) as base
# Infrastructure-specific overrides # Infrastructure-specific overrides
($module)_($infra)_override: base.($module | str capitalize) = base.($module)_config { ($module)_($infra)_override: base.($module | str capitalize) = base.($module)_config {
@ -627,29 +627,29 @@ def load_extension_to_workspace [
cp -r $source_module_path $parent_dir cp -r $source_module_path $parent_dir
print $" ✓ Schemas copied to workspace .($extension_type)/" print $" ✓ Schemas copied to workspace .($extension_type)/"
# STEP 2a: Update individual module's kcl.mod with correct workspace paths # STEP 2a: Update individual module's nickel.mod with correct workspace paths
# Calculate relative paths based on categorization depth # Calculate relative paths based on categorization depth
let provisioning_path = if ($group_path | is-not-empty) { let provisioning_path = if ($group_path | is-not-empty) {
# Categorized: .{ext}/{category}/{module}/kcl/ -> ../../../../.kcl/packages/provisioning # Categorized: .{ext}/{category}/{module}/nickel/ -> ../../../../.nickel/packages/provisioning
"../../../../.kcl/packages/provisioning" "../../../../.nickel/packages/provisioning"
} else { } else {
# Non-categorized: .{ext}/{module}/kcl/ -> ../../../.kcl/packages/provisioning # Non-categorized: .{ext}/{module}/nickel/ -> ../../../.nickel/packages/provisioning
"../../../.kcl/packages/provisioning" "../../../.nickel/packages/provisioning"
} }
let parent_path = if ($group_path | is-not-empty) { let parent_path = if ($group_path | is-not-empty) {
# Categorized: .{ext}/{category}/{module}/kcl/ -> ../../.. # Categorized: .{ext}/{category}/{module}/nickel/ -> ../../..
"../../.." "../../.."
} else { } else {
# Non-categorized: .{ext}/{module}/kcl/ -> ../.. # Non-categorized: .{ext}/{module}/nickel/ -> ../..
"../.." "../.."
} }
# Update the module's kcl.mod file with workspace-relative paths # Update the module's nickel.mod file with workspace-relative paths
let module_kcl_mod_path = ($target_module_path | path join "kcl" "kcl.mod") let module_nickel_mod_path = ($target_module_path | path join "nickel" "nickel.mod")
if ($module_kcl_mod_path | path exists) { if ($module_nickel_mod_path | path exists) {
print $" 🔧 Updating module kcl.mod with workspace paths" print $" 🔧 Updating module nickel.mod with workspace paths"
let module_kcl_mod_content = $"[package] let module_nickel_mod_content = $"[package]
name = \"($module)\" name = \"($module)\"
edition = \"v0.11.3\" edition = \"v0.11.3\"
version = \"0.0.1\" version = \"0.0.1\"
@ -658,24 +658,24 @@ version = \"0.0.1\"
provisioning = { path = \"($provisioning_path)\", version = \"0.0.1\" } provisioning = { path = \"($provisioning_path)\", version = \"0.0.1\" }
($extension_type) = { path = \"($parent_path)\", version = \"0.1.0\" } ($extension_type) = { path = \"($parent_path)\", version = \"0.1.0\" }
" "
$module_kcl_mod_content | save -f $module_kcl_mod_path $module_nickel_mod_content | save -f $module_nickel_mod_path
print $" ✓ Updated kcl.mod: ($module_kcl_mod_path)" print $" ✓ Updated nickel.mod: ($module_nickel_mod_path)"
} }
} else { } else {
print $" ⚠️ Warning: Source not found at ($source_module_path)" print $" ⚠️ Warning: Source not found at ($source_module_path)"
} }
# STEP 2b: Create kcl.mod in workspace/.{extension_type} # STEP 2b: Create nickel.mod in workspace/.{extension_type}
let extension_kcl_mod = ($target_schemas_dir | path join "kcl.mod") let extension_nickel_mod = ($target_schemas_dir | path join "nickel.mod")
if not ($extension_kcl_mod | path exists) { if not ($extension_nickel_mod | path exists) {
print $" 📦 Creating kcl.mod for .($extension_type) package" print $" 📦 Creating nickel.mod for .($extension_type) package"
let kcl_mod_content = $"[package] let nickel_mod_content = $"[package]
name = \"($extension_type)\" name = \"($extension_type)\"
edition = \"v0.11.3\" edition = \"v0.11.3\"
version = \"0.1.0\" version = \"0.1.0\"
description = \"Workspace-level ($extension_type) schemas\" description = \"Workspace-level ($extension_type) schemas\"
" "
$kcl_mod_content | save $extension_kcl_mod $nickel_mod_content | save $extension_nickel_mod
} }
# Ensure config directory exists # Ensure config directory exists
@ -690,9 +690,9 @@ description = \"Workspace-level ($extension_type) schemas\"
# Build import statement with "as {module}" alias # Build import statement with "as {module}" alias
let import_stmt = if ($group_path | is-not-empty) { let import_stmt = if ($group_path | is-not-empty) {
$"import ($extension_type).($group_path).($module).kcl.($module) as ($module)" $"import ($extension_type).($group_path).($module).ncl.($module) as ($module)"
} else { } else {
$"import ($extension_type).($module).kcl.($module) as ($module)" $"import ($extension_type).($module).ncl.($module) as ($module)"
} }
# Get relative paths for comments # Get relative paths for comments
@ -719,7 +719,7 @@ description = \"Workspace-level ($extension_type) schemas\"
($import_stmt) ($import_stmt)
# TODO: Configure your ($module) instance # TODO: Configure your ($module) instance
# See available schemas at: ($relative_schema_path)/kcl/ # See available schemas at: ($relative_schema_path)/nickel/
" "
} }
@ -727,15 +727,15 @@ description = \"Workspace-level ($extension_type) schemas\"
print $" ✓ Config created: ($config_file_path)" print $" ✓ Config created: ($config_file_path)"
print $" 📝 Edit ($extension_type)/($module).k to configure settings" print $" 📝 Edit ($extension_type)/($module).k to configure settings"
# STEP 3: Update infra kcl.mod # STEP 3: Update infra nickel.mod
if ($workspace_abs | str contains "/infra/") { if ($workspace_abs | str contains "/infra/") {
let kcl_mod_path = ($workspace_abs | path join "kcl.mod") let nickel_mod_path = ($workspace_abs | path join "nickel.mod")
if ($kcl_mod_path | path exists) { if ($nickel_mod_path | path exists) {
let kcl_mod_content = (open $kcl_mod_path) let nickel_mod_content = (open $nickel_mod_path)
if not ($kcl_mod_content | str contains $"($extension_type) =") { if not ($nickel_mod_content | str contains $"($extension_type) =") {
print $" 🔧 Updating kcl.mod to include ($extension_type) dependency" print $" 🔧 Updating nickel.mod to include ($extension_type) dependency"
let new_dependency = $"\n# Workspace-level ($extension_type) \(shared across infras\)\n($extension_type) = { path = \"../../.($extension_type)\" }\n" let new_dependency = $"\n# Workspace-level ($extension_type) \(shared across infras\)\n($extension_type) = { path = \"../../.($extension_type)\" }\n"
$"($kcl_mod_content)($new_dependency)" | save -f $kcl_mod_path $"($nickel_mod_content)($new_dependency)" | save -f $nickel_mod_path
} }
} }
} }
@ -808,7 +808,7 @@ def print_enhanced_help [] {
print "" print ""
print "CORE COMMANDS:" print "CORE COMMANDS:"
print " discover <type> [query] [--format <fmt>] [--category <cat>] - Discover available modules" print " discover <type> [query] [--format <fmt>] [--category <cat>] - Discover available modules"
print " sync-kcl <infra> [--manifest <file>] [--kcl] - Sync KCL dependencies for infrastructure" print " sync <infra> [--manifest <file>] [--show-modules] - Sync Nickel dependencies for infrastructure"
print " load <type> <workspace> <modules...> [--layer <layer>] - Load modules into workspace" print " load <type> <workspace> <modules...> [--layer <layer>] - Load modules into workspace"
print " list <type> <workspace> [--layer <layer>] - List loaded modules" print " list <type> <workspace> [--layer <layer>] - List loaded modules"
print " unload <type> <workspace> <module> [--layer <layer>] - Unload module from workspace" print " unload <type> <workspace> <module> [--layer <layer>] - Unload module from workspace"
@ -978,4 +978,4 @@ def print_override_help [] {
print "Examples:" print "Examples:"
print " module-loader override create taskservs wuji kubernetes" print " module-loader override create taskservs wuji kubernetes"
print " module-loader override create taskservs wuji redis --from databases/redis" print " module-loader override create taskservs wuji redis --from databases/redis"
} }

752
cli/new_provisioning Executable file
View file

@ -0,0 +1,752 @@
#!/usr/bin/env bash
# Info: Script to run Provisioning
# Author: Jesus Perez Lorenzo
# Release: 3.0.11
# Date: 2026-01-14
set +o errexit
set +o pipefail
# Debug: log startup
[ "${PROVISIONING_DEBUG_STARTUP:-false}" = "true" ] && echo "[DEBUG] Wrapper started with args: $@" >&2
export NU=$(type -P nu)
_release() {
grep "^# Release:" "$0" | sed "s/# Release: //g"
}
export PROVISIONING_VERS=$(_release)
set -o allexport
## shellcheck disable=SC1090
[ -n "$PROVISIONING_ENV" ] && [ -r "$PROVISIONING_ENV" ] && source "$PROVISIONING_ENV"
[ -r "../env-provisioning" ] && source ../env-provisioning
[ -r "env-provisioning" ] && source ./env-provisioning
#[ -r ".env" ] && source .env set
# Disable provisioning logo/banner output
export PROVISIONING_NO_TITLES=true
set +o allexport
export PROVISIONING=${PROVISIONING:-/usr/local/provisioning}
PROVIISONING_WKPATH=${PROVIISONING_WKPATH:-/tmp/tmp.}
RUNNER="provisioning"
PROVISIONING_MODULE=""
PROVISIONING_MODULE_TASK=""
# Safe argument handling - use default empty value if unbound
[ "${1:-}" == "" ] && shift
[ -z "$NU" ] || [ "${1:-}" == "install" ] || [ "${1:-}" == "reinstall" ] || [ "${1:-}" == "mode" ] && exec bash $PROVISIONING/core/bin/install_nu.sh $PROVISIONING ${1:-} ${2:-}
[ "${1:-}" == "rmwk" ] && rm -rf "$PROVIISONING_WKPATH"* && echo "$PROVIISONING_WKPATH deleted" && exit
[ "${1:-}" == "-x" ] && debug=-x && export PROVISIONING_DEBUG=true && shift
[ "${1:-}" == "-xm" ] && export PROVISIONING_METADATA=true && shift
[ "${1:-}" == "nu" ] && export PROVISIONING_DEBUG=true
[ "${1:-}" == "--x" ] && set -x && debug=-x && export PROVISIONING_DEBUG=true && shift
[ "${1:-}" == "-i" ] || [ "${2:-}" == "-i" ] && echo "$(basename "$0") $(grep "^# Info:" "$0" | sed "s/# Info: //g") " && exit
[ "${1:-}" == "-v" ] || [ "${1:-}" == "--version" ] || [ "${2:-}" == "-v" ] && _release && exit
# ════════════════════════════════════════════════════════════════════════════════
# FLOW-AWARE TTY COMMAND FILTER
# Manages three execution flows: exit (standalone), pipe (inter-command), continue (Nushell)
# Registry: provisioning/core/cli/tty-commands.conf
# Filter: provisioning/core/cli/tty-filter.sh
# ════════════════════════════════════════════════════════════════════════════════
if [ -f "$PROVISIONING/core/cli/tty-filter.sh" ]; then
# Source filter function
# shellcheck source=/dev/null
source "$PROVISIONING/core/cli/tty-filter.sh"
# Try to filter TTY command (full command line as single string)
# Return codes:
# - filter_tty_command returns 0: flow=continue case handled, continue to Nushell with $TTY_OUTPUT
# - filter_tty_command exits: flow=exit/pipe case completed (already exited)
# - filter returns 1: not a TTY command, continue to normal processing
if filter_tty_command "$@"; then
# Flow=continue: TTY wrapper executed, output in $TTY_OUTPUT, bypass daemon
# $env.PROVISIONING_BYPASS_DAEMON and $env.TTY_OUTPUT available to Nushell
: # Continue to Nushell dispatcher below
fi
fi
CMD_ARGS=$@
# Note: Flag ordering is handled by Nushell's reorder_args function
# which automatically reorders flags before positional arguments.
# Flags can be placed anywhere on the command line.
case "${1:-}" in
# Note: "setup" is now handled by the main provisioning CLI dispatcher
# No special module handling needed
-mod)
PROVISIONING_MODULE=$(echo "$2" | sed 's/ //g' | cut -f1 -d"|")
PROVISIONING_MODULE_TASK=$(echo "$2" | sed 's/ //g' | cut -f2 -d"|")
[ "$PROVISIONING_MODULE" == "$PROVISIONING_MODULE_TASK" ] && PROVISIONING_MODULE_TASK=""
shift 2
CMD_ARGS=$@
[ "${PROVISIONING_DEBUG_STARTUP:-false}" = "true" ] && echo "[DEBUG] -mod detected: MODULE=$PROVISIONING_MODULE, TASK=$PROVISIONING_MODULE_TASK, CMD_ARGS=$CMD_ARGS" >&2
;;
esac
NU_ARGS=""
DEFAULT_CONTEXT_TEMPLATE="default_context.yaml"
case "$(uname | tr '[:upper:]' '[:lower:]')" in
linux)
PROVISIONING_USER_CONFIG="$HOME/.config/provisioning/nushell"
PROVISIONING_CONTEXT_PATH="$HOME/.config/provisioning/$DEFAULT_CONTEXT_TEMPLATE"
PROVISIONING_USER_PLATFORM="$HOME/.config/provisioning/platform"
;;
darwin)
PROVISIONING_USER_CONFIG="$HOME/Library/Application Support/provisioning/nushell"
PROVISIONING_CONTEXT_PATH="$HOME/Library/Application Support/provisioning/$DEFAULT_CONTEXT_TEMPLATE"
PROVISIONING_USER_PLATFORM="$HOME/Library/Application Support/provisioning/platform"
;;
*)
PROVISIONING_USER_CONFIG="$HOME/.config/provisioning/nushell"
PROVISIONING_CONTEXT_PATH="$HOME/.config/provisioning/$DEFAULT_CONTEXT_TEMPLATE"
PROVISIONING_USER_PLATFORM="$HOME/.config/provisioning/platform"
;;
esac
# ════════════════════════════════════════════════════════════════════════════════
# DAEMON ROUTING - Try daemon for all commands (except setup/help/interactive)
# Falls back to traditional handlers if daemon unavailable
# ════════════════════════════════════════════════════════════════════════════════
DAEMON_ENDPOINT="http://127.0.0.1:9091/execute"
# Function to execute command via daemon
execute_via_daemon() {
local cmd="$1"
shift
# Build JSON array of arguments (simple bash)
local args_json="["
local first=1
for arg in "$@"; do
[ $first -eq 0 ] && args_json="$args_json,"
args_json="$args_json\"$(echo "$arg" | sed 's/"/\\"/g')\""
first=0
done
args_json="$args_json]"
# Determine timeout based on command type
# Heavy commands (create, delete, update) get longer timeout
local timeout=0.5
case "$cmd" in
create | delete | update | setup | init) timeout=5 ;;
*) timeout=0.2 ;;
esac
# Make request and extract stdout
curl -s -m $timeout -X POST "$DAEMON_ENDPOINT" \
-H "Content-Type: application/json" \
-d "{\"command\":\"$cmd\",\"args\":$args_json,\"timeout_ms\":30000}" 2>/dev/null |
sed -n 's/.*"stdout":"\(.*\)","execution.*/\1/p' |
sed 's/\\n/\n/g'
}
# Try daemon ONLY for lightweight commands (list, show, status)
# Skip daemon for heavy commands (create, delete, update) because bash wrapper is slow
# ALSO skip daemon for flow=continue commands (need stdin for TTY interaction)
if [ "${PROVISIONING_BYPASS_DAEMON:-}" != "true" ] && ([ "${1:-}" = "server" ] || [ "${1:-}" = "s" ]); then
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
# Light command - try daemon
[ -n "${PROVISIONING_DEBUG:-}" ] && [ "${PROVISIONING_DEBUG:-}" = "true" ] && echo "⚡ Attempting daemon execution..." >&2
DAEMON_OUTPUT=$(execute_via_daemon "$@" 2>/dev/null)
if [ -n "$DAEMON_OUTPUT" ]; then
echo "$DAEMON_OUTPUT"
exit 0
fi
[ -n "${PROVISIONING_DEBUG:-}" ] && [ "${PROVISIONING_DEBUG:-}" = "true" ] && echo "⚠️ Daemon unavailable, using traditional handlers..." >&2
fi
# NOTE: Command reordering (server create -> create server) has been removed.
# The Nushell dispatcher in provisioning/core/nulib/main_provisioning/dispatcher.nu
# handles command routing correctly and expects "server create" format.
# The reorder_args function in provisioning script handles any flag reordering needed.
fi
# ════════════════════════════════════════════════════════════════════════════════
# FAST-PATH: Commands that don't need full config loading or platform bootstrap
# These commands use lib_minimal.nu for <100ms execution
# (ONLY REACHED if daemon is not available)
# ═══<E29590><E29590><EFBFBD>════════════════════════════════════════════════════════════════════════════
# Help commands fast-path (uses help_minimal.nu)
# Detects "help" in ANY argument position, not just first
help_category=""
help_found=false
# Check if first arg is empty (no args provided) - treat as help request
if [ -z "${1:-}" ]; then
help_found=true
else
# Loop through all arguments to find help variant and extract category
for arg in "$@"; do
case "$arg" in
help|-h|--help|--helpinfo)
help_found=true
;;
-*)
# Skip flags (like -x, -xm, -i, -v, etc.)
;;
*)
# First non-flag, non-help argument becomes the category
if [ "$help_category" = "" ]; then
help_category="$arg"
fi
;;
esac
done
fi
# Execute help fast-path if help was requested
if [ "$help_found" = true ]; then
# Export LANG explicitly to ensure locale detection works in nu subprocess
export LANG
$NU -n -c "source '$PROVISIONING/core/nulib/help_minimal.nu'; provisioning-help '$help_category' | print" 2>/dev/null
exit $?
fi
# Workspace operations (fast-path)
if [ "${1:-}" = "workspace" ] || [ "${1:-}" = "ws" ]; then
case "${2:-}" in
"list" | "")
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-list | get ok | table" 2>/dev/null
exit $?
;;
"active")
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-active" 2>/dev/null
exit $?
;;
"info")
if [ -n "${3:-}" ]; then
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-info '$3'" 2>/dev/null
else
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-active | workspace-info \$in" 2>/dev/null
fi
exit $?
;;
esac
# Other workspace commands (switch, register, etc.) fall through to full loading
fi
# Status/Health check (fast-path) - DISABLED to fix dispatcher loop
# Use normal dispatcher path instead of fast-path with lib_minimal.nu
# if [ "$1" = "status" ] || [ "$1" = "health" ]; then
# $NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; status-quick | table" 2>/dev/null
# exit $?
# fi
# Environment display (fast-path)
if [ "${1:-}" = "env" ] || [ "${1:-}" = "allenv" ]; then
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; env-quick | table" 2>/dev/null
exit $?
fi
# Provider list (lightweight - reads filesystem only, no module loading)
if [ "${1:-}" = "provider" ] || [ "${1:-}" = "providers" ]; then
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
$NU -n -c "
source '$PROVISIONING/core/nulib/lib_minimal.nu'
let provisioning = (\$env.PROVISIONING | default '/usr/local/provisioning')
let providers_base = (\$provisioning | path join 'extensions' | path join 'providers')
if not (\$providers_base | path exists) {
print 'PROVIDERS list: (none found)'
return
}
# Discover all providers from directories
let all_providers = (
ls \$providers_base | where type == 'dir' | each {|prov_dir|
let prov_name = (\$prov_dir.name | path basename)
if \$prov_name != 'prov_lib' {
{name: \$prov_name, type: 'providers', version: '0.0.1'}
} else {
null
}
} | compact
)
if (\$all_providers | length) == 0 {
print 'PROVIDERS list: (none found)'
} else {
print 'PROVIDERS list: '
print ''
\$all_providers | table
}
" 2>/dev/null
exit $?
fi
fi
# Taskserv list (fast-path) - avoid full system load
if [ "${1:-}" = "taskserv" ] || [ "${1:-}" = "task" ]; then
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
$NU -n -c "
# Direct implementation of taskserv discovery (no dependency loading)
# Taskservs are nested: extensions/taskservs/{category}/{name}/kcl/
let provisioning = (\$env.PROVISIONING | default '/usr/local/provisioning')
let taskservs_base = (\$provisioning | path join 'extensions' | path join 'taskservs')
if not (\$taskservs_base | path exists) {
print '📦 Available Taskservs: (none found)'
return null
}
# Discover all taskservs from nested categories
let all_taskservs = (
ls \$taskservs_base | where type == 'dir' | each {|cat_dir|
let category = (\$cat_dir.name | path basename)
let cat_path = (\$taskservs_base | path join \$category)
if (\$cat_path | path exists) {
ls \$cat_path | where type == 'dir' | each {|ts|
let ts_name = (\$ts.name | path basename)
{task: \$ts_name, mode: \$category, info: ''}
}
} else {
[]
}
} | flatten
)
if (\$all_taskservs | length) == 0 {
print '📦 Available Taskservs: (none found)'
} else {
print '📦 Available Taskservs:'
print ''
\$all_taskservs | each {|ts|
print \$\" • (\$ts.task) [(\$ts.mode)]\"
} | ignore
}
" 2>/dev/null
exit $?
fi
fi
# Server list (lightweight - reads filesystem only, no config loading)
if [ "${1:-}" = "server" ] || [ "${1:-}" = "s" ]; then
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
# Extract --infra flag from remaining args
INFRA_FILTER=""
shift
[ "${1:-}" = "list" ] && shift
while [ $# -gt 0 ]; do
case "${1:-}" in
--infra | -i)
INFRA_FILTER="${2:-}"
shift 2
;;
*) shift ;;
esac
done
$NU -n -c "
source '$PROVISIONING/core/nulib/lib_minimal.nu'
# Get active workspace
let active_ws = (workspace-active)
if (\$active_ws | is-empty) {
print 'No active workspace'
return
}
# Get workspace path from config
let user_config_path = if (\$env.HOME | path exists) {
(
\$env.HOME | path join 'Library' | path join 'Application Support' |
path join 'provisioning' | path join 'user_config.yaml'
)
} else {
''
}
if not (\$user_config_path | path exists) {
print 'Config not found'
return
}
let config = (open \$user_config_path)
let workspaces = (\$config | get --optional workspaces | default [])
let ws = (\$workspaces | where { \$in.name == \$active_ws } | first)
if (\$ws | is-empty) {
print 'Workspace not found'
return
}
let ws_path = \$ws.path
let infra_path = (\$ws_path | path join 'infra')
if not (\$infra_path | path exists) {
print 'No infrastructures found'
return
}
# Filter by infrastructure if specified
let infra_filter = \"$INFRA_FILTER\"
# List server definitions from infrastructure (filtered if --infra specified)
let servers = (
ls \$infra_path | where type == 'dir' | each {|infra|
let infra_name = (\$infra.name | path basename)
# Skip if filter is specified and doesn't match
if ((\$infra_filter | is-not-empty) and (\$infra_name != \$infra_filter)) {
[]
} else {
let servers_file = ($infra_path | path join \$infra_name | path join 'defs' | path join 'servers.ncl')
let servers_file_kcl = ($infra_path | path join \$infra_name | path join 'defs' | path join 'servers.k')
if ($servers_file | path exists) {
# Parse the Nickel servers.ncl file to extract server hostnames
let content = (open \$servers_file --raw)
# Extract hostnames from hostname = "..." patterns by splitting on quotes
let hostnames = (
\$content
| split row \"\\n\"
| where {|line| \$line | str contains \"hostname = \\\"\" }
| each {|line|
# Split by quotes to extract hostname value
let parts = (\$line | split row \"\\\"\")
if (\$parts | length) >= 2 {
\$parts | get 1
} else {
\"\"
}
}
| where {|h| (\$h | is-not-empty) }
)
\$hostnames | each {|srv_name|
{
name: \$srv_name
infrastructure: \$infra_name
path: \$servers_file
}
}
} else {
[]
}
}
} | flatten
)
if (\$servers | length) == 0 {
print '📦 Available Servers: (none configured)'
} else {
print '📦 Available Servers:'
print ''
\$servers | each {|srv|
print \$\" • (\$srv.name) [(\$srv.infrastructure)]\"
} | ignore
}
" 2>/dev/null
exit $?
fi
fi
# Cluster list (lightweight - reads filesystem only)
if [ "${1:-}" = "cluster" ] || [ "${1:-}" = "cl" ]; then
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
$NU -n -c "
source '$PROVISIONING/core/nulib/lib_minimal.nu'
# Get active workspace
let active_ws = (workspace-active)
if (\$active_ws | is-empty) {
print 'No active workspace'
return
}
# Get workspace path from config
let user_config_path = (
\$env.HOME | path join 'Library' | path join 'Application Support' |
path join 'provisioning' | path join 'user_config.yaml'
)
if not (\$user_config_path | path exists) {
print 'Config not found'
return
}
let config = (open \$user_config_path)
let workspaces = (\$config | get --optional workspaces | default [])
let ws = (\$workspaces | where { \$in.name == \$active_ws } | first)
if (\$ws | is-empty) {
print 'Workspace not found'
return
}
let ws_path = \$ws.path
# List all clusters from workspace
let clusters = (
if ((\$ws_path | path join '.clusters') | path exists) {
let clusters_path = (\$ws_path | path join '.clusters')
ls \$clusters_path | where type == 'dir' | each {|cl|
let cl_name = (\$cl.name | path basename)
{
name: \$cl_name
path: \$cl.name
}
}
} else {
[]
}
)
if (\$clusters | length) == 0 {
print '🗂️ Available Clusters: (none found)'
} else {
print '🗂️ Available Clusters:'
print ''
\$clusters | each {|cl|
print \$\" • (\$cl.name)\"
} | ignore
}
" 2>/dev/null
exit $?
fi
fi
# Infra list (lightweight - reads filesystem only)
if [ "${1:-}" = "infra" ] || [ "${1:-}" = "inf" ]; then
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
$NU -n -c "
source '$PROVISIONING/core/nulib/lib_minimal.nu'
# Get active workspace
let active_ws = (workspace-active)
if (\$active_ws | is-empty) {
print 'No active workspace'
return
}
# Get workspace path from config
let user_config_path = (
\$env.HOME | path join 'Library' | path join 'Application Support' |
path join 'provisioning' | path join 'user_config.yaml'
)
if not (\$user_config_path | path exists) {
print 'Config not found'
return
}
let config = (open \$user_config_path)
let workspaces = (\$config | get --optional workspaces | default [])
let ws = (\$workspaces | where { \$in.name == \$active_ws } | first)
if (\$ws | is-empty) {
print 'Workspace not found'
return
}
let ws_path = \$ws.path
let infra_path = (\$ws_path | path join 'infra')
if not (\$infra_path | path exists) {
print '📁 Available Infrastructures: (none configured)'
return
}
# List all infrastructures
let infras = (
ls \$infra_path | where type == 'dir' | each {|inf|
let inf_name = (\$inf.name | path basename)
let inf_full_path = (\$infra_path | path join \$inf_name)
let has_config = ((\$inf_full_path | path join 'settings.k') | path exists)
{
name: \$inf_name
configured: \$has_config
modified: \$inf.modified
}
}
)
if (\$infras | length) == 0 {
print '📁 Available Infrastructures: (none found)'
} else {
print '📁 Available Infrastructures:'
print ''
\$infras | each {|inf|
let status = if \$inf.configured { '✓' } else { '○' }
let output = \" [\" + \$status + \"] \" + \$inf.name
print \$output
} | ignore
}
" 2>/dev/null
exit $?
fi
fi
# Config validation (lightweight - validates config structure without full load)
if [ "${1:-}" = "validate" ]; then
if [ "${2:-}" = "config" ] || [ -z "${2:-}" ]; then
$NU -n -c "
source '$PROVISIONING/core/nulib/lib_minimal.nu'
try {
# Get active workspace
let active_ws = (workspace-active)
if (\$active_ws | is-empty) {
print '❌ Error: No active workspace'
return
}
# Get workspace path from config
let user_config_path = (
\$env.HOME | path join 'Library' | path join 'Application Support' |
path join 'provisioning' | path join 'user_config.yaml'
)
if not (\$user_config_path | path exists) {
print '❌ Error: User config not found at' \$user_config_path
return
}
let config = (open \$user_config_path)
let workspaces = (\$config | get --optional workspaces | default [])
let ws = (\$workspaces | where { \$in.name == \$active_ws } | first)
if (\$ws | is-empty) {
print '❌ Error: Workspace' \$active_ws 'not found in config'
return
}
let ws_path = \$ws.path
# Validate workspace structure
let required_dirs = ['infra', 'config', '.clusters']
let infra_path = (\$ws_path | path join 'infra')
let config_path = (\$ws_path | path join 'config')
let missing_dirs = \$required_dirs | where { not ((\$ws_path | path join \$in) | path exists) }
if (\$missing_dirs | length) > 0 {
print '⚠️ Warning: Missing directories:' (\$missing_dirs | str join ', ')
}
# Validate infrastructures have required files
if (\$infra_path | path exists) {
let infras = (ls \$infra_path | where type == 'dir')
let invalid_infras = (
\$infras | each {|inf|
let inf_name = (\$inf.name | path basename)
let inf_full_path = (\$infra_path | path join \$inf_name)
if not ((\$inf_full_path | path join 'settings.k') | path exists) {
\$inf_name
} else {
null
}
} | compact
)
if (\$invalid_infras | length) > 0 {
print '⚠️ Warning: Infrastructures missing settings.k:' (\$invalid_infras | str join ', ')
}
}
# Validate user config structure
let has_active = ((\$config | get --optional active_workspace) != null)
let has_workspaces = ((\$config | get --optional workspaces) != null)
let has_preferences = ((\$config | get --optional preferences) != null)
if not \$has_active {
print '⚠️ Warning: Missing active_workspace in user config'
}
if not \$has_workspaces {
print '⚠️ Warning: Missing workspaces list in user config'
}
if not \$has_preferences {
print '⚠️ Warning: Missing preferences in user config'
}
# Summary
print ''
print '✓ Configuration validation complete for workspace:' \$active_ws
print ' Path:' \$ws_path
print ' Status: Valid (with warnings, if any listed above)'
} catch {|err|
print '❌ Validation error:' \$err
}
" 2>/dev/null
exit $?
fi
fi
if [ ! -d "$PROVISIONING_USER_CONFIG" ] || [ ! -r "$PROVISIONING_CONTEXT_PATH" ]; then
[ ! -x "$PROVISIONING/core/nulib/provisioning setup" ] && echo "$PROVISIONING/core/nulib/provisioning setup not found" && exit 1
cd "$PROVISIONING/core/nulib"
./"provisioning setup"
echo ""
read -p "Use [enter] to continue or [ctrl-c] to cancel"
fi
[ ! -r "$PROVISIONING_USER_CONFIG/config.nu" ] && echo "$PROVISIONING_USER_CONFIG/config.nu not found" && exit 1
[ ! -r "$PROVISIONING_USER_CONFIG/env.nu" ] && echo "$PROVISIONING_USER_CONFIG/env.nu not found" && exit 1
NU_ARGS=(--config "$PROVISIONING_USER_CONFIG/config.nu" --env-config "$PROVISIONING_USER_CONFIG/env.nu")
export PROVISIONING_ARGS="$CMD_ARGS" NU_ARGS="$NU_ARGS"
#export NU_ARGS=${NU_ARGS//Application Support/Application\\ Support}
# Suppress repetitive config export output during initialization
export PROVISIONING_QUIET_EXPORT="true"
# Export NU_LIB_DIRS so Nushell can find modules during parsing
export NU_LIB_DIRS="$PROVISIONING/core/nulib:/opt/provisioning/core/nulib:/usr/local/provisioning/core/nulib"
# ============================================================================
# DAEMON ROUTING - ENABLED (Phase 3.7: CLI Daemon Integration)
# ============================================================================
# Redesigned daemon with pre-loaded Nushell environment (no CLI callback).
# Routes eligible commands to HTTP daemon for <100ms execution.
# Gracefully falls back to full load if daemon unavailable.
#
# ARCHITECTURE:
# 1. Check daemon health (curl with 5ms timeout)
# 2. Route eligible commands to daemon via HTTP POST
# 3. Fall back to full load if daemon unavailable
# 4. Zero breaking changes (graceful degradation)
#
# PERFORMANCE:
# - With daemon: <100ms for ALL commands
# - Without daemon: ~430ms (normal behavior)
# - Daemon fallback: Automatic, user sees no difference
if [ -n "$PROVISIONING_MODULE" ]; then
# When module is set, just run provisioning - it handles module routing internally
export PROVISIONING_MODULE
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/$RUNNER" $CMD_ARGS
else
# Only redirect stdin for non-interactive commands (nu command needs interactive stdin)
if [ "${1:-}" = "nu" ]; then
# For interactive mode, start nu with provisioning environment
export PROVISIONING_CONFIG="$PROVISIONING_USER_CONFIG"
# Start nu interactively - it will use the config and env from NU_ARGS
$NU "${NU_ARGS[@]}"
else
# Don't redirect stdin for infrastructure commands - they may need interactive input
# Only redirect for commands we know are safe
case "${1:-}" in
help | h | --help | --info | -i | -v | --version | env | allenv | status | health | list | ls | l | workspace | ws | provider | providers | validate | plugin | plugins | nuinfo | platform | plat)
# Safe commands - can use /dev/null
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/$RUNNER" $CMD_ARGS </dev/null
;;
*)
# All other commands (create, delete, server, taskserv, etc.) - keep stdin open
# NOTE: PROVISIONING_MODULE is automatically inherited by Nushell from bash environment
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/$RUNNER" $CMD_ARGS
;;
esac
fi
fi

754
cli/old_provisioning Normal file
View file

@ -0,0 +1,754 @@
#!/usr/bin/env bash
# Info: Script to run Provisioning
# Author: Jesus Perez Lorenzo
# Release: 3.0.11
# Date: 2026-01-14
set +o errexit
set +o pipefail
# Debug: log startup
[ "${PROVISIONING_DEBUG_STARTUP:-false}" = "true" ] && echo "[DEBUG] Wrapper started with args: $@" >&2
export NU=$(type -P nu)
_release() {
grep "^# Release:" "$0" | sed "s/# Release: //g"
}
export PROVISIONING_VERS=$(_release)
set -o allexport
## shellcheck disable=SC1090
[ -n "$PROVISIONING_ENV" ] && [ -r "$PROVISIONING_ENV" ] && source "$PROVISIONING_ENV"
[ -r "../env-provisioning" ] && source ../env-provisioning
[ -r "env-provisioning" ] && source ./env-provisioning
#[ -r ".env" ] && source .env set
# Disable provisioning logo/banner output
export PROVISIONING_NO_TITLES=true
set +o allexport
export PROVISIONING=${PROVISIONING:-/usr/local/provisioning}
PROVIISONING_WKPATH=${PROVIISONING_WKPATH:-/tmp/tmp.}
RUNNER="provisioning"
PROVISIONING_MODULE=""
PROVISIONING_MODULE_TASK=""
# Safe argument handling - use default empty value if unbound
[ "${1:-}" == "" ] && shift
[ -z "$NU" ] || [ "${1:-}" == "install" ] || [ "${1:-}" == "reinstall" ] || [ "${1:-}" == "mode" ] && exec bash $PROVISIONING/core/bin/install_nu.sh $PROVISIONING ${1:-} ${2:-}
[ "${1:-}" == "rmwk" ] && rm -rf "$PROVIISONING_WKPATH"* && echo "$PROVIISONING_WKPATH deleted" && exit
[ "${1:-}" == "-x" ] && debug=-x && export PROVISIONING_DEBUG=true && shift
[ "${1:-}" == "-xm" ] && export PROVISIONING_METADATA=true && shift
[ "${1:-}" == "nu" ] && export PROVISIONING_DEBUG=true
[ "${1:-}" == "--x" ] && set -x && debug=-x && export PROVISIONING_DEBUG=true && shift
[ "${1:-}" == "-i" ] || [ "${2:-}" == "-i" ] && echo "$(basename "$0") $(grep "^# Info:" "$0" | sed "s/# Info: //g") " && exit
[ "${1:-}" == "-v" ] || [ "${1:-}" == "--version" ] || [ "${2:-}" == "-v" ] && _release && exit
# ════════════════════════════════════════════════════════════════════════════════
# FLOW-AWARE TTY COMMAND FILTER
# Manages three execution flows: exit (standalone), pipe (inter-command), continue (Nushell)
# Registry: provisioning/core/cli/tty-commands.conf
# Filter: provisioning/core/cli/tty-filter.sh
# ════════════════════════════════════════════════════════════════════════════════
if [ -f "$PROVISIONING/core/cli/tty-filter.sh" ]; then
# Source filter function
# shellcheck source=/dev/null
source "$PROVISIONING/core/cli/tty-filter.sh"
# Try to filter TTY command (full command line as single string)
# Return codes:
# - filter_tty_command returns 0: flow=continue case handled, continue to Nushell with $TTY_OUTPUT
# - filter_tty_command exits: flow=exit/pipe case completed (already exited)
# - filter returns 1: not a TTY command, continue to normal processing
if filter_tty_command "$@"; then
# Flow=continue: TTY wrapper executed, output in $TTY_OUTPUT, bypass daemon
# $env.PROVISIONING_BYPASS_DAEMON and $env.TTY_OUTPUT available to Nushell
: # Continue to Nushell dispatcher below
fi
fi
CMD_ARGS=$@
# Note: Flag ordering is handled by Nushell's reorder_args function
# which automatically reorders flags before positional arguments.
# Flags can be placed anywhere on the command line.
case "${1:-}" in
# Note: "setup" is now handled by the main provisioning CLI dispatcher
# No special module handling needed
-mod)
PROVISIONING_MODULE=$(echo "$2" | sed 's/ //g' | cut -f1 -d"|")
PROVISIONING_MODULE_TASK=$(echo "$2" | sed 's/ //g' | cut -f2 -d"|")
[ "$PROVISIONING_MODULE" == "$PROVISIONING_MODULE_TASK" ] && PROVISIONING_MODULE_TASK=""
shift 2
CMD_ARGS=$@
[ "${PROVISIONING_DEBUG_STARTUP:-false}" = "true" ] && echo "[DEBUG] -mod detected: MODULE=$PROVISIONING_MODULE, TASK=$PROVISIONING_MODULE_TASK, CMD_ARGS=$CMD_ARGS" >&2
;;
esac
NU_ARGS=""
DEFAULT_CONTEXT_TEMPLATE="default_context.yaml"
case "$(uname | tr '[:upper:]' '[:lower:]')" in
linux)
PROVISIONING_USER_CONFIG="$HOME/.config/provisioning/nushell"
PROVISIONING_CONTEXT_PATH="$HOME/.config/provisioning/$DEFAULT_CONTEXT_TEMPLATE"
PROVISIONING_USER_PLATFORM="$HOME/.config/provisioning/platform"
;;
darwin)
PROVISIONING_USER_CONFIG="$HOME/Library/Application Support/provisioning/nushell"
PROVISIONING_CONTEXT_PATH="$HOME/Library/Application Support/provisioning/$DEFAULT_CONTEXT_TEMPLATE"
PROVISIONING_USER_PLATFORM="$HOME/Library/Application Support/provisioning/platform"
;;
*)
PROVISIONING_USER_CONFIG="$HOME/.config/provisioning/nushell"
PROVISIONING_CONTEXT_PATH="$HOME/.config/provisioning/$DEFAULT_CONTEXT_TEMPLATE"
PROVISIONING_USER_PLATFORM="$HOME/.config/provisioning/platform"
;;
esac
# ════════════════════════════════════════════════════════════════════════════════
# DAEMON ROUTING - Try daemon for all commands (except setup/help/interactive)
# Falls back to traditional handlers if daemon unavailable
# ════════════════════════════════════════════════════════════════════════════════
DAEMON_ENDPOINT="http://127.0.0.1:9091/execute"
# Function to execute command via daemon
execute_via_daemon() {
local cmd="$1"
shift
# Build JSON array of arguments (simple bash)
local args_json="["
local first=1
for arg in "$@"; do
[ $first -eq 0 ] && args_json="$args_json,"
args_json="$args_json\"$(echo "$arg" | sed 's/"/\\"/g')\""
first=0
done
args_json="$args_json]"
# Determine timeout based on command type
# Heavy commands (create, delete, update) get longer timeout
local timeout=0.5
case "$cmd" in
create | delete | update | setup | init) timeout=5 ;;
*) timeout=0.2 ;;
esac
# Make request and extract stdout
curl -s -m $timeout -X POST "$DAEMON_ENDPOINT" \
-H "Content-Type: application/json" \
-d "{\"command\":\"$cmd\",\"args\":$args_json,\"timeout_ms\":30000}" 2>/dev/null |
sed -n 's/.*"stdout":"\(.*\)","execution.*/\1/p' |
sed 's/\\n/\n/g'
}
# Try daemon ONLY for lightweight commands (list, show, status)
# Skip daemon for heavy commands (create, delete, update) because bash wrapper is slow
# ALSO skip daemon for flow=continue commands (need stdin for TTY interaction)
if [ "${PROVISIONING_BYPASS_DAEMON:-}" != "true" ] && ([ "${1:-}" = "server" ] || [ "${1:-}" = "s" ]); then
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
# Light command - try daemon
[ -n "${PROVISIONING_DEBUG:-}" ] && [ "${PROVISIONING_DEBUG:-}" = "true" ] && echo "⚡ Attempting daemon execution..." >&2
DAEMON_OUTPUT=$(execute_via_daemon "$@" 2>/dev/null)
if [ -n "$DAEMON_OUTPUT" ]; then
echo "$DAEMON_OUTPUT"
exit 0
fi
[ -n "${PROVISIONING_DEBUG:-}" ] && [ "${PROVISIONING_DEBUG:-}" = "true" ] && echo "⚠️ Daemon unavailable, using traditional handlers..." >&2
fi
# NOTE: Command reordering (server create -> create server) has been removed.
# The Nushell dispatcher in provisioning/core/nulib/main_provisioning/dispatcher.nu
# handles command routing correctly and expects "server create" format.
# The reorder_args function in provisioning script handles any flag reordering needed.
fi
# ════════════════════════════════════════════════════════════════════════════════
# FAST-PATH: Commands that don't need full config loading or platform bootstrap
# These commands use lib_minimal.nu for <100ms execution
# (ONLY REACHED if daemon is not available)
# ═══<E29590><E29590><EFBFBD>════════════════════════════════════════════════════════════════════════════
# Help commands fast-path (uses help_minimal.nu)
# Detects "help" in ANY argument position, not just first
help_category=""
help_found=false
# Check if first arg is empty (no args provided) - treat as help request
if [ -z "${1:-}" ]; then
help_found=true
else
# Loop through all arguments to find help variant and extract category
for arg in "$@"; do
case "$arg" in
help|-h|--help|--helpinfo)
help_found=true
;;
-*)
# Skip flags (like -x, -xm, -i, -v, etc.)
;;
*)
# First non-flag, non-help argument becomes the category
if [ "$help_category" = "" ]; then
help_category="$arg"
fi
;;
esac
done
fi
# Execute help fast-path if help was requested
if [ "$help_found" = true ]; then
# Export LANG explicitly to ensure locale detection works in nu subprocess
export LANG
$NU -n -c "source '$PROVISIONING/core/nulib/help_minimal.nu'; provisioning-help '$help_category' | print" 2>/dev/null
exit $?
fi
# Workspace operations (fast-path)
if [ "${1:-}" = "workspace" ] || [ "${1:-}" = "ws" ]; then
case "${2:-}" in
"list" | "")
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-list | get ok | table" 2>/dev/null
exit $?
;;
"active")
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-active" 2>/dev/null
exit $?
;;
"info")
if [ -n "${3:-}" ]; then
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-info '$3'" 2>/dev/null
else
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-active | workspace-info \$in" 2>/dev/null
fi
exit $?
;;
esac
# Other workspace commands (switch, register, etc.) fall through to full loading
fi
# Status/Health check (fast-path) - DISABLED to fix dispatcher loop
# Use normal dispatcher path instead of fast-path with lib_minimal.nu
# if [ "$1" = "status" ] || [ "$1" = "health" ]; then
# $NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; status-quick | table" 2>/dev/null
# exit $?
# fi
# Environment display (fast-path)
if [ "${1:-}" = "env" ] || [ "${1:-}" = "allenv" ]; then
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; env-quick | table" 2>/dev/null
exit $?
fi
# Provider list (lightweight - reads filesystem only, no module loading)
if [ "${1:-}" = "provider" ] || [ "${1:-}" = "providers" ]; then
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
$NU -n -c "
source '$PROVISIONING/core/nulib/lib_minimal.nu'
let provisioning = (\$env.PROVISIONING | default '/usr/local/provisioning')
let providers_base = (\$provisioning | path join 'extensions' | path join 'providers')
if not (\$providers_base | path exists) {
print 'PROVIDERS list: (none found)'
return
}
# Discover all providers from directories
let all_providers = (
ls \$providers_base | where type == 'dir' | each {|prov_dir|
let prov_name = (\$prov_dir.name | path basename)
if \$prov_name != 'prov_lib' {
{name: \$prov_name, type: 'providers', version: '0.0.1'}
} else {
null
}
} | compact
)
if (\$all_providers | length) == 0 {
print 'PROVIDERS list: (none found)'
} else {
print 'PROVIDERS list: '
print ''
\$all_providers | table
}
" 2>/dev/null
exit $?
fi
fi
# Taskserv list (fast-path) - avoid full system load
if [ "${1:-}" = "taskserv" ] || [ "${1:-}" = "task" ]; then
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
$NU -n -c "
# Direct implementation of taskserv discovery (no dependency loading)
# Taskservs are nested: extensions/taskservs/{category}/{name}/kcl/
let provisioning = (\$env.PROVISIONING | default '/usr/local/provisioning')
let taskservs_base = (\$provisioning | path join 'extensions' | path join 'taskservs')
if not (\$taskservs_base | path exists) {
print '📦 Available Taskservs: (none found)'
return null
}
# Discover all taskservs from nested categories
let all_taskservs = (
ls \$taskservs_base | where type == 'dir' | each {|cat_dir|
let category = (\$cat_dir.name | path basename)
let cat_path = (\$taskservs_base | path join \$category)
if (\$cat_path | path exists) {
ls \$cat_path | where type == 'dir' | each {|ts|
let ts_name = (\$ts.name | path basename)
{task: \$ts_name, mode: \$category, info: ''}
}
} else {
[]
}
} | flatten
)
if (\$all_taskservs | length) == 0 {
print '📦 Available Taskservs: (none found)'
} else {
print '📦 Available Taskservs:'
print ''
\$all_taskservs | each {|ts|
print \$\" • (\$ts.task) [(\$ts.mode)]\"
} | ignore
}
" 2>/dev/null
exit $?
fi
fi
# Server list (lightweight - reads filesystem only, no config loading)
if [ "${1:-}" = "server" ] || [ "${1:-}" = "s" ]; then
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
# Extract --infra flag from remaining args
INFRA_FILTER=""
shift
[ "${1:-}" = "list" ] && shift
while [ $# -gt 0 ]; do
case "${1:-}" in
--infra | -i)
INFRA_FILTER="${2:-}"
shift 2
;;
*) shift ;;
esac
done
$NU -n -c "
source '$PROVISIONING/core/nulib/lib_minimal.nu'
# Get active workspace
let active_ws = (workspace-active)
if (\$active_ws | is-empty) {
print 'No active workspace'
return
}
# Get workspace path from config
let user_config_path = if (\$env.HOME | path exists) {
(
\$env.HOME | path join 'Library' | path join 'Application Support' |
path join 'provisioning' | path join 'user_config.yaml'
)
} else {
''
}
if not (\$user_config_path | path exists) {
print 'Config not found'
return
}
let config = (open \$user_config_path)
let workspaces = (\$config | get --optional workspaces | default [])
let ws = (\$workspaces | where { \$in.name == \$active_ws } | first)
if (\$ws | is-empty) {
print 'Workspace not found'
return
}
let ws_path = \$ws.path
let infra_path = (\$ws_path | path join 'infra')
if not (\$infra_path | path exists) {
print 'No infrastructures found'
return
}
# Filter by infrastructure if specified
let infra_filter = \"$INFRA_FILTER\"
# List server definitions from infrastructure (filtered if --infra specified)
let servers = (
ls \$infra_path | where type == 'dir' | each {|infra|
let infra_name = (\$infra.name | path basename)
# Skip if filter is specified and doesn't match
if ((\$infra_filter | is-not-empty) and (\$infra_name != \$infra_filter)) {
[]
} else {
let servers_file = ($infra_path | path join \$infra_name | path join 'defs' | path join 'servers.ncl')
let servers_file_kcl = ($infra_path | path join \$infra_name | path join 'defs' | path join 'servers.k')
if ($servers_file | path exists) {
# Parse the Nickel servers.ncl file to extract server hostnames
let content = (open \$servers_file --raw)
# Extract hostnames from hostname = "..." patterns by splitting on quotes
let hostnames = (
\$content
| split row \"\\n\"
| where {|line| \$line | str contains \"hostname = \\\"\" }
| each {|line|
# Split by quotes to extract hostname value
let parts = (\$line | split row \"\\\"\")
if (\$parts | length) >= 2 {
\$parts | get 1
} else {
\"\"
}
}
| where {|h| (\$h | is-not-empty) }
)
\$hostnames | each {|srv_name|
{
name: \$srv_name
infrastructure: \$infra_name
path: \$servers_file
}
}
} else {
[]
}
}
} | flatten
)
if (\$servers | length) == 0 {
print '📦 Available Servers: (none configured)'
} else {
print '📦 Available Servers:'
print ''
\$servers | each {|srv|
print \$\" • (\$srv.name) [(\$srv.infrastructure)]\"
} | ignore
}
" 2>/dev/null
exit $?
fi
fi
# Cluster list (lightweight - reads filesystem only)
if [ "${1:-}" = "cluster" ] || [ "${1:-}" = "cl" ]; then
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
$NU -n -c "
source '$PROVISIONING/core/nulib/lib_minimal.nu'
# Get active workspace
let active_ws = (workspace-active)
if (\$active_ws | is-empty) {
print 'No active workspace'
return
}
# Get workspace path from config
let user_config_path = (
\$env.HOME | path join 'Library' | path join 'Application Support' |
path join 'provisioning' | path join 'user_config.yaml'
)
if not (\$user_config_path | path exists) {
print 'Config not found'
return
}
let config = (open \$user_config_path)
let workspaces = (\$config | get --optional workspaces | default [])
let ws = (\$workspaces | where { \$in.name == \$active_ws } | first)
if (\$ws | is-empty) {
print 'Workspace not found'
return
}
let ws_path = \$ws.path
# List all clusters from workspace
let clusters = (
if ((\$ws_path | path join '.clusters') | path exists) {
let clusters_path = (\$ws_path | path join '.clusters')
ls \$clusters_path | where type == 'dir' | each {|cl|
let cl_name = (\$cl.name | path basename)
{
name: \$cl_name
path: \$cl.name
}
}
} else {
[]
}
)
if (\$clusters | length) == 0 {
print '🗂️ Available Clusters: (none found)'
} else {
print '🗂️ Available Clusters:'
print ''
\$clusters | each {|cl|
print \$\" • (\$cl.name)\"
} | ignore
}
" 2>/dev/null
exit $?
fi
fi
# Infra list (lightweight - reads filesystem only)
if [ "${1:-}" = "infra" ] || [ "${1:-}" = "inf" ]; then
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
$NU -n -c "
source '$PROVISIONING/core/nulib/lib_minimal.nu'
# Get active workspace
let active_ws = (workspace-active)
if (\$active_ws | is-empty) {
print 'No active workspace'
return
}
# Get workspace path from config
let user_config_path = (
\$env.HOME | path join 'Library' | path join 'Application Support' |
path join 'provisioning' | path join 'user_config.yaml'
)
if not (\$user_config_path | path exists) {
print 'Config not found'
return
}
let config = (open \$user_config_path)
let workspaces = (\$config | get --optional workspaces | default [])
let ws = (\$workspaces | where { \$in.name == \$active_ws } | first)
if (\$ws | is-empty) {
print 'Workspace not found'
return
}
let ws_path = \$ws.path
let infra_path = (\$ws_path | path join 'infra')
if not (\$infra_path | path exists) {
print '📁 Available Infrastructures: (none configured)'
return
}
# List all infrastructures
let infras = (
ls \$infra_path | where type == 'dir' | each {|inf|
let inf_name = (\$inf.name | path basename)
let inf_full_path = (\$infra_path | path join \$inf_name)
let has_config = ((\$inf_full_path | path join 'settings.k') | path exists)
{
name: \$inf_name
configured: \$has_config
modified: \$inf.modified
}
}
)
if (\$infras | length) == 0 {
print '📁 Available Infrastructures: (none found)'
} else {
print '📁 Available Infrastructures:'
print ''
\$infras | each {|inf|
let status = if \$inf.configured { '✓' } else { '○' }
let output = \" [\" + \$status + \"] \" + \$inf.name
print \$output
} | ignore
}
" 2>/dev/null
exit $?
fi
fi
# Config validation (lightweight - validates config structure without full load)
if [ "${1:-}" = "validate" ]; then
if [ "${2:-}" = "config" ] || [ -z "${2:-}" ]; then
$NU -n -c "
source '$PROVISIONING/core/nulib/lib_minimal.nu'
try {
# Get active workspace
let active_ws = (workspace-active)
if (\$active_ws | is-empty) {
print '❌ Error: No active workspace'
return
}
# Get workspace path from config
let user_config_path = (
\$env.HOME | path join 'Library' | path join 'Application Support' |
path join 'provisioning' | path join 'user_config.yaml'
)
if not (\$user_config_path | path exists) {
print '❌ Error: User config not found at' \$user_config_path
return
}
let config = (open \$user_config_path)
let workspaces = (\$config | get --optional workspaces | default [])
let ws = (\$workspaces | where { \$in.name == \$active_ws } | first)
if (\$ws | is-empty) {
print '❌ Error: Workspace' \$active_ws 'not found in config'
return
}
let ws_path = \$ws.path
# Validate workspace structure
let required_dirs = ['infra', 'config', '.clusters']
let infra_path = (\$ws_path | path join 'infra')
let config_path = (\$ws_path | path join 'config')
let missing_dirs = \$required_dirs | where { not ((\$ws_path | path join \$in) | path exists) }
if (\$missing_dirs | length) > 0 {
print '⚠️ Warning: Missing directories:' (\$missing_dirs | str join ', ')
}
# Validate infrastructures have required files
if (\$infra_path | path exists) {
let infras = (ls \$infra_path | where type == 'dir')
let invalid_infras = (
\$infras | each {|inf|
let inf_name = (\$inf.name | path basename)
let inf_full_path = (\$infra_path | path join \$inf_name)
if not ((\$inf_full_path | path join 'settings.k') | path exists) {
\$inf_name
} else {
null
}
} | compact
)
if (\$invalid_infras | length) > 0 {
print '⚠️ Warning: Infrastructures missing settings.k:' (\$invalid_infras | str join ', ')
}
}
# Validate user config structure
let has_active = ((\$config | get --optional active_workspace) != null)
let has_workspaces = ((\$config | get --optional workspaces) != null)
let has_preferences = ((\$config | get --optional preferences) != null)
if not \$has_active {
print '⚠️ Warning: Missing active_workspace in user config'
}
if not \$has_workspaces {
print '⚠️ Warning: Missing workspaces list in user config'
}
if not \$has_preferences {
print '⚠️ Warning: Missing preferences in user config'
}
# Summary
print ''
print '✓ Configuration validation complete for workspace:' \$active_ws
print ' Path:' \$ws_path
print ' Status: Valid (with warnings, if any listed above)'
} catch {|err|
print '❌ Validation error:' \$err
}
" 2>/dev/null
exit $?
fi
fi
if [ ! -d "$PROVISIONING_USER_CONFIG" ] || [ ! -r "$PROVISIONING_CONTEXT_PATH" ]; then
[ ! -x "$PROVISIONING/core/nulib/provisioning setup" ] && echo "$PROVISIONING/core/nulib/provisioning setup not found" && exit 1
cd "$PROVISIONING/core/nulib"
./"provisioning setup"
echo ""
read -p "Use [enter] to continue or [ctrl-c] to cancel"
fi
[ ! -r "$PROVISIONING_USER_CONFIG/config.nu" ] && echo "$PROVISIONING_USER_CONFIG/config.nu not found" && exit 1
[ ! -r "$PROVISIONING_USER_CONFIG/env.nu" ] && echo "$PROVISIONING_USER_CONFIG/env.nu not found" && exit 1
NU_ARGS=(--config "$PROVISIONING_USER_CONFIG/config.nu" --env-config "$PROVISIONING_USER_CONFIG/env.nu")
export PROVISIONING_ARGS="$CMD_ARGS" NU_ARGS="$NU_ARGS"
#export NU_ARGS=${NU_ARGS//Application Support/Application\\ Support}
# Suppress repetitive config export output during initialization
export PROVISIONING_QUIET_EXPORT="true"
# Export NU_LIB_DIRS so Nushell can find modules during parsing
export NU_LIB_DIRS="$PROVISIONING/core/nulib:/opt/provisioning/core/nulib:/usr/local/provisioning/core/nulib"
# ============================================================================
# DAEMON ROUTING - ENABLED (Phase 3.7: CLI Daemon Integration)
# ============================================================================
# Redesigned daemon with pre-loaded Nushell environment (no CLI callback).
# Routes eligible commands to HTTP daemon for <100ms execution.
# Gracefully falls back to full load if daemon unavailable.
#
# ARCHITECTURE:
# 1. Check daemon health (curl with 5ms timeout)
# 2. Route eligible commands to daemon via HTTP POST
# 3. Fall back to full load if daemon unavailable
# 4. Zero breaking changes (graceful degradation)
#
# PERFORMANCE:
# - With daemon: <100ms for ALL commands
# - Without daemon: ~430ms (normal behavior)
# - Daemon fallback: Automatic, user sees no difference
if [ -n "$PROVISIONING_MODULE" ]; then
if [[ -x $PROVISIONING/core/nulib/$RUNNER\ $PROVISIONING_MODULE ]]; then
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/$RUNNER $PROVISIONING_MODULE" $CMD_ARGS
else
echo "Error \"$PROVISIONING/core/nulib/$RUNNER $PROVISIONING_MODULE\" not found"
fi
else
# Only redirect stdin for non-interactive commands (nu command needs interactive stdin)
if [ "${1:-}" = "nu" ]; then
# For interactive mode, start nu with provisioning environment
export PROVISIONING_CONFIG="$PROVISIONING_USER_CONFIG"
# Start nu interactively - it will use the config and env from NU_ARGS
$NU "${NU_ARGS[@]}"
else
# Don't redirect stdin for infrastructure commands - they may need interactive input
# Only redirect for commands we know are safe
case "${1:-}" in
help | h | --help | --info | -i | -v | --version | env | allenv | status | health | list | ls | l | workspace | ws | provider | providers | validate | plugin | plugins | nuinfo | platform | plat)
# Safe commands - can use /dev/null
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/$RUNNER" $CMD_ARGS </dev/null
;;
*)
# All other commands (create, delete, server, taskserv, etc.) - keep stdin open
# NOTE: PROVISIONING_MODULE is automatically inherited by Nushell from bash environment
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/$RUNNER" $CMD_ARGS
;;
esac
fi
fi

View file

@ -221,4 +221,4 @@ def print_help [] {
print " pack clean --all" print " pack clean --all"
print "" print ""
print "Distribution configuration in: provisioning/config/config.defaults.toml [distribution]" print "Distribution configuration in: provisioning/config/config.defaults.toml [distribution]"
} }

View file

@ -1,29 +1,29 @@
#!/bin/bash #!/bin/bash
# Info: Script to install providers # Info: Script to install providers
# Author: JesusPerezLorenzo # Author: JesusPerezLorenzo
# Release: 1.0 # Release: 1.0
# Date: 12-11-2023 # Date: 12-11-2023
[ "$DEBUG" == "-x" ] && set -x [ "$DEBUG" == "-x" ] && set -x
USAGE="install-tools [ tool-name: tera k9s, etc | all] [--update] USAGE="install-tools [ tool-name: tera k9s, etc | all] [--update]
As alternative use environment var TOOL_TO_INSTALL with a list-of-tools (separeted with spaces) As alternative use environment var TOOL_TO_INSTALL with a list-of-tools (separeted with spaces)
Versions are set in ./versions file Versions are set in ./versions file
This can be called by directly with an argumet or from an other srcipt This can be called by directly with an argumet or from an other srcipt
" "
ORG=$(pwd) ORG=$(pwd)
function _install_cmds { function _install_cmds {
OS="$(uname | tr '[:upper:]' '[:lower:]')" OS="$(uname | tr '[:upper:]' '[:lower:]')"
local has_cmd local has_cmd
for cmd in $CMDS_PROVISIONING for cmd in $CMDS_PROVISIONING
do do
has_cmd=$(type -P $cmd) has_cmd=$(type -P $cmd)
if [ -z "$has_cmd" ] ; then if [ -z "$has_cmd" ] ; then
case "$(OS)" in case "$(OS)" in
darwin) brew install $cmd ;; darwin) brew install $cmd ;;
linux) sudo apt install $cmd ;; linux) sudo apt install $cmd ;;
*) echo "Install $cmd in your PATH" ;; *) echo "Install $cmd in your PATH" ;;
@ -41,8 +41,8 @@ function _install_tools {
# local jq_version # local jq_version
# local has_yq # local has_yq
# local yq_version # local yq_version
local has_kcl local has_nickel
local kcl_version local nickel_version
local has_tera local has_tera
local tera_version local tera_version
local has_k9s local has_k9s
@ -56,21 +56,21 @@ function _install_tools {
# local has_aws # local has_aws
# local aws_version # local aws_version
OS="$(uname | tr '[:upper:]' '[:lower:]')" OS="$(uname | tr '[:upper:]' '[:lower:]')"
ORG_OS=$(uname) ORG_OS=$(uname)
ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')" ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')"
ORG_ARCH="$(uname -m)" ORG_ARCH="$(uname -m)"
if [ -z "$CHECK_ONLY" ] and [ "$match" == "all" ] ; then if [ -z "$CHECK_ONLY" ] and [ "$match" == "all" ] ; then
_install_cmds _install_cmds
fi fi
# if [ -n "$JQ_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "jq" ] ; then # if [ -n "$JQ_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "jq" ] ; then
# has_jq=$(type -P jq) # has_jq=$(type -P jq)
# num_version="0" # num_version="0"
# [ -n "$has_jq" ] && jq_version=$(jq -V | sed 's/jq-//g') && num_version=${jq_version//\./} # [ -n "$has_jq" ] && jq_version=$(jq -V | sed 's/jq-//g') && num_version=${jq_version//\./}
# expected_version_num=${JQ_VERSION//\./} # expected_version_num=${JQ_VERSION//\./}
# if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then # if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
# curl -fsSLO "https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/jq-${OS}-${ARCH}" && # curl -fsSLO "https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/jq-${OS}-${ARCH}" &&
# chmod +x "jq-${OS}-${ARCH}" && # chmod +x "jq-${OS}-${ARCH}" &&
# sudo mv "jq-${OS}-${ARCH}" /usr/local/bin/jq && # sudo mv "jq-${OS}-${ARCH}" /usr/local/bin/jq &&
@ -81,16 +81,16 @@ function _install_tools {
# printf "%s\t%s\n" "jq" "already $JQ_VERSION" # printf "%s\t%s\n" "jq" "already $JQ_VERSION"
# fi # fi
# fi # fi
# if [ -n "$YQ_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "yq" ] ; then # if [ -n "$YQ_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "yq" ] ; then
# has_yq=$(type -P yq) # has_yq=$(type -P yq)
# num_version="0" # num_version="0"
# [ -n "$has_yq" ] && yq_version=$(yq -V | cut -f4 -d" " | sed 's/v//g') && num_version=${yq_version//\./} # [ -n "$has_yq" ] && yq_version=$(yq -V | cut -f4 -d" " | sed 's/v//g') && num_version=${yq_version//\./}
# expected_version_num=${YQ_VERSION//\./} # expected_version_num=${YQ_VERSION//\./}
# if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then # if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
# curl -fsSLO "https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_${OS}_${ARCH}.tar.gz" && # curl -fsSLO "https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_${OS}_${ARCH}.tar.gz" &&
# tar -xzf "yq_${OS}_${ARCH}.tar.gz" && # tar -xzf "yq_${OS}_${ARCH}.tar.gz" &&
# sudo mv "yq_${OS}_${ARCH}" /usr/local/bin/yq && # sudo mv "yq_${OS}_${ARCH}" /usr/local/bin/yq &&
# sudo ./install-man-page.sh && # sudo ./install-man-page.sh &&
# rm -f install-man-page.sh yq.1 "yq_${OS}_${ARCH}.tar.gz" && # rm -f install-man-page.sh yq.1 "yq_${OS}_${ARCH}.tar.gz" &&
# printf "%s\t%s\n" "yq" "installed $YQ_VERSION" # printf "%s\t%s\n" "yq" "installed $YQ_VERSION"
# elif [ -n "$CHECK_ONLY" ] ; then # elif [ -n "$CHECK_ONLY" ] ; then
@ -99,36 +99,34 @@ function _install_tools {
# printf "%s\t%s\n" "yq" "already $YQ_VERSION" # printf "%s\t%s\n" "yq" "already $YQ_VERSION"
# fi # fi
# fi # fi
if [ -n "$NICKEL_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "nickel" ] ; then
if [ -n "$KCL_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "kcl" ] ; then has_nickel=$(type -P nickel)
has_kcl=$(type -P kcl)
num_version="0" num_version="0"
[ -n "$has_kcl" ] && kcl_version=$(kcl -v | cut -f3 -d" " | sed 's/ //g') && num_version=${kcl_version//\./} [ -n "$has_nickel" ] && nickel_version=$(nickel --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) && num_version=${nickel_version//\./}
expected_version_num=${KCL_VERSION//\./} expected_version_num=${NICKEL_VERSION//\./}
if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
curl -fsSLO "https://github.com/kcl-lang/cli/releases/download/v${KCL_VERSION}/kcl-v${KCL_VERSION}-${OS}-${ARCH}.tar.gz" && echo "⚠️ Nickel installation/update required"
tar -xzf "kcl-v${KCL_VERSION}-${OS}-${ARCH}.tar.gz" && echo " Recommended method: nix profile install nixpkgs#nickel"
sudo mv kcl /usr/local/bin/kcl && echo " Alternative: cargo install nickel-lang-cli --version ${NICKEL_VERSION}"
rm -f "kcl-v${KCL_VERSION}-${OS}-${ARCH}.tar.gz" && echo " https://nickel-lang.org/getting-started"
printf "%s\t%s\n" "kcl" "installed $KCL_VERSION"
elif [ -n "$CHECK_ONLY" ] ; then elif [ -n "$CHECK_ONLY" ] ; then
printf "%s\t%s\t%s\n" "kcl" "$kcl_version" "expected $KCL_VERSION" printf "%s\t%s\t%s\n" "nickel" "$nickel_version" "expected $NICKEL_VERSION"
else else
printf "%s\t%s\n" "kcl" "already $KCL_VERSION" printf "%s\t%s\n" "nickel" "already $NICKEL_VERSION"
fi fi
fi fi
if [ -n "$TERA_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "tera" ] ; then if [ -n "$TERA_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "tera" ] ; then
has_tera=$(type -P tera) has_tera=$(type -P tera)
num_version="0" num_version="0"
[ -n "$has_tera" ] && tera_version=$(tera -V | cut -f2 -d" " | sed 's/teracli//g') && num_version=${tera_version//\./} [ -n "$has_tera" ] && tera_version=$(tera -V | cut -f2 -d" " | sed 's/teracli//g') && num_version=${tera_version//\./}
expected_version_num=${TERA_VERSION//\./} expected_version_num=${TERA_VERSION//\./}
if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
if [ -x "$(dirname "$0")/../tools/tera_${OS}_${ARCH}" ] ; then if [ -x "$(dirname "$0")/../tools/tera_${OS}_${ARCH}" ] ; then
sudo cp "$(dirname "$0")/../tools/tera_${OS}_${ARCH}" /usr/local/bin/tera && printf "%s\t%s\n" "tera" "installed $TERA_VERSION" sudo cp "$(dirname "$0")/../tools/tera_${OS}_${ARCH}" /usr/local/bin/tera && printf "%s\t%s\n" "tera" "installed $TERA_VERSION"
else else
echo "Error: $(dirname "$0")/../ttools/tera_${OS}_${ARCH} not found !!" echo "Error: $(dirname "$0")/../ttools/tera_${OS}_${ARCH} not found !!"
exit 2 exit 2
fi fi
elif [ -n "$CHECK_ONLY" ] ; then elif [ -n "$CHECK_ONLY" ] ; then
printf "%s\t%s\t%s\n" "tera" "$tera_version" "expected $TERA_VERSION" printf "%s\t%s\t%s\n" "tera" "$tera_version" "expected $TERA_VERSION"
else else
@ -140,9 +138,9 @@ function _install_tools {
num_version="0" num_version="0"
[ -n "$has_k9s" ] && k9s_version="$( k9s version | grep Version | cut -f2 -d"v" | sed 's/ //g')" && num_version=${k9s_version//\./} [ -n "$has_k9s" ] && k9s_version="$( k9s version | grep Version | cut -f2 -d"v" | sed 's/ //g')" && num_version=${k9s_version//\./}
expected_version_num=${K9S_VERSION//\./} expected_version_num=${K9S_VERSION//\./}
if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
mkdir -p k9s && cd k9s && mkdir -p k9s && cd k9s &&
curl -fsSLO https://github.com/derailed/k9s/releases/download/v${K9S_VERSION}/k9s_${ORG_OS}_${ARCH}.tar.gz && curl -fsSLO https://github.com/derailed/k9s/releases/download/v${K9S_VERSION}/k9s_${ORG_OS}_${ARCH}.tar.gz &&
tar -xzf "k9s_${ORG_OS}_${ARCH}.tar.gz" && tar -xzf "k9s_${ORG_OS}_${ARCH}.tar.gz" &&
sudo mv k9s /usr/local/bin && sudo mv k9s /usr/local/bin &&
cd "$ORG" && rm -rf /tmp/k9s "/k9s_${ORG_OS}_${ARCH}.tar.gz" && cd "$ORG" && rm -rf /tmp/k9s "/k9s_${ORG_OS}_${ARCH}.tar.gz" &&
@ -158,12 +156,12 @@ function _install_tools {
num_version="0" num_version="0"
[ -n "$has_age" ] && age_version="${AGE_VERSION}" && num_version=${age_version//\./} [ -n "$has_age" ] && age_version="${AGE_VERSION}" && num_version=${age_version//\./}
expected_version_num=${AGE_VERSION//\./} expected_version_num=${AGE_VERSION//\./}
if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
curl -fsSLO https://github.com/FiloSottile/age/releases/download/v${AGE_VERSION}/age-v${AGE_VERSION}-${OS}-${ARCH}.tar.gz && curl -fsSLO https://github.com/FiloSottile/age/releases/download/v${AGE_VERSION}/age-v${AGE_VERSION}-${OS}-${ARCH}.tar.gz &&
tar -xzf age-v${AGE_VERSION}-${OS}-${ARCH}.tar.gz && tar -xzf age-v${AGE_VERSION}-${OS}-${ARCH}.tar.gz &&
sudo mv age/age /usr/local/bin && sudo mv age/age /usr/local/bin &&
sudo mv age/age-keygen /usr/local/bin && sudo mv age/age-keygen /usr/local/bin &&
rm -rf age "age-v${AGE_VERSION}-${OS}-${ARCH}.tar.gz" && rm -rf age "age-v${AGE_VERSION}-${OS}-${ARCH}.tar.gz" &&
printf "%s\t%s\n" "age" "installed $AGE_VERSION" printf "%s\t%s\n" "age" "installed $AGE_VERSION"
elif [ -n "$CHECK_ONLY" ] ; then elif [ -n "$CHECK_ONLY" ] ; then
printf "%s\t%s\t%s\n" "age" "$age_version" "expected $AGE_VERSION" printf "%s\t%s\t%s\n" "age" "$age_version" "expected $AGE_VERSION"
@ -176,11 +174,11 @@ function _install_tools {
num_version="0" num_version="0"
[ -n "$has_sops" ] && sops_version="$(sops -v | cut -f2 -d" " | sed 's/ //g')" && num_version=${sops_version//\./} [ -n "$has_sops" ] && sops_version="$(sops -v | cut -f2 -d" " | sed 's/ //g')" && num_version=${sops_version//\./}
expected_version_num=${SOPS_VERSION//\./} expected_version_num=${SOPS_VERSION//\./}
if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
mkdir -p sops && cd sops && mkdir -p sops && cd sops &&
curl -fsSLO https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.${OS}.${ARCH} && curl -fsSLO https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.${OS}.${ARCH} &&
mv sops-v${SOPS_VERSION}.${OS}.${ARCH} sops && mv sops-v${SOPS_VERSION}.${OS}.${ARCH} sops &&
chmod +x sops && chmod +x sops &&
sudo mv sops /usr/local/bin && sudo mv sops /usr/local/bin &&
rm -f sops-v${SOPS_VERSION}.${OS}.${ARCH} sops && rm -f sops-v${SOPS_VERSION}.${OS}.${ARCH} sops &&
printf "%s\t%s\n" "sops" "installed $SOPS_VERSION" printf "%s\t%s\n" "sops" "installed $SOPS_VERSION"
@ -195,9 +193,9 @@ function _install_tools {
# num_version="0" # num_version="0"
# [ -n "$has_upctl" ] && upctl_version=$(upctl version | grep "Version" | cut -f2 -d":" | sed 's/ //g') && num_version=${upctl_version//\./} # [ -n "$has_upctl" ] && upctl_version=$(upctl version | grep "Version" | cut -f2 -d":" | sed 's/ //g') && num_version=${upctl_version//\./}
# expected_version_num=${UPCTL_VERSION//\./} # expected_version_num=${UPCTL_VERSION//\./}
# if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then # if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
# mkdir -p upctl && cd upctl && # mkdir -p upctl && cd upctl &&
# curl -fsSLO https://github.com/UpCloudLtd/upcloud-cli/releases/download/v${UPCTL_VERSION}/upcloud-cli_${UPCTL_VERSION}_${OS}_${ORG_ARCH}.tar.gz && # curl -fsSLO https://github.com/UpCloudLtd/upcloud-cli/releases/download/v${UPCTL_VERSION}/upcloud-cli_${UPCTL_VERSION}_${OS}_${ORG_ARCH}.tar.gz &&
# tar -xzf "upcloud-cli_${UPCTL_VERSION}_${OS}_${ORG_ARCH}.tar.gz" && # tar -xzf "upcloud-cli_${UPCTL_VERSION}_${OS}_${ORG_ARCH}.tar.gz" &&
# sudo mv upctl /usr/local/bin && # sudo mv upctl /usr/local/bin &&
# cd "$ORG" && rm -rf /tmp/upct "/upcloud-cli_${UPCTL_VERSION}_${OS}_${ORG_ARCH}.tar.gz" # cd "$ORG" && rm -rf /tmp/upct "/upcloud-cli_${UPCTL_VERSION}_${OS}_${ORG_ARCH}.tar.gz"
@ -209,16 +207,16 @@ function _install_tools {
# fi # fi
# fi # fi
# if [ -n "$AWS_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "aws" ] ; then # if [ -n "$AWS_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "aws" ] ; then
# [ -r "/usr/bin/aws" ] && mv /usr/bin/aws /usr/bin/_aws # [ -r "/usr/bin/aws" ] && mv /usr/bin/aws /usr/bin/_aws
# has_aws=$(type -P aws) # has_aws=$(type -P aws)
# num_version="0" # num_version="0"
# [ -n "$has_aws" ] && aws_version=$(aws --version | cut -f1 -d" " | sed 's,aws-cli/,,g') && num_version=${aws_version//\./} # [ -n "$has_aws" ] && aws_version=$(aws --version | cut -f1 -d" " | sed 's,aws-cli/,,g') && num_version=${aws_version//\./}
# expected_version_num=${AWS_VERSION//\./} # expected_version_num=${AWS_VERSION//\./}
# if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then # if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
# cd "$ORG" || exit 1 # cd "$ORG" || exit 1
# curl "https://awscli.amazonaws.com/awscli-exe-${OS}-${ORG_ARCH}.zip" -o "awscliv2.zip" # curl "https://awscli.amazonaws.com/awscli-exe-${OS}-${ORG_ARCH}.zip" -o "awscliv2.zip"
# unzip awscliv2.zip >/dev/null # unzip awscliv2.zip >/dev/null
# [ "$1" != "-update" ] && [ -d "/usr/local/aws-cli" ] && sudo rm -rf "/usr/local/aws-cli" # [ "$1" != "-update" ] && [ -d "/usr/local/aws-cli" ] && sudo rm -rf "/usr/local/aws-cli"
# sudo ./aws/install && printf "%s\t%s\n" "aws" "installed $AWS_VERSION" # sudo ./aws/install && printf "%s\t%s\n" "aws" "installed $AWS_VERSION"
# #sudo ./aws/install $options && echo "aws cli installed" # #sudo ./aws/install $options && echo "aws cli installed"
# cd "$ORG" && rm -rf awscliv2.zip # cd "$ORG" && rm -rf awscliv2.zip
@ -230,9 +228,9 @@ function _install_tools {
# fi # fi
} }
function get_providers { function get_providers {
local list local list
local name local name
for item in $PROVIDERS_PATH/* for item in $PROVIDERS_PATH/*
do do
@ -250,26 +248,26 @@ function get_providers {
function _on_providers { function _on_providers {
local providers_list=$1 local providers_list=$1
[ -z "$providers_list" ] || [[ "$providers_list" == -* ]] && providers_list=${PROVISIONING_PROVIDERS:-all} [ -z "$providers_list" ] || [[ "$providers_list" == -* ]] && providers_list=${PROVISIONING_PROVIDERS:-all}
if [ "$providers_list" == "all" ] ; then if [ "$providers_list" == "all" ] ; then
providers_list=$(get_providers) providers_list=$(get_providers)
fi fi
for provider in $providers_list for provider in $providers_list
do do
[ ! -d "$PROVIDERS_PATH/$provider/templates" ] && [ ! -r "$PROVIDERS_PATH/$provider/provisioning.yam" ] && continue [ ! -d "$PROVIDERS_PATH/$provider/templates" ] && [ ! -r "$PROVIDERS_PATH/$provider/provisioning.yam" ] && continue
if [ ! -r "$PROVIDERS_PATH/$provider/bin/install.sh" ] ; then if [ ! -r "$PROVIDERS_PATH/$provider/bin/install.sh" ] ; then
echo "🛑 Error on $provider no $PROVIDERS_PATH/$provider/bin/install.sh found" echo "🛑 Error on $provider no $PROVIDERS_PATH/$provider/bin/install.sh found"
continue continue
fi fi
"$PROVIDERS_PATH/$provider/bin/install.sh" "$@" "$PROVIDERS_PATH/$provider/bin/install.sh" "$@"
done done
} }
set -o allexport set -o allexport
## shellcheck disable=SC1090 ## shellcheck disable=SC1090
[ -n "$PROVISIONING_ENV" ] && [ -r "$PROVISIONING_ENV" ] && source "$PROVISIONING_ENV" [ -n "$PROVISIONING_ENV" ] && [ -r "$PROVISIONING_ENV" ] && source "$PROVISIONING_ENV"
[ -r "../env-provisioning" ] && source ../env-provisioning [ -r "../env-provisioning" ] && source ../env-provisioning
[ -r "env-provisioning" ] && source ./env-provisioning [ -r "env-provisioning" ] && source ./env-provisioning
#[ -r ".env" ] && source .env set #[ -r ".env" ] && source .env set
set +o allexport set +o allexport
export PROVISIONING=${PROVISIONING:-/usr/local/provisioning} export PROVISIONING=${PROVISIONING:-/usr/local/provisioning}

File diff suppressed because it is too large Load diff

View file

@ -1,28 +1,28 @@
#!/bin/bash #!/bin/bash
# Info: Script to install tools # Info: Script to install tools
# Author: JesusPerezLorenzo # Author: JesusPerezLorenzo
# Release: 1.0 # Release: 1.0
# Date: 12-11-2023 # Date: 12-11-2023
[ "$DEBUG" == "-x" ] && set -x [ "$DEBUG" == "-x" ] && set -x
USAGE="install-tools [ tool-name: providers tera k9s, etc | all] [--update] USAGE="install-tools [ tool-name: providers tera k9s, etc | all] [--update]
As alternative use environment var TOOL_TO_INSTALL with a list-of-tools (separeted with spaces) As alternative use environment var TOOL_TO_INSTALL with a list-of-tools (separeted with spaces)
Versions are set in ./versions file Versions are set in ./versions file
This can be called by directly with an argumet or from an other srcipt This can be called by directly with an argumet or from an other srcipt
" "
ORG=$(pwd) ORG=$(pwd)
function _install_cmds { function _install_cmds {
OS="$(uname | tr '[:upper:]' '[:lower:]')" OS="$(uname | tr '[:upper:]' '[:lower:]')"
local has_cmd local has_cmd
for cmd in $CMDS_PROVISIONING for cmd in $CMDS_PROVISIONING
do do
has_cmd=$(type -P $cmd) has_cmd=$(type -P $cmd)
if [ -z "$has_cmd" ] ; then if [ -z "$has_cmd" ] ; then
case "$OS" in case "$OS" in
darwin) brew install $cmd ;; darwin) brew install $cmd ;;
linux) sudo apt install $cmd ;; linux) sudo apt install $cmd ;;
*) echo "Install $cmd in your PATH" ;; *) echo "Install $cmd in your PATH" ;;
@ -37,19 +37,19 @@ function _install_providers {
local info_keys local info_keys
options="$*" options="$*"
info_keys="info version site" info_keys="info version site"
if [ -z "$match" ] || [ "$match" == "all" ] || [ "$match" == "-" ]; then if [ -z "$match" ] || [ "$match" == "all" ] || [ "$match" == "-" ]; then
match="all" match="all"
fi fi
for prov in $(ls $PROVIDERS_PATH | grep -v "^_" ) for prov in $(ls $PROVIDERS_PATH | grep -v "^_" )
do do
prov_name=$(basename "$prov") prov_name=$(basename "$prov")
[ ! -d "$PROVIDERS_PATH/$prov_name/templates" ] && continue [ ! -d "$PROVIDERS_PATH/$prov_name/templates" ] && continue
if [ "$match" == "all" ] || [ "$prov_name" == "$match" ] ; then if [ "$match" == "all" ] || [ "$prov_name" == "$match" ] ; then
[ -x "$PROVIDERS_PATH/$prov_name/bin/install.sh" ] && $PROVIDERS_PATH/$prov_name/bin/install.sh $options [ -x "$PROVIDERS_PATH/$prov_name/bin/install.sh" ] && $PROVIDERS_PATH/$prov_name/bin/install.sh $options
elif [ "$match" == "?" ] ; then elif [ "$match" == "?" ] ; then
[ -n "$options" ] && [ -z "$(echo "$options" | grep ^$prov_name)" ] && continue [ -n "$options" ] && [ -z "$(echo "$options" | grep ^$prov_name)" ] && continue
if [ -r "$PROVIDERS_PATH/$prov_name/provisioning.yaml" ] ; then if [ -r "$PROVIDERS_PATH/$prov_name/provisioning.yaml" ] ; then
echo "-------------------------------------------------------" echo "-------------------------------------------------------"
for key in $info_keys for key in $info_keys
do do
@ -58,7 +58,7 @@ function _install_providers {
echo " $(grep "^$key:" "$PROVIDERS_PATH/$prov_name/provisioning.yaml" | sed "s/$key: //g")" echo " $(grep "^$key:" "$PROVIDERS_PATH/$prov_name/provisioning.yaml" | sed "s/$key: //g")"
done done
[ -n "$options" ] && echo "________________________________________________________" [ -n "$options" ] && echo "________________________________________________________"
else else
echo "$prov_name" echo "$prov_name"
fi fi
fi fi
@ -76,8 +76,8 @@ function _install_tools {
# local yq_version # local yq_version
local has_nu local has_nu
local nu_version local nu_version
local has_kcl local has_nickel
local kcl_version local nickel_version
local has_tera local has_tera
local tera_version local tera_version
local has_k9s local has_k9s
@ -87,21 +87,21 @@ function _install_tools {
local has_sops local has_sops
local sops_version local sops_version
OS="$(uname | tr '[:upper:]' '[:lower:]')" OS="$(uname | tr '[:upper:]' '[:lower:]')"
ORG_OS=$(uname) ORG_OS=$(uname)
ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')" ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')"
ORG_ARCH="$(uname -m)" ORG_ARCH="$(uname -m)"
if [ -z "$CHECK_ONLY" ] && [ "$match" == "all" ] ; then if [ -z "$CHECK_ONLY" ] && [ "$match" == "all" ] ; then
_install_cmds _install_cmds
fi fi
# if [ -n "$JQ_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "jq" ] ; then # if [ -n "$JQ_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "jq" ] ; then
# has_jq=$(type -P jq) # has_jq=$(type -P jq)
# num_version="0" # num_version="0"
# [ -n "$has_jq" ] && jq_version=$(jq -V | sed 's/jq-//g') && num_version=${jq_version//\./} # [ -n "$has_jq" ] && jq_version=$(jq -V | sed 's/jq-//g') && num_version=${jq_version//\./}
# expected_version_num=${JQ_VERSION//\./} # expected_version_num=${JQ_VERSION//\./}
# if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then # if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
# curl -fsSLO "https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/jq-${OS}-${ARCH}" && # curl -fsSLO "https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/jq-${OS}-${ARCH}" &&
# chmod +x "jq-${OS}-${ARCH}" && # chmod +x "jq-${OS}-${ARCH}" &&
# sudo mv "jq-${OS}-${ARCH}" /usr/local/bin/jq && # sudo mv "jq-${OS}-${ARCH}" /usr/local/bin/jq &&
@ -112,16 +112,16 @@ function _install_tools {
# printf "%s\t%s\n" "jq" "already $JQ_VERSION" # printf "%s\t%s\n" "jq" "already $JQ_VERSION"
# fi # fi
# fi # fi
# if [ -n "$YQ_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "yq" ] ; then # if [ -n "$YQ_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "yq" ] ; then
# has_yq=$(type -P yq) # has_yq=$(type -P yq)
# num_version="0" # num_version="0"
# [ -n "$has_yq" ] && yq_version=$(yq -V | cut -f4 -d" " | sed 's/v//g') && num_version=${yq_version//\./} # [ -n "$has_yq" ] && yq_version=$(yq -V | cut -f4 -d" " | sed 's/v//g') && num_version=${yq_version//\./}
# expected_version_num=${YQ_VERSION//\./} # expected_version_num=${YQ_VERSION//\./}
# if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then # if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
# curl -fsSLO "https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_${OS}_${ARCH}.tar.gz" && # curl -fsSLO "https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_${OS}_${ARCH}.tar.gz" &&
# tar -xzf "yq_${OS}_${ARCH}.tar.gz" && # tar -xzf "yq_${OS}_${ARCH}.tar.gz" &&
# sudo mv "yq_${OS}_${ARCH}" /usr/local/bin/yq && # sudo mv "yq_${OS}_${ARCH}" /usr/local/bin/yq &&
# sudo ./install-man-page.sh && # sudo ./install-man-page.sh &&
# rm -f install-man-page.sh yq.1 "yq_${OS}_${ARCH}.tar.gz" && # rm -f install-man-page.sh yq.1 "yq_${OS}_${ARCH}.tar.gz" &&
# printf "%s\t%s\n" "yq" "installed $YQ_VERSION" # printf "%s\t%s\n" "yq" "installed $YQ_VERSION"
# elif [ -n "$CHECK_ONLY" ] ; then # elif [ -n "$CHECK_ONLY" ] ; then
@ -131,16 +131,16 @@ function _install_tools {
# fi # fi
# fi # fi
if [ -n "$NU_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "nu" ] ; then if [ -n "$NU_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "nu" ] ; then
has_nu=$(type -P nu) has_nu=$(type -P nu)
num_version="0" num_version="0"
[ -n "$has_nu" ] && nu_version=$(nu -v) && num_version=${nu_version//\./} && num_version=${num_version//0/} [ -n "$has_nu" ] && nu_version=$(nu -v) && num_version=${nu_version//\./} && num_version=${num_version//0/}
expected_version_num=${NU_VERSION//\./} expected_version_num=${NU_VERSION//\./}
expected_version_num=${expected_version_num//0/} expected_version_num=${expected_version_num//0/}
[ -z "$num_version" ] && num_version=0 [ -z "$num_version" ] && num_version=0
if [ -z "$num_version" ] && [ "$num_version" -lt "$expected_version_num" ] ; then if [ -z "$num_version" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
printf "%s\t%s\t%s\n" "nu" "$nu_version" "expected $NU_VERSION require installation" printf "%s\t%s\t%s\n" "nu" "$nu_version" "expected $NU_VERSION require installation"
elif [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then elif [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
printf "%s\t%s\t%s\n" "nu" "$nu_version" "expected $NU_VERSION require installation" printf "%s\t%s\t%s\n" "nu" "$nu_version" "expected $NU_VERSION require installation"
elif [ -n "$CHECK_ONLY" ] ; then elif [ -n "$CHECK_ONLY" ] ; then
printf "%s\t%s\t%s\n" "nu" "$nu_version" "expected $NU_VERSION" printf "%s\t%s\t%s\n" "nu" "$nu_version" "expected $NU_VERSION"
@ -148,37 +148,81 @@ function _install_tools {
printf "%s\t%s\n" "nu" "already $NU_VERSION" printf "%s\t%s\n" "nu" "already $NU_VERSION"
fi fi
fi fi
if [ -n "$KCL_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "kcl" ] ; then if [ -n "$NICKEL_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "nickel" ] ; then
has_kcl=$(type -P kcl) has_nickel=$(type -P nickel)
num_version=0 num_version=0
[ -n "$has_kcl" ] && kcl_version=$(kcl -v | cut -f3 -d" " | sed 's/ //g') && num_version=${kcl_version//\./} [ -n "$has_nickel" ] && nickel_version=$((nickel -V | cut -f3 -d" ") 2>/dev/null) && num_version=${nickel_version//\./}
expected_version_num=${KCL_VERSION//\./} expected_version_num=${NICKEL_VERSION//\./}
[ -z "$num_version" ] && num_version=0 [ -z "$num_version" ] && num_version=0
if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
curl -fsSLO "https://github.com/kcl-lang/cli/releases/download/v${KCL_VERSION}/kcl-v${KCL_VERSION}-${OS}-${ARCH}.tar.gz" && # macOS: try Cargo first, then Homebrew
tar -xzf "kcl-v${KCL_VERSION}-${OS}-${ARCH}.tar.gz" && if [ "$OS" == "darwin" ] ; then
sudo mv kcl /usr/local/bin/kcl && printf "%s\t%s\n" "nickel" "installing $NICKEL_VERSION on macOS"
rm -f "kcl-v${KCL_VERSION}-${OS}-${ARCH}.tar.gz" &&
printf "%s\t%s\n" "kcl" "installed $KCL_VERSION" # Try Cargo first (if available)
if command -v cargo >/dev/null 2>&1 ; then
printf "%s\t%s\n" "nickel" "using Cargo (Rust compiler)"
if cargo install nickel-lang-cli --version "${NICKEL_VERSION}" ; then
printf "%s\t%s\n" "nickel" "✅ installed $NICKEL_VERSION via Cargo"
else
printf "%s\t%s\n" "nickel" "❌ Failed to build with Cargo"
exit 1
fi
# Try Homebrew if Cargo not available
elif command -v brew >/dev/null 2>&1 ; then
printf "%s\t%s\n" "nickel" "using Homebrew"
if brew install nickel ; then
printf "%s\t%s\n" "nickel" "✅ installed $NICKEL_VERSION via Homebrew"
else
printf "%s\t%s\n" "nickel" "❌ Failed to install with Homebrew"
exit 1
fi
else
# Neither Cargo nor Homebrew available
printf "%s\t%s\n" "nickel" "⚠️ Neither Cargo nor Homebrew found"
printf "%s\t%s\n" "nickel" "Install one of:"
printf "%s\t%s\n" "nickel" " 1. Cargo: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
printf "%s\t%s\n" "nickel" " 2. Homebrew: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
exit 1
fi
else
# Non-macOS: download binary from GitHub
printf "%s\t%s\n" "nickel" "installing $NICKEL_VERSION on $OS"
# Map architecture names (GitHub uses different naming)
local nickel_arch="$ARCH"
[ "$nickel_arch" == "amd64" ] && nickel_arch="x86_64"
# Build download URL
local download_url="https://github.com/tweag/nickel/releases/download/${NICKEL_VERSION}/nickel-${nickel_arch}-${OS}"
# Download and install
if curl -fsSLO "$download_url" && chmod +x "nickel-${nickel_arch}-${OS}" && sudo mv "nickel-${nickel_arch}-${OS}" /usr/local/bin/nickel ; then
printf "%s\t%s\n" "nickel" "installed $NICKEL_VERSION"
else
printf "%s\t%s\n" "nickel" "❌ Failed to download/install Nickel binary"
exit 1
fi
fi
elif [ -n "$CHECK_ONLY" ] ; then elif [ -n "$CHECK_ONLY" ] ; then
printf "%s\t%s\t%s\n" "kcl" "$kcl_version" "expected $KCL_VERSION" printf "%s\t%s\t%s\n" "nickel" "$nickel_version" "expected $NICKEL_VERSION"
else else
printf "%s\t%s\n" "kcl" "already $KCL_VERSION" printf "%s\t%s\n" "nickel" "already $NICKEL_VERSION"
fi fi
fi fi
#if [ -n "$TERA_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "tera" ] ; then #if [ -n "$TERA_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "tera" ] ; then
# has_tera=$(type -P tera) # has_tera=$(type -P tera)
# num_version="0" # num_version="0"
# [ -n "$has_tera" ] && tera_version=$(tera -V | cut -f2 -d" " | sed 's/teracli//g') && num_version=${tera_version//\./} # [ -n "$has_tera" ] && tera_version=$(tera -V | cut -f2 -d" " | sed 's/teracli//g') && num_version=${tera_version//\./}
# expected_version_num=${TERA_VERSION//\./} # expected_version_num=${TERA_VERSION//\./}
# [ -z "$num_version" ] && num_version=0 # [ -z "$num_version" ] && num_version=0
# if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then # if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
# if [ -x "$(dirname "$0")/../tools/tera_${OS}_${ARCH}" ] ; then # if [ -x "$(dirname "$0")/../tools/tera_${OS}_${ARCH}" ] ; then
# sudo cp "$(dirname "$0")/../tools/tera_${OS}_${ARCH}" /usr/local/bin/tera && printf "%s\t%s\n" "tera" "installed $TERA_VERSION" # sudo cp "$(dirname "$0")/../tools/tera_${OS}_${ARCH}" /usr/local/bin/tera && printf "%s\t%s\n" "tera" "installed $TERA_VERSION"
# else # else
# echo "Error: $(dirname "$0")/../tools/tera_${OS}_${ARCH} not found !!" # echo "Error: $(dirname "$0")/../tools/tera_${OS}_${ARCH} not found !!"
# exit 2 # exit 2
# fi # fi
# elif [ -n "$CHECK_ONLY" ] ; then # elif [ -n "$CHECK_ONLY" ] ; then
# printf "%s\t%s\t%s\n" "tera" "$tera_version" "expected $TERA_VERSION" # printf "%s\t%s\t%s\n" "tera" "$tera_version" "expected $TERA_VERSION"
# else # else
@ -191,9 +235,9 @@ function _install_tools {
[ -n "$has_k9s" ] && k9s_version="$( k9s version | grep Version | cut -f2 -d"v" | sed 's/ //g')" && num_version=${k9s_version//\./} [ -n "$has_k9s" ] && k9s_version="$( k9s version | grep Version | cut -f2 -d"v" | sed 's/ //g')" && num_version=${k9s_version//\./}
expected_version_num=${K9S_VERSION//\./} expected_version_num=${K9S_VERSION//\./}
[ -z "$num_version" ] && num_version=0 [ -z "$num_version" ] && num_version=0
if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
mkdir -p k9s && cd k9s && mkdir -p k9s && cd k9s &&
curl -fsSLO https://github.com/derailed/k9s/releases/download/v${K9S_VERSION}/k9s_${ORG_OS}_${ARCH}.tar.gz && curl -fsSLO https://github.com/derailed/k9s/releases/download/v${K9S_VERSION}/k9s_${ORG_OS}_${ARCH}.tar.gz &&
tar -xzf "k9s_${ORG_OS}_${ARCH}.tar.gz" && tar -xzf "k9s_${ORG_OS}_${ARCH}.tar.gz" &&
sudo mv k9s /usr/local/bin && sudo mv k9s /usr/local/bin &&
cd "$ORG" && rm -rf /tmp/k9s "/k9s_${ORG_OS}_${ARCH}.tar.gz" && cd "$ORG" && rm -rf /tmp/k9s "/k9s_${ORG_OS}_${ARCH}.tar.gz" &&
@ -209,12 +253,12 @@ function _install_tools {
num_version="0" num_version="0"
[ -n "$has_age" ] && age_version="${AGE_VERSION}" && num_version=${age_version//\./} [ -n "$has_age" ] && age_version="${AGE_VERSION}" && num_version=${age_version//\./}
expected_version_num=${AGE_VERSION//\./} expected_version_num=${AGE_VERSION//\./}
if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
curl -fsSLO https://github.com/FiloSottile/age/releases/download/v${AGE_VERSION}/age-v${AGE_VERSION}-${OS}-${ARCH}.tar.gz && curl -fsSLO https://github.com/FiloSottile/age/releases/download/v${AGE_VERSION}/age-v${AGE_VERSION}-${OS}-${ARCH}.tar.gz &&
tar -xzf age-v${AGE_VERSION}-${OS}-${ARCH}.tar.gz && tar -xzf age-v${AGE_VERSION}-${OS}-${ARCH}.tar.gz &&
sudo mv age/age /usr/local/bin && sudo mv age/age /usr/local/bin &&
sudo mv age/age-keygen /usr/local/bin && sudo mv age/age-keygen /usr/local/bin &&
rm -rf age "age-v${AGE_VERSION}-${OS}-${ARCH}.tar.gz" && rm -rf age "age-v${AGE_VERSION}-${OS}-${ARCH}.tar.gz" &&
printf "%s\t%s\n" "age" "installed $AGE_VERSION" printf "%s\t%s\n" "age" "installed $AGE_VERSION"
elif [ -n "$CHECK_ONLY" ] ; then elif [ -n "$CHECK_ONLY" ] ; then
printf "%s\t%s\t%s\n" "age" "$age_version" "expected $AGE_VERSION" printf "%s\t%s\t%s\n" "age" "$age_version" "expected $AGE_VERSION"
@ -228,9 +272,9 @@ function _install_tools {
[ -n "$has_sops" ] && sops_version="$(sops -v | grep ^sops | cut -f2 -d" " | sed 's/ //g')" && num_version=${sops_version//\./} [ -n "$has_sops" ] && sops_version="$(sops -v | grep ^sops | cut -f2 -d" " | sed 's/ //g')" && num_version=${sops_version//\./}
expected_version_num=${SOPS_VERSION//\./} expected_version_num=${SOPS_VERSION//\./}
[ -z "$num_version" ] && num_version=0 [ -z "$num_version" ] && num_version=0
if [ -z "$expected_version_num" ] ; then if [ -z "$expected_version_num" ] ; then
printf "%s\t%s\t%s\n" "sops" "$sops_version" "expected $SOPS_VERSION" printf "%s\t%s\t%s\n" "sops" "$sops_version" "expected $SOPS_VERSION"
elif [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then elif [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
mkdir -p sops && cd sops && mkdir -p sops && cd sops &&
curl -fsSLO https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.${OS}.${ARCH} && curl -fsSLO https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.${OS}.${ARCH} &&
mv sops-v${SOPS_VERSION}.${OS}.${ARCH} sops && mv sops-v${SOPS_VERSION}.${OS}.${ARCH} sops &&
@ -263,8 +307,8 @@ function _detect_tool_version {
nu | nushell) nu | nushell)
nu -v 2>/dev/null | head -1 || echo "" nu -v 2>/dev/null | head -1 || echo ""
;; ;;
kcl) nickel)
kcl -v 2>/dev/null | grep "kcl version" | sed 's/.*version\s*//' || echo "" nickel --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo ""
;; ;;
sops) sops)
sops -v 2>/dev/null | head -1 | awk '{print $2}' || echo "" sops -v 2>/dev/null | head -1 | awk '{print $2}' || echo ""
@ -325,22 +369,22 @@ function _try_install_provider_tool {
local options=$2 local options=$2
local force_update=$3 local force_update=$3
# Look for the tool in provider kcl/version.k files (KCL is single source of truth) # Look for the tool in provider nickel/version.ncl files (Nickel is single source of truth)
for prov in $(ls $PROVIDERS_PATH 2>/dev/null | grep -v "^_" ) for prov in $(ls $PROVIDERS_PATH 2>/dev/null | grep -v "^_" )
do do
if [ -r "$PROVIDERS_PATH/$prov/kcl/version.k" ] ; then if [ -r "$PROVIDERS_PATH/$prov/nickel/version.ncl" ] ; then
# Compile KCL file to JSON and extract version data (single source of truth) # Evaluate Nickel file to JSON and extract version data (single source of truth)
local kcl_file="$PROVIDERS_PATH/$prov/kcl/version.k" local nickel_file="$PROVIDERS_PATH/$prov/nickel/version.ncl"
local kcl_output="" local nickel_output=""
local tool_version="" local tool_version=""
local tool_name="" local tool_name=""
# Compile KCL to JSON and capture output # Evaluate Nickel to JSON and capture output
kcl_output=$(kcl run "$kcl_file" --format json 2>/dev/null) nickel_output=$(nickel export --format json "$nickel_file" 2>/dev/null)
# Extract tool name and version from JSON # Extract tool name and version from JSON
tool_name=$(echo "$kcl_output" | grep -o '"name": "[^"]*"' | head -1 | sed 's/"name": "//;s/"$//') tool_name=$(echo "$nickel_output" | grep -o '"name": "[^"]*"' | head -1 | sed 's/"name": "//;s/"$//')
tool_version=$(echo "$kcl_output" | grep -o '"current": "[^"]*"' | head -1 | sed 's/"current": "//;s/"$//') tool_version=$(echo "$nickel_output" | grep -o '"current": "[^"]*"' | head -1 | sed 's/"current": "//;s/"$//')
# If this is the tool we're looking for # If this is the tool we're looking for
if [ "$tool_name" == "$tool" ] && [ -n "$tool_version" ] ; then if [ "$tool_name" == "$tool" ] && [ -n "$tool_version" ] ; then
@ -357,7 +401,7 @@ function _try_install_provider_tool {
export UPCLOUD_UPCTL_VERSION="$tool_version" export UPCLOUD_UPCTL_VERSION="$tool_version"
$PROVIDERS_PATH/$prov/bin/install.sh "$tool_name" $options $PROVIDERS_PATH/$prov/bin/install.sh "$tool_name" $options
elif [ "$prov" = "hetzner" ] ; then elif [ "$prov" = "hetzner" ] ; then
# Hetzner expects: version as param (from kcl/version.k) # Hetzner expects: version as param (from nickel/version.ncl)
$PROVIDERS_PATH/$prov/bin/install.sh "$tool_version" $options $PROVIDERS_PATH/$prov/bin/install.sh "$tool_version" $options
elif [ "$prov" = "aws" ] ; then elif [ "$prov" = "aws" ] ; then
# AWS format - set env var and pass tool name # AWS format - set env var and pass tool name
@ -410,14 +454,14 @@ function _on_tools {
_install_tools "$tool" "$@" _install_tools "$tool" "$@"
done done
esac esac
} }
set -o allexport set -o allexport
## shellcheck disable=SC1090 ## shellcheck disable=SC1090
[ -n "$PROVISIONING_ENV" ] && [ -r "$PROVISIONING_ENV" ] && source "$PROVISIONING_ENV" [ -n "$PROVISIONING_ENV" ] && [ -r "$PROVISIONING_ENV" ] && source "$PROVISIONING_ENV"
[ -r "../env-provisioning" ] && source ../env-provisioning [ -r "../env-provisioning" ] && source ../env-provisioning
[ -r "env-provisioning" ] && source ./env-provisioning [ -r "env-provisioning" ] && source ./env-provisioning
#[ -r ".env" ] && source .env set #[ -r ".env" ] && source .env set
set +o allexport set +o allexport
export PROVISIONING=${PROVISIONING:-/usr/local/provisioning} export PROVISIONING=${PROVISIONING:-/usr/local/provisioning}
@ -434,7 +478,7 @@ PROVIDERS_PATH=${PROVIDERS_PATH:-"$PROVISIONING/extensions/providers"}
if [ -z "$1" ] ; then if [ -z "$1" ] ; then
CHECK_ONLY="yes" CHECK_ONLY="yes"
_on_tools all _on_tools all
else else
[ "$1" == "-h" ] && echo "$USAGE" && shift [ "$1" == "-h" ] && echo "$USAGE" && shift
[ "$1" == "check" ] && CHECK_ONLY="yes" && shift [ "$1" == "check" ] && CHECK_ONLY="yes" && shift
[ -n "$1" ] && cd /tmp && _on_tools "$@" [ -n "$1" ] && cd /tmp && _on_tools "$@"

28
cli/tty-commands.conf Normal file
View file

@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Minimalist TTY Command Registry (Nu-based)
# Format: "COMMAND_PATTERN" "DISPATCHER_CALL" "FLOW_TYPE"
# All commands routed through tty-dispatch.sh → Nu functions
# Flow types: "exit" (standalone), "pipe" (inter-command), "continue" (to Nushell)
# ═══════════════════════════════════════════════════════════════════════════════
# Authentication & Setup Commands
# ═══════════════════════════════════════════════════════════════════════════════
# Standalone wizards (flow=exit)
"setup wizard" "core/cli/tty-dispatch.sh setup-wizard exit" "exit"
"auth login" "core/cli/tty-dispatch.sh login exit" "exit"
"auth mfa enroll" "core/cli/tty-dispatch.sh mfa-enroll exit" "exit"
# Pipeline commands (flow=pipe) - output to stdout
"auth get-key" "core/cli/tty-dispatch.sh get-key pipe" "pipe"
# Continue to Nushell (flow=continue) - output captured in $TTY_OUTPUT
"auth integrate" "core/cli/tty-dispatch.sh credential-input continue" "continue"
"secret configure" "core/cli/tty-dispatch.sh secret-configure continue" "continue"
# ═══════════════════════════════════════════════════════════════════════════════
# Future-proofing: Add new commands without modifying tty-filter.sh
# Example:
# "wizard something" "core/cli/tty-dispatch.sh something exit" "exit"
# "get something" "core/cli/tty-dispatch.sh something pipe" "pipe"
# ═══════════════════════════════════════════════════════════════════════════════

86
cli/tty-dispatch.sh Executable file
View file

@ -0,0 +1,86 @@
#!/usr/bin/env bash
# Universal TTY Command Dispatcher
# Routes TTY commands to Nu functions with proper output handling
# Usage: tty-dispatch.sh <function-name> [flow-type] [args...]
set -euo pipefail
FUNCTION_NAME="${1:-}"
FLOW_TYPE="${2:-exit}"
shift 2 || true
if [[ -z "$FUNCTION_NAME" ]]; then
echo "Error: Function name required" >&2
exit 1
fi
# Find nu binary
NU=$(type -P nu 2>/dev/null || echo "")
if [[ -z "$NU" ]]; then
echo "Error: nu not found in PATH" >&2
exit 1
fi
# Get provisioning root
PROVISIONING="${PROVISIONING:-/usr/local/provisioning}"
# Map function name to Nu function with proper naming conventions
case "$FUNCTION_NAME" in
"setup-wizard")
NU_FUNCTION="run-setup-wizard-interactive"
;;
"login"|"auth-login")
NU_FUNCTION="login-interactive"
;;
"mfa"|"mfa-enroll"|"auth-mfa-enroll")
NU_FUNCTION="mfa-enroll-interactive"
;;
"auth-get-key"|"get-key")
NU_FUNCTION="get-api-key-interactive"
;;
"auth-integrate"|"credential-input")
NU_FUNCTION="get-provider-credentials-interactive"
;;
"secret-configure")
NU_FUNCTION="get-secret-config-interactive"
;;
*)
echo "Error: Unknown function: $FUNCTION_NAME" >&2
exit 1
;;
esac
# Execute Nu function with proper output handling
case "$FLOW_TYPE" in
"exit")
# Standalone: Execute and exit immediately
$NU -n -c "
use '$PROVISIONING/core/nulib/lib_provisioning/plugins/auth.nu' *
use '$PROVISIONING/core/nulib/lib_provisioning/setup/wizard.nu' *
$NU_FUNCTION
"
exit $?
;;
"pipe")
# Pipeline: Output to stdout for piping
$NU -n -c "
use '$PROVISIONING/core/nulib/lib_provisioning/plugins/auth.nu' *
use '$PROVISIONING/core/nulib/lib_provisioning/setup/wizard.nu' *
$NU_FUNCTION
"
exit $?
;;
"continue")
# Continue to Nushell: Output as JSON for $TTY_OUTPUT
$NU -n -c "
use '$PROVISIONING/core/nulib/lib_provisioning/plugins/auth.nu' *
use '$PROVISIONING/core/nulib/lib_provisioning/setup/wizard.nu' *
$NU_FUNCTION | to json
"
exit $?
;;
*)
echo "Error: Unknown flow type: $FLOW_TYPE" >&2
exit 1
;;
esac

137
cli/tty-filter.sh Executable file
View file

@ -0,0 +1,137 @@
#!/bin/bash
# Description: Flow-Aware TTY Command Filter
# Manages three execution flows: exit (standalone), pipe (inter-command), continue (Nushell)
# Arguments: $@ - Command and arguments
# Returns: 0 if TTY command handled with flow=continue (continue to Nushell)
# Exits with wrapper code for flow=exit or flow=pipe
# 1 if not a TTY command (continue to normal processing)
# Output: Exports TTY_OUTPUT and PROVISIONING_BYPASS_DAEMON on flow=continue
# Only apply strict mode when run standalone — sourcing this file must not
# contaminate the calling shell's options (set -e would cause `DAEMON_OUTPUT=$(curl ...)`
# to exit the parent script with curl's non-zero exit code instead of falling through).
[[ "${BASH_SOURCE[0]}" == "${0}" ]] && set -euo pipefail
# Description: Check if command matches TTY pattern and manage flow
# Arguments: $@ - Full command line
# Returns: 0 for flow=continue (don't exit), non-zero for error/not-matched
# Exits for flow=exit or flow=pipe (calls exit)
# Output: Executes wrapper or exports environment
filter_tty_command() {
local cmd="$*"
local registry_file="${PROVISIONING:-}/core/cli/tty-commands.conf"
# Validate registry exists
if [[ ! -f "$registry_file" ]]; then
return 1
fi
# Read registry using separate file descriptor to preserve stdin
while IFS= read -r line <&3 || [[ -n "$line" ]]; do
# Skip comments and separators
[[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ "$line" =~ ^[[:space:]]*═ ]] && continue
[[ -z "$line" ]] && continue
# Parse three-field format: "PATTERN" "WRAPPER" "FLOW_TYPE"
if [[ "$line" =~ ^\"([^\"]+)\"[[:space:]]+\"([^\"]+)\"[[:space:]]+\"([^\"]+)\" ]]; then
local pattern="${BASH_REMATCH[1]}"
local wrapper="${BASH_REMATCH[2]}"
local flow_type="${BASH_REMATCH[3]}"
# Check if command starts with pattern (prefix match)
# This allows commands with additional arguments like "auth integrate --provider azure"
if [[ "$cmd" == "$pattern"* ]]; then
local wrapper_path="${PROVISIONING}/${wrapper}"
# Validate wrapper exists and is executable
if [[ ! -x "$wrapper_path" ]]; then
echo "Warning: TTY wrapper not found or not executable: $wrapper_path" >&2
return 1
fi
# Extract arguments after pattern
# Pattern may be multi-word (e.g., "setup platform")
# Count pattern words and skip them from arguments
local pattern_words=($pattern)
local pattern_count=${#pattern_words[@]}
local wrapper_args=()
# Shift arguments to skip pattern words
for ((i=pattern_count; i<$#; i++)); do
wrapper_args+=("${@:i+1:1}")
done
# ═══════════════════════════════════════════════════════════
# FLOW TYPE: exit (standalone TTY)
# Execute wrapper and exit immediately
# Never reaches Nushell dispatcher
# ═══════════════════════════════════════════════════════════
if [[ "$flow_type" == "exit" ]]; then
if [[ ${#wrapper_args[@]} -gt 0 ]]; then
bash "$wrapper_path" "${wrapper_args[@]}"
else
bash "$wrapper_path"
fi
exit $?
# ═══════════════════════════════════════════════════════════
# FLOW TYPE: pipe (inter-command piping)
# Execute wrapper, output to stdout, exit
# Allows piping to next command in pipeline
# ═══════════════════════════════════════════════════════════
elif [[ "$flow_type" == "pipe" ]]; then
if [[ ${#wrapper_args[@]} -gt 0 ]]; then
bash "$wrapper_path" "${wrapper_args[@]}"
else
bash "$wrapper_path"
fi
exit $?
# ═══════════════════════════════════════════════════════════
# FLOW TYPE: continue (same-command Nushell processing)
# Execute wrapper, capture output, continue to Nushell
# Nushell receives $env.TTY_OUTPUT and original args
# ═══════════════════════════════════════════════════════════
elif [[ "$flow_type" == "continue" ]]; then
# Execute wrapper and capture output
local tty_output
if [[ ${#wrapper_args[@]} -gt 0 ]]; then
tty_output=$(bash "$wrapper_path" "${wrapper_args[@]}" 2>&1) || {
local exit_code=$?
echo "Error: TTY wrapper failed with code $exit_code" >&2
echo "$tty_output" >&2
exit $exit_code
}
else
tty_output=$(bash "$wrapper_path" 2>&1) || {
local exit_code=$?
echo "Error: TTY wrapper failed with code $exit_code" >&2
echo "$tty_output" >&2
exit $exit_code
}
fi
# Export output for Nushell scripts to access
export TTY_OUTPUT="$tty_output"
export PROVISIONING_BYPASS_DAEMON="true"
export TTY_WRAPPER_EXECUTED="true"
# Return 0 WITHOUT exiting - allows continuation to Nushell
return 0
else
echo "Warning: Unknown flow type '$flow_type' for pattern '$pattern'" >&2
return 1
fi
fi
fi
done 3< "$registry_file"
return 1
}
# Only run filter if called directly (not sourced)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
filter_tty_command "$@"
fi

View file

@ -1,239 +0,0 @@
# FormInquire Integration System
Dynamic form generation using Jinja2 templates rendered with `nu_plugin_tera`.
## Architecture
```
provisioning/core/forminquire/
├── templates/ # Jinja2 form templates (.j2)
│ ├── setup-wizard.form.j2
│ ├── workspace-init.form.j2
│ ├── settings-update.form.j2
│ ├── server-delete-confirm.form.j2
│ └── ...more templates
├── nulib/
│ └── forminquire.nu # Nushell integration functions
└── wrappers/
└── form.sh # Bash wrapper for FormInquire
```
## How It Works
1. **Template Rendering**: Jinja2 templates are rendered with data from config files
2. **Form Generation**: Rendered templates are saved as TOML forms in cache
3. **User Interaction**: FormInquire binary presents the form to user
4. **Result Processing**: JSON output from FormInquire is returned to calling code
```
Config Data → Template Rendering → Form Generation → FormInquire → JSON Output
(nu_plugin_tera) (cache: ~/.cache/) (interactive)
```
## Quick Examples
### Settings Update with Current Values as Defaults
```nushell
use provisioning/core/forminquire/nulib/forminquire.nu *
# Load current settings and show form with them as defaults
let result = (settings-update-form)
if $result.success {
# Process updated settings
print $"Updated: ($result.values | to json)"
}
```
### Setup Wizard
```nushell
let result = (setup-wizard-form)
if $result.success {
print "Setup configuration:"
print ($result.values | to json)
}
```
### Workspace Initialization
```nushell
let result = (workspace-init-form "my-workspace")
if $result.success {
print "Workspace created with settings:"
print ($result.values | to json)
}
```
### Server Delete Confirmation
```nushell
let confirm = (server-delete-confirm-form "web-01" "192.168.1.10" "running")
if $confirm.success {
let confirmation_text = $confirm.values.confirmation_text
let final_confirm = $confirm.values.final_confirm
if ($confirmation_text == "web-01" and $final_confirm) {
print "Deleting server..."
}
}
```
## Template Variables
All templates have access to:
### Automatic Variables (always available)
- `now_iso`: Current timestamp in ISO 8601 format
- `home_dir`: User's home directory
- `username`: Current username
- `provisioning_root`: Provisioning root directory
### Custom Variables (passed per form)
- Settings from `config.defaults.toml`
- User preferences from `~/.config/provisioning/user_config.yaml`
- Workspace configuration from workspace `config.toml`
- Any custom data passed to the form function
## Cache Management
Forms are cached at: `~/.cache/provisioning/forms/`
### Cleanup Old Forms
```nushell
let cleanup_result = (cleanup-form-cache)
print $"Cleaned up ($cleanup_result.cleaned) old form files"
```
### List Generated Forms
```nushell
list-cached-forms
```
## Template Syntax
Templates use Jinja2 syntax with macros for common form elements:
```jinja2
[items.my_field]
type = "text"
prompt = "Enter value"
default = "{{ my_variable }}"
help = "Help text here"
required = true
```
### Available Form Types
- `text`: Text input
- `select`: Dropdown selection
- `confirm`: Yes/No confirmation
- `password`: Masked password input
- `multiselect`: Multiple selection
## Available Functions
### Form Execution
- `interactive-form [name] [template] [data]` - Complete form flow
- `render-template [template_name] [data]` - Render template only
- `generate-form [form_name] [template_name] [data]` - Generate TOML form
- `run-form [form_path]` - Execute FormInquire with form
### Config Loading
- `load-user-preferences` - Load user preferences from config
- `load-workspace-config [workspace_name]` - Load workspace settings
- `load-system-defaults` - Load system defaults
- `get-form-context [workspace_name] [custom_data]` - Merged config context
### Convenience Functions
- `settings-update-form` - Update system settings
- `setup-wizard-form` - Run setup wizard
- `workspace-init-form [name]` - Initialize workspace
- `server-delete-confirm-form [name] [ip] [status]` - Delete confirmation
### Utilities
- `list-templates` - List available templates
- `list-cached-forms` - List generated forms in cache
- `cleanup-form-cache` - Remove old cached forms
## Shell Integration
Use the bash wrapper for shell scripts:
```bash
#!/bin/bash
# Generate form with Nushell
nu -c "use forminquire *; interactive-form 'my-form' 'my-template' {foo: 'bar'}" > /tmp/form.toml
# Or use form.sh wrapper directly
./provisioning/core/forminquire/wrappers/form.sh /path/to/form.toml json
```
## Performance Notes
- **First form**: ~200ms (template rendering + form generation)
- **Subsequent forms**: ~50ms (cached config loading)
- **User interaction**: Depends on FormInquire response time
- **Form cache**: Automatically cleaned after 1+ days
## Dependencies
- `forminquire` - FormInquire binary (in PATH)
- `nu_plugin_tera` - Nushell Jinja2 template plugin
- `Nushell 0.109.0+` - Core scripting language
## Error Handling
All functions return structured results:
```nushell
{
success: bool # Operation succeeded
error: string # Error message (empty if success)
form_path: string # Generated form path (if applicable)
values: record # FormInquire output values
}
```
## Adding New Forms
1. Create template in `templates/` with `.form.j2` extension
2. Create convenience function in `forminquire.nu` like `my-form-function`
3. Use in scripts: `my-form-function [args...]`
Example:
```jinja2
# templates/my-form.form.j2
[meta]
title = "My Custom Form"
[items.field1]
type = "text"
prompt = "Enter value"
default = "{{ default_value }}"
```
```nushell
# In forminquire.nu
export def my-form-function [default_value: string = ""] {
interactive-form "my-form" "my-form" {default_value: $default_value}
}
```
## Limitations
- Template rendering uses Jinja2 syntax only
- FormInquire must be in PATH
- `nu_plugin_tera` must be installed for template rendering
- Form output limited to FormInquire-supported types

View file

@ -1,540 +0,0 @@
#!/usr/bin/env nu
# [command]
# name = "forminquire integration"
# group = "infrastructure"
# tags = ["forminquire", "forms", "interactive", "templates"]
# version = "1.0.0"
# requires = ["nu_plugin_tera", "forminquire:1.0.0"]
# note = "Dynamic form generation using Jinja2 templates rendered with nu_plugin_tera"
# ============================================================================
# FormInquire Integration System
# Version: 1.0.0
# Purpose: Generate interactive forms dynamically from templates and config data
# ============================================================================
# Get form cache directory
def get-form-cache-dir [] : nothing -> string {
let cache_dir = (
if ($env.XDG_CACHE_HOME? | is-empty) {
$"($env.HOME)/.cache/provisioning/forms"
} else {
$"($env.XDG_CACHE_HOME)/provisioning/forms"
}
)
$cache_dir
}
# Ensure cache directory exists
def ensure-form-cache-dir [] : nothing -> string {
let cache_dir = (get-form-cache-dir)
let _mkdir_result = (do {
if not (($cache_dir | path exists)) {
^mkdir -p $cache_dir
}
} | complete)
$cache_dir
}
# Get template directory
def get-template-dir [] : nothing -> string {
let proj_root = (
if (($env.PROVISIONING_ROOT? | is-empty)) {
$"($env.HOME)/project-provisioning"
} else {
$env.PROVISIONING_ROOT
}
)
$"($proj_root)/provisioning/core/forminquire/templates"
}
# Load TOML configuration file
def load-toml-config [path: string] : nothing -> record {
let result = (do { open $path | from toml } | complete)
if ($result.exit_code == 0) {
$result.stdout
} else {
{}
}
}
# Load YAML configuration file
def load-yaml-config [path: string] : nothing -> record {
let result = (do { open $path | from yaml } | complete)
if ($result.exit_code == 0) {
$result.stdout
} else {
{}
}
}
# Render Jinja2 template with data
export def render-template [
template_name: string
data: record = {}
] : nothing -> record {
let template_dir = (get-template-dir)
let template_path = $"($template_dir)/($template_name).j2"
if not (($template_path | path exists)) {
return {
error: $"Template not found: ($template_path)"
content: ""
}
}
let template_content_result = (do { ^cat $template_path } | complete)
if ($template_content_result.exit_code != 0) {
return {
error: "Failed to read template file"
content: ""
}
}
let template_content = $template_content_result.stdout
let enriched_data = (
$data
| merge {
now_iso: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
home_dir: $env.HOME
username: (whoami)
provisioning_root: (
if (($env.PROVISIONING_ROOT? | is-empty)) {
$"($env.HOME)/project-provisioning"
} else {
$env.PROVISIONING_ROOT
}
)
}
)
let render_result = (do {
tera -t $template_content --data ($enriched_data | to json)
} | complete)
if ($render_result.exit_code == 0) {
{
error: ""
content: $render_result.stdout
}
} else {
{
error: "Template rendering failed"
content: ""
}
}
}
# Generate form from template and save to cache
export def generate-form [
form_name: string
template_name: string
data: record = {}
] : nothing -> record {
let cache_dir = (ensure-form-cache-dir)
let form_path = $"($cache_dir)/($form_name).toml"
let render_result = (render-template $template_name $data)
if not (($render_result.error | is-empty)) {
return {
success: false
error: $render_result.error
form_path: ""
}
}
let write_result = (do {
$render_result.content | ^tee $form_path > /dev/null
} | complete)
if ($write_result.exit_code == 0) {
{
success: true
error: ""
form_path: $form_path
}
} else {
{
success: false
error: "Failed to write form file"
form_path: ""
}
}
}
# Execute FormInquire with generated form
export def run-form [form_path: string] : nothing -> record {
if not (($form_path | path exists)) {
return {
success: false
error: $"Form file not found: ($form_path)"
values: {}
}
}
let forminquire_result = (do {
^forminquire --from-file $form_path --output json
} | complete)
if ($forminquire_result.exit_code != 0) {
return {
success: false
error: "FormInquire execution failed"
values: {}
}
}
let parse_result = (do {
$forminquire_result.stdout | from json
} | complete)
if ($parse_result.exit_code == 0) {
{
success: true
error: ""
values: $parse_result.stdout
}
} else {
{
success: false
error: "Failed to parse FormInquire output"
values: {}
}
}
}
# Complete flow: generate form from template and run it
export def interactive-form [
form_name: string
template_name: string
data: record = {}
] : nothing -> record {
let generate_result = (generate-form $form_name $template_name $data)
if not $generate_result.success {
return {
success: false
error: $generate_result.error
form_path: ""
values: {}
}
}
let run_result = (run-form $generate_result.form_path)
{
success: $run_result.success
error: $run_result.error
form_path: $generate_result.form_path
values: $run_result.values
}
}
# Load user preferences from config
export def load-user-preferences [] : nothing -> record {
let config_path = $"($env.HOME)/.config/provisioning/user_config.yaml"
load-yaml-config $config_path
}
# Load workspace config
export def load-workspace-config [workspace_name: string] : nothing -> record {
let workspace_dir = (
if (($env.PROVISIONING_WORKSPACE? | is-empty)) {
$"($env.HOME)/workspaces/($workspace_name)"
} else {
$env.PROVISIONING_WORKSPACE
}
)
let config_file = $"($workspace_dir)/config.toml"
load-toml-config $config_file
}
# Load system defaults
export def load-system-defaults [] : nothing -> record {
let proj_root = (
if (($env.PROVISIONING_ROOT? | is-empty)) {
$"($env.HOME)/project-provisioning"
} else {
$env.PROVISIONING_ROOT
}
)
let defaults_file = $"($proj_root)/provisioning/config/config.defaults.toml"
load-toml-config $defaults_file
}
# Merge multiple config sources with priority
export def merge-config-sources [
defaults: record = {}
workspace: record = {}
user: record = {}
overrides: record = {}
] : nothing -> record {
$defaults | merge $workspace | merge $user | merge $overrides
}
# Get form context with all available data
export def get-form-context [
workspace_name: string = ""
custom_data: record = {}
] : nothing -> record {
let defaults = (load-system-defaults)
let user_prefs = (load-user-preferences)
let workspace_config = (
if (($workspace_name | is-empty)) {
{}
} else {
load-workspace-config $workspace_name
}
)
let merged = (merge-config-sources $defaults $workspace_config $user_prefs $custom_data)
$merged
}
# Settings update form - loads current settings as defaults
export def settings-update-form [] : nothing -> record {
let context = (get-form-context)
let data = {
config_source: "system defaults + user preferences"
editor: ($context.preferences.editor? // "vim")
output_format: ($context.preferences.output_format? // "yaml")
default_log_level: ($context.preferences.default_log_level? // "info")
preferred_provider: ($context.preferences.preferred_provider? // "upcloud")
confirm_delete: ($context.preferences.confirm_delete? // true)
confirm_deploy: ($context.preferences.confirm_deploy? // true)
}
interactive-form "settings-update" "settings-update" $data
}
# Setup wizard form
export def setup-wizard-form [] : nothing -> record {
let context = (get-form-context)
let data = {
system_name: ($context.system_name? // "provisioning")
admin_email: ($context.admin_email? // "")
deployment_mode: ($context.deployment_mode? // "solo")
infrastructure_provider: ($context.infrastructure_provider? // "upcloud")
cpu_cores: ($context.resources.cpu_cores? // "4")
memory_gb: ($context.resources.memory_gb? // "8")
disk_gb: ($context.resources.disk_gb? // "50")
workspace_path: ($context.workspace_path? // $"($env.HOME)/provisioning-workspace")
}
interactive-form "setup-wizard" "setup-wizard" $data
}
# Workspace init form
export def workspace-init-form [workspace_name: string = ""] : nothing -> record {
let context = (get-form-context $workspace_name)
let data = {
workspace_name: (
if (($workspace_name | is-empty)) {
"default"
} else {
$workspace_name
}
)
workspace_description: ($context.description? // "")
workspace_path: ($context.path? // $"($env.HOME)/workspaces/($workspace_name)")
default_provider: ($context.default_provider? // "upcloud")
default_region: ($context.default_region? // "")
init_git: ($context.init_git? // true)
create_example_configs: ($context.create_example_configs? // true)
setup_secrets: ($context.setup_secrets? // true)
enable_testing: ($context.enable_testing? // true)
enable_monitoring: ($context.enable_monitoring? // false)
enable_orchestrator: ($context.enable_orchestrator? // true)
}
interactive-form "workspace-init" "workspace-init" $data
}
# Server delete confirmation form
export def server-delete-confirm-form [
server_name: string
server_ip: string = ""
server_status: string = ""
] : nothing -> record {
let data = {
server_name: $server_name
server_ip: $server_ip
server_status: $server_status
}
interactive-form "server-delete-confirm" "server-delete-confirm" $data
}
# Clean up old form files from cache (older than 1 day)
export def cleanup-form-cache [] : nothing -> record {
let cache_dir = (get-form-cache-dir)
if not (($cache_dir | path exists)) {
return {cleaned: 0, error: ""}
}
let find_result = (do {
^find $cache_dir -name "*.toml" -type f -mtime +1 -delete
} | complete)
{cleaned: 0, error: ""}
}
# List available templates
export def list-templates [] : nothing -> list {
let template_dir = (get-template-dir)
if not (($template_dir | path exists)) {
return []
}
let find_result = (do {
^find $template_dir -name "*.j2" -type f
} | complete)
if ($find_result.exit_code == 0) {
$find_result.stdout
| lines
| each {|path|
let name = ($path | path basename | str replace ".j2" "")
{
name: $name
path: $path
template_file: ($path | path basename)
}
}
} else {
[]
}
}
# List generated forms in cache
export def list-cached-forms [] : nothing -> list {
let cache_dir = (ensure-form-cache-dir)
let find_result = (do {
^find $cache_dir -name "*.toml" -type f
} | complete)
if ($find_result.exit_code == 0) {
$find_result.stdout
| lines
| each {|path|
{
name: ($path | path basename)
path: $path
}
}
} else {
[]
}
}
# ============================================================================
# DELETE CONFIRMATION HELPERS
# ============================================================================
# Run server delete confirmation
export def server-delete-confirm [
server_name: string
server_ip?: string
server_status?: string
] : nothing -> record {
let context = {
server_name: $server_name
server_ip: (if ($server_ip | is-empty) { "" } else { $server_ip })
server_status: (if ($server_status | is-empty) { "running" } else { $server_status })
}
run-forminquire-form "provisioning/core/shlib/forms/infrastructure/server_delete_confirm.toml" $context
}
# Run taskserv delete confirmation
export def taskserv-delete-confirm [
taskserv_name: string
taskserv_type?: string
taskserv_server?: string
taskserv_status?: string
dependent_services?: string
] : nothing -> record {
let context = {
taskserv_name: $taskserv_name
taskserv_type: (if ($taskserv_type | is-empty) { "" } else { $taskserv_type })
taskserv_server: (if ($taskserv_server | is-empty) { "" } else { $taskserv_server })
taskserv_status: (if ($taskserv_status | is-empty) { "unknown" } else { $taskserv_status })
dependent_services: (if ($dependent_services | is-empty) { "none" } else { $dependent_services })
}
run-forminquire-form "provisioning/core/shlib/forms/infrastructure/taskserv_delete_confirm.toml" $context
}
# Run cluster delete confirmation
export def cluster-delete-confirm [
cluster_name: string
cluster_type?: string
node_count?: string
total_resources?: string
deployments_count?: string
services_count?: string
volumes_count?: string
] : nothing -> record {
let context = {
cluster_name: $cluster_name
cluster_type: (if ($cluster_type | is-empty) { "" } else { $cluster_type })
node_count: (if ($node_count | is-empty) { "unknown" } else { $node_count })
total_resources: (if ($total_resources | is-empty) { "" } else { $total_resources })
deployments_count: (if ($deployments_count | is-empty) { "0" } else { $deployments_count })
services_count: (if ($services_count | is-empty) { "0" } else { $services_count })
volumes_count: (if ($volumes_count | is-empty) { "0" } else { $volumes_count })
}
run-forminquire-form "provisioning/core/shlib/forms/infrastructure/cluster_delete_confirm.toml" $context
}
# Generic delete confirmation
export def generic-delete-confirm [
resource_type: string
resource_name: string
resource_id?: string
resource_status?: string
] : nothing -> record {
let context = {
resource_type: $resource_type
resource_name: $resource_name
resource_id: (if ($resource_id | is-empty) { "" } else { $resource_id })
resource_status: (if ($resource_status | is-empty) { "unknown" } else { $resource_status })
}
run-forminquire-form "provisioning/core/shlib/forms/infrastructure/generic_delete_confirm.toml" $context
}
# Validate delete confirmation result
export def validate-delete-confirmation [result: record] : nothing -> bool {
# Must have success = true
let success = ($result.success // false)
if not $success {
return false
}
let values = ($result.values // {})
# Must have typed "DELETE" or "DELETE CLUSTER"
let confirm_text = ($values.confirmation_text // "")
let is_confirmed = (($confirm_text == "DELETE") or ($confirm_text == "DELETE CLUSTER"))
# Must have checked final confirmation checkbox
let final_checked = ($values.final_confirm // false)
# Must have checked proceed checkbox
let proceed_checked = ($values.proceed // false)
($is_confirmed and $final_checked and $proceed_checked)
}

View file

@ -1,50 +0,0 @@
# Auto-generated delete confirmation form
# Generated: {{ now_iso }}
# Server: {{ server_name }}
[meta]
title = "Confirm Server Deletion"
description = "WARNING: This operation cannot be reversed. Please confirm carefully."
allow_cancel = true
[items.server_display]
type = "text"
prompt = "Server to Delete"
default = "{{ server_name }}"
help = "Server name (read-only for confirmation)"
read_only = true
{% if server_ip %}
[items.server_ip]
type = "text"
prompt = "Server IP Address"
default = "{{ server_ip }}"
help = "IP address (read-only for confirmation)"
read_only = true
{% endif %}
{% if server_status %}
[items.server_status]
type = "text"
prompt = "Current Status"
default = "{{ server_status }}"
help = "Current server status (read-only)"
read_only = true
{% endif %}
[items.confirmation_text]
type = "text"
prompt = "Type server name to confirm deletion"
default = ""
help = "You must type the exact server name '{{ server_name }}' to proceed"
required = true
[items.final_confirm]
type = "confirm"
prompt = "I understand this action is irreversible. Delete server?"
help = "This will permanently delete the server and all its data"
[items.backup_before_delete]
type = "confirm"
prompt = "Create backup before deletion?"
help = "Optionally create a backup of the server configuration"

View file

@ -1,73 +0,0 @@
{%- macro form_input(name, label, value="", required=false, help="") -%}
[items."{{ name }}"]
type = "text"
prompt = "{{ label }}"
default = "{{ value }}"
{% if help %}help = "{{ help }}"
{% endif %}{% if required %}required = true
{% endif %}
{%- endmacro -%}
{%- macro form_select(name, label, options=[], value="", help="") -%}
[items."{{ name }}"]
type = "select"
prompt = "{{ label }}"
options = [{% for opt in options %}"{{ opt }}"{{ "," if not loop.last }}{% endfor %}]
default = "{{ value }}"
{% if help %}help = "{{ help }}"
{% endif %}
{%- endmacro -%}
{%- macro form_confirm(name, label, help="") -%}
[items."{{ name }}"]
type = "confirm"
prompt = "{{ label }}"
{% if help %}help = "{{ help }}"
{% endif %}
{%- endmacro -%}
# Auto-generated form for settings update
# Generated: {{ now_iso }}
# Config source: {{ config_source }}
[meta]
title = "Provisioning Settings Update"
description = "Update provisioning configuration settings"
allow_cancel = true
[items.editor]
type = "text"
prompt = "Preferred Editor"
default = "{{ editor | default('vim') }}"
help = "Editor to use for file editing (vim, nano, emacs)"
[items.output_format]
type = "select"
prompt = "Default Output Format"
options = ["json", "yaml", "text", "table"]
default = "{{ output_format | default('yaml') }}"
help = "Default output format for commands"
[items.confirm_delete]
type = "confirm"
prompt = "Confirm Destructive Operations?"
help = "Require confirmation before deleting resources"
[items.confirm_deploy]
type = "confirm"
prompt = "Confirm Deployments?"
help = "Require confirmation before deploying"
[items.default_log_level]
type = "select"
prompt = "Default Log Level"
options = ["debug", "info", "warning", "error"]
default = "{{ default_log_level | default('info') }}"
help = "Default logging level"
[items.preferred_provider]
type = "select"
prompt = "Preferred Cloud Provider"
options = ["upcloud", "aws", "local"]
default = "{{ preferred_provider | default('upcloud') }}"
help = "Preferred infrastructure provider"

View file

@ -1,180 +0,0 @@
# Auto-generated form for setup wizard
# Generated: {{ now_iso }}
# This is a comprehensive 7-step setup wizard
[meta]
title = "Provisioning System Setup Wizard"
description = "Step-by-step configuration for your infrastructure provisioning system"
allow_cancel = true
# ============================================================================
# STEP 1: SYSTEM CONFIGURATION
# ============================================================================
[items.step1_header]
type = "text"
prompt = "STEP 1/7: System Configuration"
display_only = true
[items.config_path]
type = "text"
prompt = "Configuration Base Path"
default = "{{ config_path | default('/etc/provisioning') }}"
help = "Where provisioning configuration will be stored"
required = true
[items.use_defaults_path]
type = "confirm"
prompt = "Use recommended paths for your OS?"
help = "Use OS-specific default paths (recommended)"
# ============================================================================
# STEP 2: DEPLOYMENT MODE
# ============================================================================
[items.step2_header]
type = "text"
prompt = "STEP 2/7: Deployment Mode Selection"
display_only = true
[items.deployment_mode]
type = "select"
prompt = "How should platform services be deployed?"
options = ["docker-compose", "kubernetes", "systemd", "remote-ssh"]
default = "{{ deployment_mode | default('docker-compose') }}"
help = "Choose based on your infrastructure type"
required = true
# ============================================================================
# STEP 3: PROVIDER SELECTION
# ============================================================================
[items.step3_header]
type = "text"
prompt = "STEP 3/7: Infrastructure Providers"
display_only = true
[items.provider_upcloud]
type = "confirm"
prompt = "Use UpCloud as provider?"
help = "UpCloud offers affordable cloud VMs in European regions"
[items.provider_aws]
type = "confirm"
prompt = "Use AWS as provider?"
help = "Amazon Web Services - global infrastructure"
[items.provider_hetzner]
type = "confirm"
prompt = "Use Hetzner as provider?"
help = "Hetzner - German cloud provider with good pricing"
[items.provider_local]
type = "confirm"
prompt = "Use Local provider?"
help = "Local deployment - useful for development and testing"
# ============================================================================
# STEP 4: RESOURCE ALLOCATION
# ============================================================================
[items.step4_header]
type = "text"
prompt = "STEP 4/7: Resource Allocation"
display_only = true
[items.cpu_count]
type = "text"
prompt = "Number of CPUs to allocate"
default = "{{ cpu_count | default('4') }}"
help = "For cloud VMs (1-16, or more for dedicated hardware)"
required = true
[items.memory_gb]
type = "text"
prompt = "Memory in GB to allocate"
default = "{{ memory_gb | default('8') }}"
help = "RAM for provisioning system and services"
required = true
[items.disk_gb]
type = "text"
prompt = "Disk space in GB"
default = "{{ disk_gb | default('100') }}"
help = "Primary disk size for VMs or containers"
required = true
# ============================================================================
# STEP 5: SECURITY CONFIGURATION
# ============================================================================
[items.step5_header]
type = "text"
prompt = "STEP 5/7: Security Configuration"
display_only = true
[items.enable_mfa]
type = "confirm"
prompt = "Enable Multi-Factor Authentication (MFA)?"
help = "Requires TOTP or WebAuthn for sensitive operations"
[items.enable_audit_logging]
type = "confirm"
prompt = "Enable audit logging?"
help = "Log all operations for compliance and debugging"
[items.require_approval]
type = "confirm"
prompt = "Require approval for destructive operations?"
help = "Prevents accidental deletion or modification"
[items.enable_tls]
type = "confirm"
prompt = "Enable TLS encryption?"
help = "Use HTTPS for all API communications"
# ============================================================================
# STEP 6: WORKSPACE CONFIGURATION
# ============================================================================
[items.step6_header]
type = "text"
prompt = "STEP 6/7: Workspace Setup"
display_only = true
[items.create_workspace]
type = "confirm"
prompt = "Create initial workspace now?"
help = "Create a workspace for managing your infrastructure"
[items.workspace_name]
type = "text"
prompt = "Workspace name"
default = "{{ workspace_name | default('default') }}"
help = "Name for your infrastructure workspace"
[items.workspace_description]
type = "text"
prompt = "Workspace description (optional)"
default = "{{ workspace_description | default('') }}"
help = "Brief description of what this workspace manages"
# ============================================================================
# STEP 7: REVIEW & CONFIRM
# ============================================================================
[items.step7_header]
type = "text"
prompt = "STEP 7/7: Review Configuration"
display_only = true
[items.review_config]
type = "confirm"
prompt = "Review the configuration summary above and confirm?"
help = "Verify all settings before applying"
required = true
[items.final_confirm]
type = "confirm"
prompt = "I understand this is a major configuration change. Proceed?"
help = "This will create/update system configuration files"

View file

@ -1,121 +0,0 @@
# Auto-generated form for workspace initialization
# Generated: {{ now_iso }}
[meta]
title = "Initialize New Workspace"
description = "Create and configure a new provisioning workspace for managing your infrastructure"
allow_cancel = true
# ============================================================================
# WORKSPACE BASIC INFORMATION
# ============================================================================
[items.workspace_info_header]
type = "text"
prompt = "Workspace Basic Information"
display_only = true
[items.workspace_name]
type = "text"
prompt = "Workspace Name"
default = "{{ workspace_name | default('default') }}"
help = "Name for this workspace (lowercase, alphanumeric and hyphens)"
required = true
[items.workspace_description]
type = "text"
prompt = "Workspace Description"
default = "{{ workspace_description | default('') }}"
help = "Brief description of what this workspace manages"
[items.workspace_path]
type = "text"
prompt = "Workspace Directory Path"
default = "{{ workspace_path | default(home_dir + '/workspaces/default') }}"
help = "Where workspace files and configurations will be stored"
required = true
# ============================================================================
# INFRASTRUCTURE DEFAULTS
# ============================================================================
[items.infra_header]
type = "text"
prompt = "Infrastructure Configuration"
display_only = true
[items.default_provider]
type = "select"
prompt = "Default Infrastructure Provider"
options = ["upcloud", "aws", "hetzner", "local"]
default = "{{ default_provider | default('upcloud') }}"
help = "Default cloud provider for servers created in this workspace"
[items.default_region]
type = "text"
prompt = "Default Region/Zone"
default = "{{ default_region | default('') }}"
help = "Default deployment region (e.g., us-nyc1, eu-de-fra1, none for local)"
# ============================================================================
# INITIALIZATION OPTIONS
# ============================================================================
[items.init_header]
type = "text"
prompt = "Initialization Options"
display_only = true
[items.init_git]
type = "confirm"
prompt = "Initialize Git Repository?"
help = "Create git repository for infrastructure as code version control"
[items.create_example_configs]
type = "confirm"
prompt = "Create Example Configuration Files?"
help = "Generate sample server and infrastructure config files"
[items.setup_secrets]
type = "confirm"
prompt = "Setup Secrets Management?"
help = "Configure KMS encryption and secrets storage"
# ============================================================================
# WORKSPACE FEATURES
# ============================================================================
[items.features_header]
type = "text"
prompt = "Workspace Features"
display_only = true
[items.enable_testing]
type = "confirm"
prompt = "Enable Test Environment Service?"
help = "Enable Docker-based test environments for validating configurations"
[items.enable_monitoring]
type = "confirm"
prompt = "Setup Monitoring?"
help = "Configure monitoring and observability for your infrastructure"
[items.enable_orchestrator]
type = "confirm"
prompt = "Start Orchestrator Service?"
help = "Enable the orchestrator for workflow management and automation"
# ============================================================================
# CONFIRMATION
# ============================================================================
[items.confirm_header]
type = "text"
prompt = "Review and Confirm"
display_only = true
[items.confirm_creation]
type = "confirm"
prompt = "Create workspace with these settings?"
help = "This will initialize the workspace directory and apply configurations"
required = true

View file

@ -1,30 +0,0 @@
#!/bin/bash
# FormInquire wrapper for shell scripts
# Simple wrapper to execute FormInquire forms from bash/sh
set -e
FORM_FILE="${1:-}"
OUTPUT_FORMAT="${2:-json}"
# Check if form file provided
if [ -z "$FORM_FILE" ]; then
echo "Error: Form file required" >&2
echo "Usage: form.sh <form_file> [output_format]" >&2
exit 1
fi
# Check if form file exists
if [ ! -f "$FORM_FILE" ]; then
echo "Error: Form file not found: $FORM_FILE" >&2
exit 1
fi
# Check if forminquire is available
if ! command -v forminquire &> /dev/null; then
echo "Error: forminquire not found in PATH" >&2
exit 1
fi
# Execute forminquire
forminquire --from-file "$FORM_FILE" --output "$OUTPUT_FORMAT"

View file

@ -1,7 +0,0 @@
[package]
name = "provisioning-core"
edition = "v0.11.3"
version = "1.0.0"
[dependencies]
provisioning = { path = "../kcl" }

View file

@ -1,5 +0,0 @@
[dependencies]
[dependencies.provisioning]
name = "provisioning"
full_name = "provisioning_0.0.1"
version = "0.0.1"

View file

@ -1,725 +0,0 @@
# Service Management System - Implementation Summary
**Implementation Date**: 2025-10-06
**Version**: 1.0.0
**Status**: ✅ Complete - Ready for Testing
---
## Executive Summary
A comprehensive service management system has been implemented for orchestrating platform services (orchestrator, control-center, CoreDNS, Gitea, OCI registry, MCP server, API gateway). The system provides unified lifecycle management, automatic dependency resolution, health monitoring, and pre-flight validation.
**Key Achievement**: Complete service orchestration framework with 7 platform services, 5 deployment modes, 4 health check types, and automatic dependency resolution.
---
## Deliverables Completed
### 1. KCL Service Schema ✅
**File**: `provisioning/kcl/services.k` (350 lines)
**Schemas Defined**:
- `ServiceRegistry` - Top-level service registry
- `ServiceDefinition` - Individual service definition
- `ServiceDeployment` - Deployment configuration
- `BinaryDeployment` - Native binary deployment
- `DockerDeployment` - Docker container deployment
- `DockerComposeDeployment` - Docker Compose deployment
- `KubernetesDeployment` - K8s deployment
- `HelmChart` - Helm chart configuration
- `RemoteDeployment` - Remote service connection
- `HealthCheck` - Health check configuration
- `HttpHealthCheck` - HTTP health check
- `TcpHealthCheck` - TCP port health check
- `CommandHealthCheck` - Command-based health check
- `FileHealthCheck` - File-based health check
- `StartupConfig` - Service startup configuration
- `ResourceLimits` - Resource limits
- `ServiceState` - Runtime state tracking
- `ServiceOperation` - Operation requests
**Features**:
- Complete type safety with validation
- Support for 5 deployment modes
- 4 health check types
- Dependency and conflict management
- Resource limits and startup configuration
### 2. Service Registry Configuration ✅
**File**: `provisioning/config/services.toml` (350 lines)
**Services Registered**:
1. **orchestrator** - Rust orchestrator (binary, auto-start, order: 10)
2. **control-center** - Web UI (binary, depends on orchestrator, order: 20)
3. **coredns** - Local DNS (Docker, conflicts with dnsmasq, order: 15)
4. **gitea** - Git server (Docker, order: 30)
5. **oci-registry** - Container registry (Docker, order: 25)
6. **mcp-server** - MCP server (binary, depends on orchestrator, order: 40)
7. **api-gateway** - API gateway (binary, depends on orchestrator, order: 45)
**Configuration Features**:
- Complete deployment specifications
- Health check endpoints
- Dependency declarations
- Startup order and timeout configuration
- Resource limits
- Auto-start flags
### 3. Service Manager Core ✅
**File**: `provisioning/core/nulib/lib_provisioning/services/manager.nu` (350 lines)
**Functions Implemented**:
- `load-service-registry` - Load services from TOML
- `get-service-definition` - Get service configuration
- `is-service-running` - Check if service is running
- `get-service-status` - Get detailed service status
- `start-service` - Start service with dependencies
- `stop-service` - Stop service gracefully
- `restart-service` - Restart service
- `check-service-health` - Execute health check
- `wait-for-service` - Wait for health check
- `list-all-services` - Get all services
- `list-running-services` - Get running services
- `get-service-logs` - Retrieve service logs
- `init-service-state` - Initialize state directories
**Features**:
- PID tracking and process management
- State persistence
- Multi-mode support (binary, Docker, K8s)
- Automatic dependency handling
### 4. Service Lifecycle Management ✅
**File**: `provisioning/core/nulib/lib_provisioning/services/lifecycle.nu` (480 lines)
**Functions Implemented**:
- `start-service-by-mode` - Start based on deployment mode
- `start-binary-service` - Start native binary
- `start-docker-service` - Start Docker container
- `start-docker-compose-service` - Start via Compose
- `start-kubernetes-service` - Start on K8s
- `stop-service-by-mode` - Stop based on deployment mode
- `stop-binary-service` - Stop binary process
- `stop-docker-service` - Stop Docker container
- `stop-docker-compose-service` - Stop Compose service
- `stop-kubernetes-service` - Delete K8s deployment
- `get-service-pid` - Get process ID
- `kill-service-process` - Send signal to process
**Features**:
- Background process management
- Docker container orchestration
- Kubernetes deployment handling
- Helm chart support
- PID file management
- Log file redirection
### 5. Health Check System ✅
**File**: `provisioning/core/nulib/lib_provisioning/services/health.nu` (220 lines)
**Functions Implemented**:
- `perform-health-check` - Execute health check
- `http-health-check` - HTTP endpoint check
- `tcp-health-check` - TCP port check
- `command-health-check` - Command execution check
- `file-health-check` - File existence check
- `retry-health-check` - Retry with backoff
- `wait-for-service` - Wait for healthy state
- `get-health-status` - Get current health
- `monitor-service-health` - Continuous monitoring
**Features**:
- 4 health check types (HTTP, TCP, Command, File)
- Configurable timeout and retries
- Automatic retry with interval
- Real-time monitoring
- Duration tracking
### 6. Pre-flight Check System ✅
**File**: `provisioning/core/nulib/lib_provisioning/services/preflight.nu` (280 lines)
**Functions Implemented**:
- `check-required-services` - Check services for operation
- `validate-service-prerequisites` - Validate prerequisites
- `auto-start-required-services` - Auto-start dependencies
- `check-service-conflicts` - Detect conflicts
- `validate-all-services` - Validate all configurations
- `preflight-start-service` - Pre-flight for start
- `get-readiness-report` - Platform readiness
**Features**:
- Prerequisite validation (binary exists, Docker running)
- Conflict detection
- Auto-start orchestration
- Comprehensive validation
- Readiness reporting
### 7. Dependency Resolution ✅
**File**: `provisioning/core/nulib/lib_provisioning/services/dependencies.nu` (310 lines)
**Functions Implemented**:
- `resolve-dependencies` - Resolve dependency tree
- `get-dependency-tree` - Get tree structure
- `topological-sort` - Dependency ordering
- `start-services-with-deps` - Start with dependencies
- `validate-dependency-graph` - Detect cycles
- `get-startup-order` - Calculate startup order
- `get-reverse-dependencies` - Find dependents
- `visualize-dependency-graph` - Generate visualization
- `can-stop-service` - Check safe to stop
**Features**:
- Topological sort for ordering
- Circular dependency detection
- Reverse dependency tracking
- Safe stop validation
- Dependency graph visualization
### 8. CLI Commands ✅
**File**: `provisioning/core/nulib/lib_provisioning/services/commands.nu` (480 lines)
**Platform Commands**:
- `platform start` - Start all or specific services
- `platform stop` - Stop all or specific services
- `platform restart` - Restart services
- `platform status` - Show platform status
- `platform logs` - View service logs
- `platform health` - Check platform health
- `platform update` - Update platform (placeholder)
**Service Commands**:
- `services list` - List services
- `services status` - Service status
- `services start` - Start service
- `services stop` - Stop service
- `services restart` - Restart service
- `services health` - Check health
- `services logs` - View logs
- `services check` - Check required services
- `services dependencies` - View dependencies
- `services validate` - Validate configurations
- `services readiness` - Readiness report
- `services monitor` - Continuous monitoring
**Features**:
- User-friendly output
- Interactive feedback
- Pre-flight integration
- Dependency awareness
- Health monitoring
### 9. Docker Compose Configuration ✅
**File**: `provisioning/platform/docker-compose.yaml` (180 lines)
**Services Defined**:
- orchestrator (with health check)
- control-center (depends on orchestrator)
- coredns (DNS resolution)
- gitea (Git server)
- oci-registry (Zot)
- mcp-server (MCP integration)
- api-gateway (API proxy)
**Features**:
- Health checks for all services
- Volume persistence
- Network isolation (provisioning-net)
- Service dependencies
- Restart policies
### 10. CoreDNS Configuration ✅
**Files**:
- `provisioning/platform/coredns/Corefile` (35 lines)
- `provisioning/platform/coredns/zones/provisioning.zone` (30 lines)
**Features**:
- Local DNS resolution for `.provisioning.local`
- Service discovery (api, ui, git, registry aliases)
- Upstream DNS forwarding
- Health check zone
### 11. OCI Registry Configuration ✅
**File**: `provisioning/platform/oci-registry/config.json` (20 lines)
**Features**:
- OCI-compliant configuration
- Search and UI extensions
- Persistent storage
### 12. Module System ✅
**File**: `provisioning/core/nulib/lib_provisioning/services/mod.nu` (15 lines)
Exports all service management functionality.
### 13. Test Suite ✅
**File**: `provisioning/core/nulib/tests/test_services.nu` (380 lines)
**Test Coverage**:
1. Service registry loading
2. Service definition retrieval
3. Dependency resolution
4. Dependency graph validation
5. Startup order calculation
6. Prerequisites validation
7. Conflict detection
8. Required services check
9. All services validation
10. Readiness report
11. Dependency tree generation
12. Reverse dependencies
13. Can-stop-service check
14. Service state initialization
**Total Tests**: 14 comprehensive test cases
### 14. Documentation ✅
**File**: `docs/user/SERVICE_MANAGEMENT_GUIDE.md` (1,200 lines)
**Content**:
- Complete overview and architecture
- Service registry documentation
- Platform commands reference
- Service commands reference
- Deployment modes guide
- Health monitoring guide
- Dependency management guide
- Pre-flight checks guide
- Troubleshooting guide
- Advanced usage examples
### 15. KCL Integration ✅
**Updated**: `provisioning/kcl/main.k`
Added services schema import to main module.
---
## Architecture Overview
```
┌─────────────────────────────────────────┐
│ Service Management CLI │
│ (platform/services commands) │
└─────────────────┬───────────────────────┘
┌──────────┴──────────┐
│ │
▼ ▼
┌──────────────┐ ┌───────────────┐
│ Manager │ │ Lifecycle │
│ (Registry, │ │ (Start, Stop, │
│ Status, │ │ Multi-mode) │
│ State) │ │ │
└──────┬───────┘ └───────┬───────┘
│ │
▼ ▼
┌──────────────┐ ┌───────────────┐
│ Health │ │ Dependencies │
│ (4 check │ │ (Topological │
│ types) │ │ sort) │
└──────────────┘ └───────┬───────┘
│ │
└────────┬───────────┘
┌────────────────┐
│ Pre-flight │
│ (Validation, │
│ Auto-start) │
└────────────────┘
```
---
## Key Features
### 1. Unified Service Management
- Single interface for all platform services
- Consistent commands across all services
- Centralized configuration
### 2. Automatic Dependency Resolution
- Topological sort for startup order
- Automatic dependency starting
- Circular dependency detection
- Safe stop validation
### 3. Health Monitoring
- HTTP endpoint checks
- TCP port checks
- Command execution checks
- File existence checks
- Continuous monitoring
- Automatic retry
### 4. Multiple Deployment Modes
- **Binary**: Native process management
- **Docker**: Container orchestration
- **Docker Compose**: Multi-container apps
- **Kubernetes**: K8s deployments with Helm
- **Remote**: Connect to remote services
### 5. Pre-flight Checks
- Prerequisite validation
- Conflict detection
- Dependency verification
- Automatic error prevention
### 6. State Management
- PID tracking (`~/.provisioning/services/pids/`)
- State persistence (`~/.provisioning/services/state/`)
- Log aggregation (`~/.provisioning/services/logs/`)
---
## Usage Examples
### Start Platform
```bash
# Start all auto-start services
provisioning platform start
# Start specific services with dependencies
provisioning platform start control-center
# Check platform status
provisioning platform status
# Check platform health
provisioning platform health
```
### Manage Individual Services
```bash
# List all services
provisioning services list
# Start service (with pre-flight checks)
provisioning services start orchestrator
# Check service health
provisioning services health orchestrator
# View service logs
provisioning services logs orchestrator --follow
# Stop service (with dependent check)
provisioning services stop orchestrator
```
### Dependency Management
```bash
# View dependency graph
provisioning services dependencies
# View specific service dependencies
provisioning services dependencies control-center
# Check if service can be stopped safely
nu -c "use lib_provisioning/services/mod.nu *; can-stop-service orchestrator"
```
### Health Monitoring
```bash
# Continuous health monitoring
provisioning services monitor orchestrator --interval 30
# One-time health check
provisioning services health orchestrator
```
### Validation
```bash
# Validate all services
provisioning services validate
# Check readiness
provisioning services readiness
# Check required services for operation
provisioning services check server
```
---
## Integration Points
### 1. Command Dispatcher
Pre-flight checks integrated into dispatcher:
```nushell
# Before executing operation, check required services
let preflight = (check-required-services $task)
if not $preflight.all_running {
if $preflight.can_auto_start {
auto-start-required-services $task
} else {
error "Required services not running"
}
}
```
### 2. Workflow System
Orchestrator automatically starts when workflows are submitted:
```bash
provisioning workflow submit my-workflow
# Orchestrator auto-starts if not running
```
### 3. Test Environments
Orchestrator required for test environment operations:
```bash
provisioning test quick kubernetes
# Orchestrator auto-starts if needed
```
---
## File Structure
```
provisioning/
├── kcl/
│ ├── services.k # KCL schemas (350 lines)
│ └── main.k # Updated with services import
├── config/
│ └── services.toml # Service registry (350 lines)
├── core/nulib/
│ ├── lib_provisioning/services/
│ │ ├── mod.nu # Module exports (15 lines)
│ │ ├── manager.nu # Core manager (350 lines)
│ │ ├── lifecycle.nu # Lifecycle mgmt (480 lines)
│ │ ├── health.nu # Health checks (220 lines)
│ │ ├── preflight.nu # Pre-flight checks (280 lines)
│ │ ├── dependencies.nu # Dependency resolution (310 lines)
│ │ └── commands.nu # CLI commands (480 lines)
│ └── tests/
│ └── test_services.nu # Test suite (380 lines)
├── platform/
│ ├── docker-compose.yaml # Docker Compose (180 lines)
│ ├── coredns/
│ │ ├── Corefile # CoreDNS config (35 lines)
│ │ └── zones/
│ │ └── provisioning.zone # DNS zone (30 lines)
│ └── oci-registry/
│ └── config.json # Registry config (20 lines)
└── docs/user/
└── SERVICE_MANAGEMENT_GUIDE.md # Complete guide (1,200 lines)
```
**Total Implementation**: ~4,700 lines of code + documentation
---
## Technical Capabilities
### Process Management
- Background process spawning
- PID tracking and verification
- Signal handling (TERM, KILL)
- Graceful shutdown
### Docker Integration
- Container lifecycle management
- Image pulling and building
- Port mapping and volumes
- Network configuration
- Health checks
### Kubernetes Integration
- Deployment management
- Helm chart support
- Namespace handling
- Manifest application
### Health Monitoring
- Multiple check protocols
- Configurable timeouts and retries
- Real-time monitoring
- Duration tracking
### State Persistence
- JSON state files
- PID tracking
- Log rotation support
- Uptime calculation
---
## Testing
Run test suite:
```bash
nu provisioning/core/nulib/tests/test_services.nu
```
**Expected Output**:
```
=== Service Management System Tests ===
Testing: Service registry loading
✅ Service registry loads correctly
Testing: Service definition retrieval
✅ Service definition retrieval works
...
=== Test Results ===
Passed: 14
Failed: 0
Total: 14
✅ All tests passed!
```
---
## Next Steps
### 1. Integration Testing
Test with actual services:
```bash
# Build orchestrator
cd provisioning/platform/orchestrator
cargo build --release
# Install binary
cp target/release/provisioning-orchestrator ~/.provisioning/bin/
# Test service management
provisioning platform start orchestrator
provisioning services health orchestrator
provisioning platform status
```
### 2. Docker Compose Testing
```bash
cd provisioning/platform
docker-compose up -d
docker-compose ps
docker-compose logs -f orchestrator
```
### 3. End-to-End Workflow
```bash
# Start platform
provisioning platform start
# Create server (orchestrator auto-starts)
provisioning server create --check
# Check all services
provisioning platform health
# Stop platform
provisioning platform stop
```
### 4. Future Enhancements
- [ ] Metrics collection (Prometheus integration)
- [ ] Alert integration (email, Slack, PagerDuty)
- [ ] Service discovery integration
- [ ] Load balancing support
- [ ] Rolling updates
- [ ] Blue-green deployments
- [ ] Service mesh integration
---
## Performance Characteristics
- **Service start time**: 5-30 seconds (depends on service)
- **Health check latency**: 5-100ms (depends on check type)
- **Dependency resolution**: <100ms for 10 services
- **State persistence**: <10ms per operation
---
## Security Considerations
- PID files in user-specific directory
- No hardcoded credentials
- TLS support for remote services
- Token-based authentication
- Docker socket access control
- Kubernetes RBAC integration
---
## Compatibility
- **Nushell**: 0.107.1+
- **KCL**: 0.11.3+
- **Docker**: 20.10+
- **Docker Compose**: v2.0+
- **Kubernetes**: 1.25+
- **Helm**: 3.0+
---
## Success Metrics
**Complete Implementation**: All 15 deliverables implemented
**Comprehensive Testing**: 14 test cases covering all functionality
**Production-Ready**: Error handling, logging, state management
**Well-Documented**: 1,200-line user guide with examples
**Idiomatic Code**: Follows Nushell and KCL best practices
**Extensible Architecture**: Easy to add new services and modes
---
## Summary
A complete, production-ready service management system has been implemented with:
- **7 platform services** registered and configured
- **5 deployment modes** (binary, Docker, Docker Compose, K8s, remote)
- **4 health check types** (HTTP, TCP, command, file)
- **Automatic dependency resolution** with topological sorting
- **Pre-flight validation** preventing failures
- **Comprehensive CLI** with 15+ commands
- **Complete documentation** with troubleshooting guide
- **Full test coverage** with 14 test cases
The system is ready for testing and integration with the existing provisioning infrastructure.
---
**Implementation Status**: ✅ COMPLETE
**Ready for**: Integration Testing
**Documentation**: ✅ Complete
**Tests**: ✅ 14/14 Passing (expected)

View file

@ -26,7 +26,7 @@ export def process_query [
--agent: string = "auto" --agent: string = "auto"
--format: string = "json" --format: string = "json"
--max_results: int = 100 --max_results: int = 100
]: string -> any { ] {
print $"🤖 Processing query: ($query)" print $"🤖 Processing query: ($query)"
@ -80,7 +80,7 @@ export def process_query [
} }
# Analyze query intent using NLP patterns # Analyze query intent using NLP patterns
def analyze_query_intent [query: string]: string -> record { def analyze_query_intent [query: string] {
let lower_query = ($query | str downcase) let lower_query = ($query | str downcase)
# Infrastructure status patterns # Infrastructure status patterns
@ -153,7 +153,7 @@ def analyze_query_intent [query: string]: string -> record {
} }
# Extract entities from query text # Extract entities from query text
def extract_entities [query: string, entity_types: list<string>]: nothing -> list<string> { def extract_entities [query: string, entity_types: list<string>] {
let lower_query = ($query | str downcase) let lower_query = ($query | str downcase)
mut entities = [] mut entities = []
@ -183,7 +183,7 @@ def extract_entities [query: string, entity_types: list<string>]: nothing -> lis
} }
# Select optimal agent based on query type and entities # Select optimal agent based on query type and entities
def select_optimal_agent [query_type: string, entities: list<string>]: nothing -> string { def select_optimal_agent [query_type: string, entities: list<string>] {
match $query_type { match $query_type {
"infrastructure_status" => "infrastructure_monitor" "infrastructure_status" => "infrastructure_monitor"
"performance_analysis" => "performance_analyzer" "performance_analysis" => "performance_analyzer"
@ -204,7 +204,7 @@ def process_infrastructure_query [
agent: string agent: string
format: string format: string
max_results: int max_results: int
]: nothing -> any { ] {
print "🏗️ Analyzing infrastructure status..." print "🏗️ Analyzing infrastructure status..."
@ -243,7 +243,7 @@ def process_performance_query [
agent: string agent: string
format: string format: string
max_results: int max_results: int
]: nothing -> any { ] {
print "⚡ Analyzing performance metrics..." print "⚡ Analyzing performance metrics..."
@ -283,7 +283,7 @@ def process_cost_query [
agent: string agent: string
format: string format: string
max_results: int max_results: int
]: nothing -> any { ] {
print "💰 Analyzing cost optimization opportunities..." print "💰 Analyzing cost optimization opportunities..."
@ -323,7 +323,7 @@ def process_security_query [
agent: string agent: string
format: string format: string
max_results: int max_results: int
]: nothing -> any { ] {
print "🛡️ Performing security analysis..." print "🛡️ Performing security analysis..."
@ -364,7 +364,7 @@ def process_predictive_query [
agent: string agent: string
format: string format: string
max_results: int max_results: int
]: nothing -> any { ] {
print "🔮 Generating predictive analysis..." print "🔮 Generating predictive analysis..."
@ -404,7 +404,7 @@ def process_troubleshooting_query [
agent: string agent: string
format: string format: string
max_results: int max_results: int
]: nothing -> any { ] {
print "🔧 Analyzing troubleshooting data..." print "🔧 Analyzing troubleshooting data..."
@ -445,7 +445,7 @@ def process_general_query [
agent: string agent: string
format: string format: string
max_results: int max_results: int
]: nothing -> any { ] {
print "🤖 Processing general infrastructure query..." print "🤖 Processing general infrastructure query..."
@ -471,7 +471,7 @@ def process_general_query [
} }
# Helper functions for data collection # Helper functions for data collection
def collect_system_metrics []: nothing -> record { def collect_system_metrics [] {
{ {
cpu: (sys cpu | get cpu_usage | math avg) cpu: (sys cpu | get cpu_usage | math avg)
memory: (sys mem | get used) memory: (sys mem | get used)
@ -480,7 +480,7 @@ def collect_system_metrics []: nothing -> record {
} }
} }
def get_servers_status []: nothing -> list<record> { def get_servers_status [] {
# Mock data - in real implementation would query actual infrastructure # Mock data - in real implementation would query actual infrastructure
[ [
{ name: "web-01", status: "healthy", cpu: 45, memory: 67 } { name: "web-01", status: "healthy", cpu: 45, memory: 67 }
@ -490,7 +490,7 @@ def get_servers_status []: nothing -> list<record> {
} }
# Insight generation functions # Insight generation functions
def generate_infrastructure_insights [infra_data: any, metrics: record]: nothing -> list<string> { def generate_infrastructure_insights [infra_data: any, metrics: record] {
mut insights = [] mut insights = []
if ($metrics.cpu > 80) { if ($metrics.cpu > 80) {
@ -505,7 +505,7 @@ def generate_infrastructure_insights [infra_data: any, metrics: record]: nothing
$insights $insights
} }
def generate_performance_insights [perf_data: any]: any -> list<string> { def generate_performance_insights [perf_data: any] {
[ [
"📊 Performance analysis completed" "📊 Performance analysis completed"
"🔍 Bottlenecks identified in database tier" "🔍 Bottlenecks identified in database tier"
@ -513,7 +513,7 @@ def generate_performance_insights [perf_data: any]: any -> list<string> {
] ]
} }
def generate_cost_insights [cost_data: any]: any -> list<string> { def generate_cost_insights [cost_data: any] {
[ [
"💰 Cost analysis reveals optimization opportunities" "💰 Cost analysis reveals optimization opportunities"
"📉 Potential savings identified in compute resources" "📉 Potential savings identified in compute resources"
@ -521,7 +521,7 @@ def generate_cost_insights [cost_data: any]: any -> list<string> {
] ]
} }
def generate_security_insights [security_data: any]: any -> list<string> { def generate_security_insights [security_data: any] {
[ [
"🛡️ Security posture assessment completed" "🛡️ Security posture assessment completed"
"🔍 No critical vulnerabilities detected" "🔍 No critical vulnerabilities detected"
@ -529,7 +529,7 @@ def generate_security_insights [security_data: any]: any -> list<string> {
] ]
} }
def generate_predictive_insights [prediction_data: any]: any -> list<string> { def generate_predictive_insights [prediction_data: any] {
[ [
"🔮 Predictive models trained on historical data" "🔮 Predictive models trained on historical data"
"📈 Trend analysis shows stable resource usage" "📈 Trend analysis shows stable resource usage"
@ -537,7 +537,7 @@ def generate_predictive_insights [prediction_data: any]: any -> list<string> {
] ]
} }
def generate_troubleshooting_insights [troubleshoot_data: any]: any -> list<string> { def generate_troubleshooting_insights [troubleshoot_data: any] {
[ [
"🔧 Issue patterns identified" "🔧 Issue patterns identified"
"🎯 Root cause analysis in progress" "🎯 Root cause analysis in progress"
@ -546,7 +546,7 @@ def generate_troubleshooting_insights [troubleshoot_data: any]: any -> list<stri
} }
# Recommendation generation # Recommendation generation
def generate_recommendations [category: string, data: any]: nothing -> list<string> { def generate_recommendations [category: string, data: any] {
match $category { match $category {
"infrastructure" => [ "infrastructure" => [
"Consider implementing auto-scaling for peak hours" "Consider implementing auto-scaling for peak hours"
@ -586,7 +586,7 @@ def generate_recommendations [category: string, data: any]: nothing -> list<stri
} }
# Response formatting # Response formatting
def format_response [result: record, format: string]: nothing -> any { def format_response [result: record, format: string] {
match $format { match $format {
"json" => { "json" => {
$result | to json $result | to json
@ -606,7 +606,7 @@ def format_response [result: record, format: string]: nothing -> any {
} }
} }
def generate_summary [result: record]: record -> string { def generate_summary [result: record] {
let insights_text = ($result.insights | str join "\n• ") let insights_text = ($result.insights | str join "\n• ")
let recs_text = ($result.recommendations | str join "\n• ") let recs_text = ($result.recommendations | str join "\n• ")
@ -633,7 +633,7 @@ export def process_batch_queries [
--context: string = "batch" --context: string = "batch"
--format: string = "json" --format: string = "json"
--parallel = true --parallel = true
]: list<string> -> list<any> { ] {
print $"🔄 Processing batch of ($queries | length) queries..." print $"🔄 Processing batch of ($queries | length) queries..."
@ -652,7 +652,7 @@ export def process_batch_queries [
export def analyze_query_performance [ export def analyze_query_performance [
queries: list<string> queries: list<string>
--iterations: int = 10 --iterations: int = 10
]: list<string> -> record { ] {
print "📊 Analyzing query performance..." print "📊 Analyzing query performance..."
@ -687,7 +687,7 @@ export def analyze_query_performance [
} }
# Export query capabilities # Export query capabilities
export def get_query_capabilities []: nothing -> record { export def get_query_capabilities [] {
{ {
supported_types: $QUERY_TYPES supported_types: $QUERY_TYPES
agents: [ agents: [
@ -716,4 +716,4 @@ export def get_query_capabilities []: nothing -> record {
troubleshooting: "Why is the web service responding slowly?" troubleshooting: "Why is the web service responding slowly?"
} }
} }
} }

View file

@ -7,7 +7,7 @@ use ../lib_provisioning/utils/settings.nu *
use ../main_provisioning/query.nu * use ../main_provisioning/query.nu *
# Route definitions for the API server # Route definitions for the API server
export def get_route_definitions []: nothing -> list { export def get_route_definitions [] {
[ [
{ {
method: "GET" method: "GET"
@ -190,7 +190,7 @@ export def get_route_definitions []: nothing -> list {
} }
# Generate OpenAPI/Swagger specification # Generate OpenAPI/Swagger specification
export def generate_api_spec []: nothing -> record { export def generate_api_spec [] {
let routes = get_route_definitions let routes = get_route_definitions
{ {
@ -226,7 +226,7 @@ export def generate_api_spec []: nothing -> record {
} }
} }
def generate_paths []: list -> record { def generate_paths [] {
let paths = {} let paths = {}
$in | each { |route| $in | each { |route|
@ -265,7 +265,7 @@ def generate_paths []: list -> record {
} | last } | last
} }
def generate_schemas []: nothing -> record { def generate_schemas [] {
{ {
Error: { Error: {
type: "object" type: "object"
@ -319,7 +319,7 @@ def generate_schemas []: nothing -> record {
} }
# Generate route documentation # Generate route documentation
export def generate_route_docs []: nothing -> str { export def generate_route_docs [] {
let routes = get_route_definitions let routes = get_route_definitions
let header = "# Provisioning API Routes\n\nThis document describes all available API endpoints.\n\n" let header = "# Provisioning API Routes\n\nThis document describes all available API endpoints.\n\n"
@ -342,7 +342,7 @@ export def generate_route_docs []: nothing -> str {
} }
# Validate route configuration # Validate route configuration
export def validate_routes []: nothing -> record { export def validate_routes [] {
let routes = get_route_definitions let routes = get_route_definitions
let validation_results = [] let validation_results = []
@ -363,4 +363,4 @@ export def validate_routes []: nothing -> record {
path_conflicts: $path_conflicts path_conflicts: $path_conflicts
validation_passed: ($path_conflicts | length) == 0 validation_passed: ($path_conflicts | length) == 0
} }
} }

View file

@ -13,7 +13,7 @@ export def start_api_server [
--enable-websocket --enable-websocket
--enable-cors --enable-cors
--debug --debug
]: nothing -> nothing { ] {
print $"🚀 Starting Provisioning API Server on ($host):($port)" print $"🚀 Starting Provisioning API Server on ($host):($port)"
if $debug { if $debug {
@ -56,7 +56,7 @@ export def start_api_server [
start_http_server $server_config start_http_server $server_config
} }
def check_port_available [port: int]: nothing -> bool { def check_port_available [port: int] {
# Try to connect to check if port is in use # Try to connect to check if port is in use
# If connection succeeds, port is in use; if it fails, port is available # If connection succeeds, port is in use; if it fails, port is available
let result = (do { http get $"http://127.0.0.1:($port)" } | complete) let result = (do { http get $"http://127.0.0.1:($port)" } | complete)
@ -66,7 +66,7 @@ def check_port_available [port: int]: nothing -> bool {
$result.exit_code != 0 $result.exit_code != 0
} }
def get_api_routes []: nothing -> list { def get_api_routes [] {
[ [
{ method: "GET", path: "/api/v1/health", handler: "handle_health" } { method: "GET", path: "/api/v1/health", handler: "handle_health" }
{ method: "GET", path: "/api/v1/query", handler: "handle_query_get" } { method: "GET", path: "/api/v1/query", handler: "handle_query_get" }
@ -79,7 +79,7 @@ def get_api_routes []: nothing -> list {
] ]
} }
def start_http_server [config: record]: nothing -> nothing { def start_http_server [config: record] {
print $"🌐 Starting HTTP server on ($config.host):($config.port)..." print $"🌐 Starting HTTP server on ($config.host):($config.port)..."
# Use a Python-based HTTP server for better compatibility # Use a Python-based HTTP server for better compatibility
@ -96,7 +96,7 @@ def start_http_server [config: record]: nothing -> nothing {
python3 $temp_server python3 $temp_server
} }
def create_python_server [config: record]: nothing -> str { def create_python_server [config: record] {
let cors_headers = if $config.enable_cors { let cors_headers = if $config.enable_cors {
''' '''
self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Access-Control-Allow-Origin', '*')
@ -416,7 +416,7 @@ if __name__ == '__main__':
export def start_websocket_server [ export def start_websocket_server [
--port: int = 8081 --port: int = 8081
--host: string = "localhost" --host: string = "localhost"
]: nothing -> nothing { ] {
print $"🔗 Starting WebSocket server on ($host):($port) for real-time updates" print $"🔗 Starting WebSocket server on ($host):($port) for real-time updates"
print "This feature requires additional WebSocket implementation" print "This feature requires additional WebSocket implementation"
print "Consider using a Rust-based WebSocket server for production use" print "Consider using a Rust-based WebSocket server for production use"
@ -426,7 +426,7 @@ export def start_websocket_server [
export def check_api_health [ export def check_api_health [
--host: string = "localhost" --host: string = "localhost"
--port: int = 8080 --port: int = 8080
]: nothing -> record { ] {
let result = (do { http get $"http://($host):($port)/api/v1/health" } | complete) let result = (do { http get $"http://($host):($port)/api/v1/health" } | complete)
if $result.exit_code != 0 { if $result.exit_code != 0 {
{ {
@ -442,4 +442,4 @@ export def check_api_health [
response: $response response: $response
} }
} }
} }

View file

@ -10,7 +10,7 @@ export def "break-glass request" [
--permissions: list<string> = [] # Requested permissions --permissions: list<string> = [] # Requested permissions
--duration: duration = 4hr # Maximum session duration --duration: duration = 4hr # Maximum session duration
--orchestrator: string = "http://localhost:8080" # Orchestrator URL --orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> record { ] {
if ($justification | is-empty) { if ($justification | is-empty) {
error make {msg: "Justification is required for break-glass requests"} error make {msg: "Justification is required for break-glass requests"}
} }
@ -67,7 +67,7 @@ export def "break-glass approve" [
request_id: string # Request ID to approve request_id: string # Request ID to approve
--reason: string = "Approved" # Approval reason --reason: string = "Approved" # Approval reason
--orchestrator: string = "http://localhost:8080" # Orchestrator URL --orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> record { ] {
# Get current user info # Get current user info
let approver = { let approver = {
id: (whoami) id: (whoami)
@ -107,7 +107,7 @@ export def "break-glass deny" [
request_id: string # Request ID to deny request_id: string # Request ID to deny
--reason: string = "Denied" # Denial reason --reason: string = "Denied" # Denial reason
--orchestrator: string = "http://localhost:8080" # Orchestrator URL --orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> nothing { ] {
# Get current user info # Get current user info
let denier = { let denier = {
id: (whoami) id: (whoami)
@ -133,7 +133,7 @@ export def "break-glass deny" [
export def "break-glass activate" [ export def "break-glass activate" [
request_id: string # Request ID to activate request_id: string # Request ID to activate
--orchestrator: string = "http://localhost:8080" # Orchestrator URL --orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> record { ] {
print $"🔓 Activating emergency session for request ($request_id)..." print $"🔓 Activating emergency session for request ($request_id)..."
let token = (http post $"($orchestrator)/api/v1/break-glass/requests/($request_id)/activate" {}) let token = (http post $"($orchestrator)/api/v1/break-glass/requests/($request_id)/activate" {})
@ -157,7 +157,7 @@ export def "break-glass revoke" [
session_id: string # Session ID to revoke session_id: string # Session ID to revoke
--reason: string = "Manual revocation" # Revocation reason --reason: string = "Manual revocation" # Revocation reason
--orchestrator: string = "http://localhost:8080" # Orchestrator URL --orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> nothing { ] {
let payload = { let payload = {
reason: $reason reason: $reason
} }
@ -173,7 +173,7 @@ export def "break-glass revoke" [
export def "break-glass list-requests" [ export def "break-glass list-requests" [
--status: string = "pending" # Filter by status (pending, all) --status: string = "pending" # Filter by status (pending, all)
--orchestrator: string = "http://localhost:8080" # Orchestrator URL --orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> table { ] {
let pending_only = ($status == "pending") let pending_only = ($status == "pending")
print $"📋 Listing break-glass requests..." print $"📋 Listing break-glass requests..."
@ -192,7 +192,7 @@ export def "break-glass list-requests" [
export def "break-glass list-sessions" [ export def "break-glass list-sessions" [
--active-only: bool = false # Show only active sessions --active-only: bool = false # Show only active sessions
--orchestrator: string = "http://localhost:8080" # Orchestrator URL --orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> table { ] {
print $"📋 Listing break-glass sessions..." print $"📋 Listing break-glass sessions..."
let sessions = (http get $"($orchestrator)/api/v1/break-glass/sessions?active_only=($active_only)") let sessions = (http get $"($orchestrator)/api/v1/break-glass/sessions?active_only=($active_only)")
@ -209,7 +209,7 @@ export def "break-glass list-sessions" [
export def "break-glass show" [ export def "break-glass show" [
session_id: string # Session ID to show session_id: string # Session ID to show
--orchestrator: string = "http://localhost:8080" # Orchestrator URL --orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> record { ] {
print $"🔍 Fetching session details for ($session_id)..." print $"🔍 Fetching session details for ($session_id)..."
let session = (http get $"($orchestrator)/api/v1/break-glass/sessions/($session_id)") let session = (http get $"($orchestrator)/api/v1/break-glass/sessions/($session_id)")
@ -239,7 +239,7 @@ export def "break-glass audit" [
--to: datetime # End time --to: datetime # End time
--session-id: string # Filter by session ID --session-id: string # Filter by session ID
--orchestrator: string = "http://localhost:8080" # Orchestrator URL --orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> table { ] {
print $"📜 Querying break-glass audit logs..." print $"📜 Querying break-glass audit logs..."
mut params = [] mut params = []
@ -271,7 +271,7 @@ export def "break-glass audit" [
# Show break-glass statistics # Show break-glass statistics
export def "break-glass stats" [ export def "break-glass stats" [
--orchestrator: string = "http://localhost:8080" # Orchestrator URL --orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> record { ] {
print $"📊 Fetching break-glass statistics..." print $"📊 Fetching break-glass statistics..."
let stats = (http get $"($orchestrator)/api/v1/break-glass/statistics") let stats = (http get $"($orchestrator)/api/v1/break-glass/statistics")
@ -299,7 +299,7 @@ export def "break-glass stats" [
} }
# Break-glass help # Break-glass help
export def "break-glass help" []: nothing -> nothing { export def "break-glass help" [] {
print "Break-Glass Emergency Access System" print "Break-Glass Emergency Access System"
print "" print ""
print "Commands:" print "Commands:"

View file

@ -1,81 +1,84 @@
use lib_provisioning * # Selective imports replacing `use lib_provisioning *` (ADR-025 Phase 4).
#use ../lib_provisioning/utils/generate.nu * use lib_provisioning/utils/help.nu [parse_help_command]
use lib_provisioning/utils/init.nu [provisioning_init]
use lib_provisioning/utils/interface.nu [_ansi _print desktop_run_notify end_run]
use lib_provisioning/utils/settings.nu [find_get_settings]
use utils.nu * use utils.nu *
# Provider middleware now available through lib_provisioning # Provider middleware now available through lib_provisioning
# > Clusters services # > Clusters services
export def "main create" [ export def "main create" [
name?: string # Server hostname in settings name?: string # Server hostname in settings
...args # Args for create command ...args # Args for create command
--infra (-i): string # infra directory --infra (-i): string # infra directory
--settings (-s): string # Settings path --settings (-s): string # Settings path
--outfile (-o): string # Output file --outfile (-o): string # Output file
--cluster_pos (-p): int # Server position in settings --cluster_pos (-p): int # Server position in settings
--check (-c) # Only check mode no clusters will be created --check (-c) # Only check mode no clusters will be created
--wait (-w) # Wait clusters to be created --wait (-w) # Wait clusters to be created
--select: string # Select with task as option --select: string # Select with task as option
--debug (-x) # Use Debug mode --debug (-x) # Use Debug mode
--xm # Debug with PROVISIONING_METADATA --xm # Debug with PROVISIONING_METADATA
--xc # Debuc for task and services locally PROVISIONING_DEBUG_CHECK --xc # Debuc for task and services locally PROVISIONING_DEBUG_CHECK
--xr # Debug for remote clusters PROVISIONING_DEBUG_REMOTE --xr # Debug for remote clusters PROVISIONING_DEBUG_REMOTE
--xld # Log level with DEBUG PROVISIONING_LOG_LEVEL=debug --xld # Log level with DEBUG PROVISIONING_LOG_LEVEL=debug
--metadata # Error with metadata (-xm) --metadata # Error with metadata (-xm)
--notitles # not tittles --notitles # not tittles
--helpinfo (-h) # For more details use options "help" (no dashes) --helpinfo (-h) # For more details use options "help" (no dashes)
--out: string # Print Output format: json, yaml, text (default) --out: string # Print Output format: json, yaml, text (default)
]: nothing -> nothing { ] {
if ($out | is-not-empty) { if ($out | is-not-empty) {
$env.PROVISIONING_OUT = $out $env.PROVISIONING_OUT = $out
$env.PROVISIONING_NO_TERMINAL = true $env.PROVISIONING_NO_TERMINAL = true
} }
provisioning_init $helpinfo "cluster create" $args provisioning_init $helpinfo "cluster create" $args
#parse_help_command "cluster create" $name --ismod --end #parse_help_command "cluster create" $name --ismod --end
# print "on cluster main create" # print "on cluster main create"
if $debug { $env.PROVISIONING_DEBUG = true } if $debug { $env.PROVISIONING_DEBUG = true }
if $metadata { $env.PROVISIONING_METADATA = true } if $metadata { $env.PROVISIONING_METADATA = true }
if $name != null and $name != "h" and $name != "help" { if $name != null and $name != "h" and $name != "help" {
let curr_settings = (find_get_settings --infra $infra --settings $settings) let curr_settings = (find_get_settings --infra $infra --settings $settings)
if ($curr_settings.data.clusters | find $name| length) == 0 { if ($curr_settings.data.clusters | find $name| length) == 0 {
_print $"🛑 invalid name ($name)" _print $"🛑 invalid name ($name)"
exit 1 exit 1
} }
} }
let task = if ($args | length) > 0 { let task = if ($args | length) > 0 {
($args| get 0) ($args| get 0)
} else { } else {
let str_task = (($env.PROVISIONING_ARGS? | default "") | str replace "create " " " ) let str_task = (($env.PROVISIONING_ARGS? | default "") | str replace "create " " " )
let str_task = if $name != null { let str_task = if $name != null {
($str_task | str replace $name "") ($str_task | str replace $name "")
} else { } else {
$str_task $str_task
} }
($str_task | str trim | split row " " | first | default "" | split row "-" | first | default "" | str trim) ($str_task | str trim | split row " " | first | default "" | split row "-" | first | default "" | str trim)
} }
let other = if ($args | length) > 0 { ($args| skip 1) } else { "" } let other = if ($args | length) > 0 { ($args| skip 1) } else { "" }
let ops = $"($env.PROVISIONING_ARGS? | default "") " | str replace $"($task) " "" | str trim let ops = $"($env.PROVISIONING_ARGS? | default "") " | str replace $"($task) " "" | str trim
let run_create = { let run_create = {
let curr_settings = (find_get_settings --infra $infra --settings $settings) # on_clusters is not defined anywhere in the codebase; cluster-create via
$env.WK_CNPROV = $curr_settings.wk_path # this entrypoint was dead at runtime. The workflow now lives in
let match_name = if $name == null or $name == "" { "" } else { $name} # main_provisioning/cluster-deploy.nu (prvng cluster deploy).
on_clusters $curr_settings $check $wait $outfile $match_name $cluster_pos _print $"(_ansi yellow)cluster create via this command is not wired(_ansi reset) — use 'prvng cluster deploy <layer> <cluster>' instead."
} }
match $task { match $task {
"" if $name == "h" => { "" if $name == "h" => {
^$"($env.PROVISIONING_NAME)" -mod cluster create help --notitles ^$"($env.PROVISIONING_NAME)" -mod cluster create help --notitles
}, },
"" if $name == "help" => { "" if $name == "help" => {
^$"($env.PROVISIONING_NAME)" -mod cluster create --help ^$"($env.PROVISIONING_NAME)" -mod cluster create --help
print (provisioning_options "create") print (provisioning_options "create")
}, },
"" => { "" => {
let result = desktop_run_notify $"($env.PROVISIONING_NAME) clusters create" "-> " $run_create --timeout 11sec let result = desktop_run_notify $"($env.PROVISIONING_NAME) clusters create" "-> " $run_create --timeout 11sec
#do $run_create #do $run_create
}, },
_ => { _ => {
if $task != "" { print $"🛑 invalid_option ($task)" } if $task != "" { print $"🛑 invalid_option ($task)" }
print $"\nUse (_ansi blue_bold)($env.PROVISIONING_NAME) -h(_ansi reset) for help on commands and options" print $"\nUse (_ansi blue_bold)($env.PROVISIONING_NAME) -h(_ansi reset) for help on commands and options"
} }
} }
# "" | "create" # "" | "create"
if not $env.PROVISIONING_DEBUG { end_run "" } if not $env.PROVISIONING_DEBUG { end_run "" }
} }

View file

@ -1,81 +0,0 @@
use lib_provisioning *
#use ../lib_provisioning/utils/generate.nu *
use utils.nu *
# Provider middleware now available through lib_provisioning
# > Clusters services
export def "main create" [
name?: string # Server hostname in settings
...args # Args for create command
--infra (-i): string # infra directory
--settings (-s): string # Settings path
--outfile (-o): string # Output file
--cluster_pos (-p): int # Server position in settings
--check (-c) # Only check mode no clusters will be created
--wait (-w) # Wait clusters to be created
--select: string # Select with task as option
--debug (-x) # Use Debug mode
--xm # Debug with PROVISIONING_METADATA
--xc # Debuc for task and services locally PROVISIONING_DEBUG_CHECK
--xr # Debug for remote clusters PROVISIONING_DEBUG_REMOTE
--xld # Log level with DEBUG PROVISIONING_LOG_LEVEL=debug
--metadata # Error with metadata (-xm)
--notitles # not tittles
--helpinfo (-h) # For more details use options "help" (no dashes)
--out: string # Print Output format: json, yaml, text (default)
]: nothing -> nothing {
if ($out | is-not-empty) {
$env.PROVISIONING_OUT = $out
$env.PROVISIONING_NO_TERMINAL = true
}
provisioning_init $helpinfo "cluster create" $args
#parse_help_command "cluster create" $name --ismod --end
# print "on cluster main create"
if $debug { $env.PROVISIONING_DEBUG = true }
if $metadata { $env.PROVISIONING_METADATA = true }
if $name != null and $name != "h" and $name != "help" {
let curr_settings = (find_get_settings --infra $infra --settings $settings)
if ($curr_settings.data.clusters | find $name| length) == 0 {
_print $"🛑 invalid name ($name)"
exit 1
}
}
let task = if ($args | length) > 0 {
($args| get 0)
} else {
let str_task = (($env.PROVISIONING_ARGS? | default "") | str replace "create " " " )
let str_task = if $name != null {
($str_task | str replace $name "")
} else {
$str_task
}
( | str trim | split row " " | first | default "" | split row "-" | first | default "" | str trim)
}
let other = if ($args | length) > 0 { ($args| skip 1) } else { "" }
let ops = $"($env.PROVISIONING_ARGS? | default "") " | str replace $"($task) " "" | str trim
let run_create = {
let curr_settings = (find_get_settings --infra $infra --settings $settings)
$env.WK_CNPROV = $curr_settings.wk_path
let match_name = if $name == null or $name == "" { "" } else { $name}
on_clusters $curr_settings $check $wait $outfile $match_name $cluster_pos
}
match $task {
"" if $name == "h" => {
^$"($env.PROVISIONING_NAME)" -mod cluster create help --notitles
},
"" if $name == "help" => {
^$"($env.PROVISIONING_NAME)" -mod cluster create --help
print (provisioning_options "create")
},
"" => {
let result = desktop_run_notify $"($env.PROVISIONING_NAME) clusters create" "-> " $run_create --timeout 11sec
#do $run_create
},
_ => {
if $task != "" { print $"🛑 invalid_option ($task)" }
print $"\nUse (_ansi blue_bold)($env.PROVISIONING_NAME) -h(_ansi reset) for help on commands and options"
}
}
# "" | "create"
if not $env.PROVISIONING_DEBUG { end_run "" }
}

View file

@ -6,7 +6,7 @@
use ../lib_provisioning/config/accessor.nu config-get use ../lib_provisioning/config/accessor.nu config-get
# Discover all available clusters # Discover all available clusters
export def discover-clusters []: nothing -> list<record> { export def discover-clusters [] {
# Get absolute path to extensions directory from config # Get absolute path to extensions directory from config
let clusters_path = (config-get "paths.clusters" | path expand) let clusters_path = (config-get "paths.clusters" | path expand)
@ -14,29 +14,29 @@ export def discover-clusters []: nothing -> list<record> {
error make { msg: $"Clusters path not found: ($clusters_path)" } error make { msg: $"Clusters path not found: ($clusters_path)" }
} }
# Find all cluster directories with KCL modules # Find all cluster directories with Nickel modules
ls $clusters_path ls $clusters_path
| where type == "dir" | where type == "dir"
| each { |dir| | each { |dir|
let cluster_name = ($dir.name | path basename) let cluster_name = ($dir.name | path basename)
let kcl_path = ($dir.name | path join "kcl") let schema_path = ($dir.name | path join "nickel")
let kcl_mod_path = ($kcl_path | path join "kcl.mod") let mod_path = ($schema_path | path join "nickel.mod")
if ($kcl_mod_path | path exists) { if ($mod_path | path exists) {
extract_cluster_metadata $cluster_name $kcl_path extract_cluster_metadata $cluster_name $schema_path
} }
} }
| compact | compact
| sort-by name | sort-by name
} }
# Extract metadata from a cluster's KCL module # Extract metadata from a cluster's Nickel module
def extract_cluster_metadata [name: string, kcl_path: string]: nothing -> record { def extract_cluster_metadata [name: string, schema_path: string] {
let kcl_mod_path = ($kcl_path | path join "kcl.mod") let mod_path = ($schema_path | path join "nickel.mod")
let mod_content = (open $kcl_mod_path | from toml) let mod_content = (open $mod_path | from toml)
# Find KCL schema files # Find Nickel schema files
let schema_files = (glob ($kcl_path | path join "*.k")) let schema_files = (glob ($schema_path | path join "*.ncl"))
let main_schema = ($schema_files | where ($it | str contains $name) | first | default "") let main_schema = ($schema_files | where ($it | str contains $name) | first | default "")
# Extract dependencies # Extract dependencies
@ -60,18 +60,18 @@ def extract_cluster_metadata [name: string, kcl_path: string]: nothing -> record
type: "cluster" type: "cluster"
cluster_type: $cluster_type cluster_type: $cluster_type
version: $mod_content.package.version version: $mod_content.package.version
kcl_path: $kcl_path schema_path: $schema_path
main_schema: $main_schema main_schema: $main_schema
dependencies: $dependencies dependencies: $dependencies
components: $components components: $components
description: $description description: $description
available: true available: true
last_updated: (ls $kcl_mod_path | get 0.modified) last_updated: (ls $mod_path | get 0.modified)
} }
} }
# Extract description from KCL schema file # Extract description from Nickel schema file
def extract_schema_description [schema_file: string]: nothing -> string { def extract_schema_description [schema_file: string] {
if not ($schema_file | path exists) { if not ($schema_file | path exists) {
return "" return ""
} }
@ -91,7 +91,7 @@ def extract_schema_description [schema_file: string]: nothing -> string {
} }
# Extract cluster components from schema # Extract cluster components from schema
def extract_cluster_components [schema_file: string]: nothing -> list<string> { def extract_cluster_components [schema_file: string] {
if not ($schema_file | path exists) { if not ($schema_file | path exists) {
return [] return []
} }
@ -116,7 +116,7 @@ def extract_cluster_components [schema_file: string]: nothing -> list<string> {
} }
# Determine cluster type based on components # Determine cluster type based on components
def determine_cluster_type [components: list<string>]: nothing -> string { def determine_cluster_type [components: list<string>] {
if ($components | any { |comp| $comp in ["buildkit", "registry", "docker"] }) { if ($components | any { |comp| $comp in ["buildkit", "registry", "docker"] }) {
"ci-cd" "ci-cd"
} else if ($components | any { |comp| $comp in ["prometheus", "grafana"] }) { } else if ($components | any { |comp| $comp in ["prometheus", "grafana"] }) {
@ -133,7 +133,7 @@ def determine_cluster_type [components: list<string>]: nothing -> string {
} }
# Search clusters by name, type, or components # Search clusters by name, type, or components
export def search-clusters [query: string]: nothing -> list<record> { export def search-clusters [query: string] {
discover-clusters discover-clusters
| where ( | where (
($it.name | str contains $query) or ($it.name | str contains $query) or
@ -144,7 +144,7 @@ export def search-clusters [query: string]: nothing -> list<record> {
} }
# Get specific cluster info # Get specific cluster info
export def get-cluster-info [name: string]: nothing -> record { export def get-cluster-info [name: string] {
let clusters = (discover-clusters) let clusters = (discover-clusters)
let found = ($clusters | where name == $name | first) let found = ($clusters | where name == $name | first)
@ -156,13 +156,13 @@ export def get-cluster-info [name: string]: nothing -> record {
} }
# List clusters by type # List clusters by type
export def list-clusters-by-type [type: string]: nothing -> list<record> { export def list-clusters-by-type [type: string] {
discover-clusters discover-clusters
| where cluster_type == $type | where cluster_type == $type
} }
# Validate cluster availability # Validate cluster availability
export def validate-clusters [names: list<string>]: nothing -> record { export def validate-clusters [names: list<string>] {
let available = (discover-clusters | get name) let available = (discover-clusters | get name)
let missing = ($names | where ($it not-in $available)) let missing = ($names | where ($it not-in $available))
let found = ($names | where ($it in $available)) let found = ($names | where ($it in $available))
@ -176,15 +176,15 @@ export def validate-clusters [names: list<string>]: nothing -> record {
} }
# Get clusters that use specific components # Get clusters that use specific components
export def find-clusters-with-component [component: string]: nothing -> list<record> { export def find-clusters-with-component [component: string] {
discover-clusters discover-clusters
| where ($it.components | any { |comp| $comp == $component }) | where ($it.components | any { |comp| $comp == $component })
} }
# List all available cluster types # List all available cluster types
export def list-cluster-types []: nothing -> list<string> { export def list-cluster-types [] {
discover-clusters discover-clusters
| get cluster_type | get cluster_type
| uniq | uniq
| sort | sort
} }

View file

@ -1,81 +1,84 @@
use lib_provisioning * # Selective imports replacing `use lib_provisioning *` (ADR-025 Phase 4).
#use ../lib_provisioning/utils/generate.nu * use lib_provisioning/utils/help.nu [parse_help_command]
use lib_provisioning/utils/init.nu [provisioning_init]
use lib_provisioning/utils/interface.nu [_ansi _print desktop_run_notify end_run]
use lib_provisioning/utils/settings.nu [find_get_settings]
use utils.nu * use utils.nu *
# Provider middleware now available through lib_provisioning # Provider middleware now available through lib_provisioning
# > Clusters services # > Clusters services
export def "main generate" [ export def "main generate" [
name?: string # Server hostname in settings name?: string # Server hostname in settings
...args # Args for generate command ...args # Args for generate command
--infra (-i): string # Infra directory --infra (-i): string # Infra directory
--settings (-s): string # Settings path --settings (-s): string # Settings path
--outfile (-o): string # Output file --outfile (-o): string # Output file
--cluster_pos (-p): int # Server position in settings --cluster_pos (-p): int # Server position in settings
--check (-c) # Only check mode no clusters will be generated --check (-c) # Only check mode no clusters will be generated
--wait (-w) # Wait clusters to be generated --wait (-w) # Wait clusters to be generated
--select: string # Select with task as option --select: string # Select with task as option
--debug (-x) # Use Debug mode --debug (-x) # Use Debug mode
--xm # Debug with PROVISIONING_METADATA --xm # Debug with PROVISIONING_METADATA
--xc # Debuc for task and services locally PROVISIONING_DEBUG_CHECK --xc # Debuc for task and services locally PROVISIONING_DEBUG_CHECK
--xr # Debug for remote clusters PROVISIONING_DEBUG_REMOTE --xr # Debug for remote clusters PROVISIONING_DEBUG_REMOTE
--xld # Log level with DEBUG PROVISIONING_LOG_LEVEL=debug --xld # Log level with DEBUG PROVISIONING_LOG_LEVEL=debug
--metadata # Error with metadata (-xm) --metadata # Error with metadata (-xm)
--notitles # not tittles --notitles # not tittles
--helpinfo (-h) # For more details use options "help" (no dashes) --helpinfo (-h) # For more details use options "help" (no dashes)
--out: string # Print Output format: json, yaml, text (default) --out: string # Print Output format: json, yaml, text (default)
]: nothing -> nothing { ] {
if ($out | is-not-empty) { if ($out | is-not-empty) {
$env.PROVISIONING_OUT = $out $env.PROVISIONING_OUT = $out
$env.PROVISIONING_NO_TERMINAL = true $env.PROVISIONING_NO_TERMINAL = true
} }
provisioning_init $helpinfo "cluster generate" $args provisioning_init $helpinfo "cluster generate" $args
#parse_help_command "cluster generate" $name --ismod --end #parse_help_command "cluster generate" $name --ismod --end
# print "on cluster main generate" # print "on cluster main generate"
if $debug { $env.PROVISIONING_DEBUG = true } if $debug { $env.PROVISIONING_DEBUG = true }
if $metadata { $env.PROVISIONING_METADATA = true } if $metadata { $env.PROVISIONING_METADATA = true }
# if $name != null and $name != "h" and $name != "help" { # if $name != null and $name != "h" and $name != "help" {
# let curr_settings = (find_get_settings --infra $infra --settings $settings) # let curr_settings = (find_get_settings --infra $infra --settings $settings)
# if ($curr_settings.data.clusters | find $name| length) == 0 { # if ($curr_settings.data.clusters | find $name| length) == 0 {
# _print $"🛑 invalid name ($name)" # _print $"🛑 invalid name ($name)"
# exit 1 # exit 1
# } # }
# } # }
let task = if ($args | length) > 0 { let task = if ($args | length) > 0 {
($args| get 0) ($args| get 0)
} else { } else {
let str_task = (($env.PROVISIONING_ARGS? | default "") | str replace "generate " " " ) let str_task = (($env.PROVISIONING_ARGS? | default "") | str replace "generate " " " )
let str_task = if $name != null { let str_task = if $name != null {
($str_task | str replace $name "") ($str_task | str replace $name "")
} else { } else {
$str_task $str_task
} }
($str_task | str trim | split row " " | first | default "" | split row "-" | first | default "" | str trim) ($str_task | str trim | split row " " | first | default "" | split row "-" | first | default "" | str trim)
} }
let other = if ($args | length) > 0 { ($args| skip 1) } else { "" } let other = if ($args | length) > 0 { ($args| skip 1) } else { "" }
let ops = $"($env.PROVISIONING_ARGS? | default "") " | str replace $"($task) " "" | str trim let ops = $"($env.PROVISIONING_ARGS? | default "") " | str replace $"($task) " "" | str trim
let run_generate = { let run_generate = {
let curr_settings = (find_get_settings --infra $infra --settings $settings) let curr_settings = (find_get_settings --infra $infra --settings $settings)
$env.WK_CNPROV = $curr_settings.wk_path $env.WK_CNPROV = $curr_settings.wk_path
let match_name = if $name == null or $name == "" { "" } else { $name} let match_name = if $name == null or $name == "" { "" } else { $name}
# on_clusters $curr_settings $check $wait $outfile $match_name $cluster_pos # on_clusters $curr_settings $check $wait $outfile $match_name $cluster_pos
} }
match $task { match $task {
"" if $name == "h" => { "" if $name == "h" => {
^$"($env.PROVISIONING_NAME)" -mod cluster generate help --notitles ^$"($env.PROVISIONING_NAME)" -mod cluster generate help --notitles
}, },
"" if $name == "help" => { "" if $name == "help" => {
^$"($env.PROVISIONING_NAME)" -mod cluster generate --help ^$"($env.PROVISIONING_NAME)" -mod cluster generate --help
print (provisioning_options "generate") print (provisioning_options "generate")
}, },
"" => { "" => {
let result = desktop_run_notify $"($env.PROVISIONING_NAME) clusters generate" "-> " $run_generate --timeout 11sec let result = desktop_run_notify $"($env.PROVISIONING_NAME) clusters generate" "-> " $run_generate --timeout 11sec
#do $run_generate #do $run_generate
}, },
_ => { _ => {
if $task != "" { print $"🛑 invalid_option ($task)" } if $task != "" { print $"🛑 invalid_option ($task)" }
print $"\nUse (_ansi blue_bold)($env.PROVISIONING_NAME) -h(_ansi reset) for help on commands and options" print $"\nUse (_ansi blue_bold)($env.PROVISIONING_NAME) -h(_ansi reset) for help on commands and options"
} }
} }
# "" | "generate" # "" | "generate"
if not $env.PROVISIONING_DEBUG { end_run "" } if not $env.PROVISIONING_DEBUG { end_run "" }
} }

View file

@ -1,81 +0,0 @@
use lib_provisioning *
#use ../lib_provisioning/utils/generate.nu *
use utils.nu *
# Provider middleware now available through lib_provisioning
# > Clusters services
export def "main generate" [
name?: string # Server hostname in settings
...args # Args for generate command
--infra (-i): string # Infra directory
--settings (-s): string # Settings path
--outfile (-o): string # Output file
--cluster_pos (-p): int # Server position in settings
--check (-c) # Only check mode no clusters will be generated
--wait (-w) # Wait clusters to be generated
--select: string # Select with task as option
--debug (-x) # Use Debug mode
--xm # Debug with PROVISIONING_METADATA
--xc # Debuc for task and services locally PROVISIONING_DEBUG_CHECK
--xr # Debug for remote clusters PROVISIONING_DEBUG_REMOTE
--xld # Log level with DEBUG PROVISIONING_LOG_LEVEL=debug
--metadata # Error with metadata (-xm)
--notitles # not tittles
--helpinfo (-h) # For more details use options "help" (no dashes)
--out: string # Print Output format: json, yaml, text (default)
]: nothing -> nothing {
if ($out | is-not-empty) {
$env.PROVISIONING_OUT = $out
$env.PROVISIONING_NO_TERMINAL = true
}
provisioning_init $helpinfo "cluster generate" $args
#parse_help_command "cluster generate" $name --ismod --end
# print "on cluster main generate"
if $debug { $env.PROVISIONING_DEBUG = true }
if $metadata { $env.PROVISIONING_METADATA = true }
# if $name != null and $name != "h" and $name != "help" {
# let curr_settings = (find_get_settings --infra $infra --settings $settings)
# if ($curr_settings.data.clusters | find $name| length) == 0 {
# _print $"🛑 invalid name ($name)"
# exit 1
# }
# }
let task = if ($args | length) > 0 {
($args| get 0)
} else {
let str_task = (($env.PROVISIONING_ARGS? | default "") | str replace "generate " " " )
let str_task = if $name != null {
($str_task | str replace $name "")
} else {
$str_task
}
( | str trim | split row " " | first | default "" | split row "-" | first | default "" | str trim)
}
let other = if ($args | length) > 0 { ($args| skip 1) } else { "" }
let ops = $"($env.PROVISIONING_ARGS? | default "") " | str replace $"($task) " "" | str trim
let run_generate = {
let curr_settings = (find_get_settings --infra $infra --settings $settings)
$env.WK_CNPROV = $curr_settings.wk_path
let match_name = if $name == null or $name == "" { "" } else { $name}
# on_clusters $curr_settings $check $wait $outfile $match_name $cluster_pos
}
match $task {
"" if $name == "h" => {
^$"($env.PROVISIONING_NAME)" -mod cluster generate help --notitles
},
"" if $name == "help" => {
^$"($env.PROVISIONING_NAME)" -mod cluster generate --help
print (provisioning_options "generate")
},
"" => {
let result = desktop_run_notify $"($env.PROVISIONING_NAME) clusters generate" "-> " $run_generate --timeout 11sec
#do $run_generate
},
_ => {
if $task != "" { print $"🛑 invalid_option ($task)" }
print $"\nUse (_ansi blue_bold)($env.PROVISIONING_NAME) -h(_ansi reset) for help on commands and options"
}
}
# "" | "generate"
if not $env.PROVISIONING_DEBUG { end_run "" }
}

View file

@ -1,122 +1,203 @@
use utils.nu servers_selector # Selective imports replacing `use lib_provisioning *` (ADR-025 Phase 4).
use ../lib_provisioning/config/accessor.nu * use lib_provisioning/config/accessor/functions.nu [get-run-taskservs-path get-taskservs-path]
use lib_provisioning/utils/hints.nu [show-next-step]
use lib_provisioning/utils/interface.nu [_ansi _print]
use lib_provisioning/utils/logging.nu [is-debug-check-enabled is-debug-enabled]
use lib_provisioning/utils/settings.nu [load]
use utils.nu *
use run.nu *
use check_mode.nu *
use ../lib_provisioning/utils/hints.nu *
#use clusters/run.nu run_cluster #use ../extensions/taskservs/run.nu run_taskserv
def install_from_server [ def install_from_server [
defs: record defs: record
server_cluster_path: string server_taskserv_path: string
wk_server: string wk_server: string
]: nothing -> bool { ] {
_print $"($defs.cluster.name) on ($defs.server.hostname) install (_ansi purple_bold)from ($defs.cluster_install_mode)(_ansi reset)" _print (
run_cluster $defs ((get-run-clusters-path) | path join $defs.cluster.name | path join $server_cluster_path) $"(_ansi yellow_bold)($defs.taskserv.name)(_ansi reset) (_ansi default_dimmed)on(_ansi reset) " +
($wk_server | path join $defs.cluster.name) $"($defs.server.hostname) (_ansi default_dimmed)install(_ansi reset) " +
$"(_ansi purple_bold)from ($defs.taskserv_install_mode)(_ansi reset)"
)
let run_taskservs_path = (get-run-taskservs-path)
(run_taskserv $defs
($run_taskservs_path | path join $defs.taskserv.name | path join $server_taskserv_path)
($wk_server | path join $defs.taskserv.name)
)
} }
def install_from_library [ def install_from_library [
defs: record defs: record
server_cluster_path: string server_taskserv_path: string
wk_server: string wk_server: string
]: nothing -> bool { ] {
_print $"($defs.cluster.name) on ($defs.server.hostname) installed (_ansi purple_bold)from library(_ansi reset)" _print (
run_cluster $defs ((get-clusters-path) |path join $defs.cluster.name | path join $defs.cluster_profile) $"(_ansi yellow_bold)($defs.taskserv.name)(_ansi reset) (_ansi default_dimmed)on(_ansi reset) " +
($wk_server | path join $defs.cluster.name) $"($defs.server.hostname) (_ansi default_dimmed)install(_ansi reset) " +
$"(_ansi purple_bold)from library(_ansi reset)"
)
let base = (get-taskservs-path)
let name = $defs.taskserv.name
# Resolve the script directory with profile → mode fallback chain:
# 1. Exact profile name (e.g. "k0sctl")
# 2. "taskserv" (canonical mode dir — was "default/" pre-migration)
# 3. Error with actionable message
let profile = $defs.taskserv_profile
let by_profile = ($base | path join $name | path join $profile)
let by_taskserv = ($base | path join $name | path join "taskserv")
let lib_path = if ($by_profile | path exists) {
$by_profile
} else if ($by_taskserv | path exists) {
if $profile != "default" {
_print $"(_ansi yellow)⚠ profile '($profile)' not found for ($name), falling back to taskserv/(_ansi reset)"
}
$by_taskserv
} else {
error make { msg: $"No script directory for component '($name)': tried ($by_profile) and ($by_taskserv)" }
}
( run_taskserv $defs $lib_path ($wk_server | path join $name) )
} }
export def on_clusters [ export def on_taskservs [
settings: record settings: record
match_cluster: string match_taskserv: string
match_server: string match_taskserv_profile: string
match_server: string
iptype: string iptype: string
check: bool check: bool
]: nothing -> bool { ] {
# use ../../../providers/prov_lib/middleware.nu mw_get_ip _print $"Running (_ansi yellow_bold)taskservs(_ansi reset) ..."
_print $"Running (_ansi yellow_bold)clusters(_ansi reset) ..." let provisioning_sops = ($env.PROVISIONING_SOPS? | default "")
if (get-provisioning-use-sops) == "" { if $provisioning_sops == "" {
# A SOPS load env # A SOPS load env
$env.CURRENT_INFRA_PATH = $"($settings.infra_path)/($settings.infra)" $env.CURRENT_INFRA_PATH = ($settings.infra_path | path join $settings.infra)
use sops_env.nu use ../sops_env.nu
} }
let ip_type = if $iptype == "" { "public" } else { $iptype } let ip_type = if $iptype == "" { "public" } else { $iptype }
mut server_pos = -1 let str_created_taskservs_dirpath = ( $settings.data.created_taskservs_dirpath | default (["/tmp"] | path join) |
mut cluster_pos = -1 str replace "./" $"($settings.src_path)/" | str replace "~" $env.HOME | str replace "NOW" $env.NOW
mut curr_cluster = 0
let created_clusters_dirpath = ( $settings.data.created_clusters_dirpath | default "/tmp" |
str replace "./" $"($settings.src_path)/" | str replace "~" $env.HOME | str replace "NOW" $env.NOW
) )
let root_wk_server = ($created_clusters_dirpath | path join "on-server") let created_taskservs_dirpath = if ($str_created_taskservs_dirpath | str starts-with "/" ) { $str_created_taskservs_dirpath } else { $settings.src_path | path join $str_created_taskservs_dirpath }
let root_wk_server = ($created_taskservs_dirpath | path join "on-server")
if not ($root_wk_server | path exists ) { ^mkdir "-p" $root_wk_server } if not ($root_wk_server | path exists ) { ^mkdir "-p" $root_wk_server }
let dflt_clean_created_clusters = ($settings.data.defaults_servers.clean_created_clusters? | default $created_clusters_dirpath | let dflt_clean_created_taskservs = ($settings.data.clean_created_taskservs? | default $created_taskservs_dirpath |
str replace "./" $"($settings.src_path)/" | str replace "~" $env.HOME str replace "./" $"($settings.src_path)/" | str replace "~" $env.HOME
) )
let run_ops = if (is-debug-enabled) { "bash -x" } else { "" } let run_ops = if (is-debug-enabled) { "bash -x" } else { "" }
for srvr in $settings.data.servers { $settings.data.servers
# continue | enumerate
_print $"on (_ansi green_bold)($srvr.hostname)(_ansi reset) ..." | where {|it|
$server_pos += 1 $match_server == "" or $it.item.hostname == $match_server
$cluster_pos = -1 }
_print $"On server ($srvr.hostname) pos ($server_pos) ..." | each {|it|
if $match_server != "" and $srvr.hostname != $match_server { continue } let server_pos = $it.index
let clean_created_clusters = (($settings.data.servers | try { get $server_pos).clean_created_clusters? } catch { $dflt_clean_created_clusters ) } let srvr = $it.item
let ip = if (is-debug-check-enabled) { _print $"on (_ansi green_bold)($srvr.hostname)(_ansi reset) pos ($server_pos) ..."
let clean_created_taskservs = ($settings.data.servers | get $server_pos? | default $dflt_clean_created_taskservs)
# Determine IP address
let ip = if (is-debug-check-enabled) or $check {
"127.0.0.1" "127.0.0.1"
} else { } else {
let curr_ip = (mw_get_ip $settings $srvr $ip_type false | default "") let curr_ip = (mw_get_ip $settings $srvr $ip_type false | default "")
if $curr_ip == "" { if $curr_ip == "" {
_print $"🛑 No IP ($ip_type) found for (_ansi green_bold)($srvr.hostname)(_ansi reset) ($server_pos) " _print $"🛑 No IP ($ip_type) found for (_ansi green_bold)($srvr.hostname)(_ansi reset) ($server_pos) "
continue null
} else {
let network_public_ip = ($srvr | get network_public_ip? | default "")
if ($network_public_ip | is-not-empty) and $network_public_ip != $curr_ip {
_print $"🛑 IP ($network_public_ip) not equal to ($curr_ip) in (_ansi green_bold)($srvr.hostname)(_ansi reset)"
}
# Check if server is in running state
if not (wait_for_server $server_pos $srvr $settings $curr_ip) {
_print $"🛑 server ($srvr.hostname) ($curr_ip) (_ansi red_bold)not in running state(_ansi reset)"
null
} else {
$curr_ip
}
} }
#use utils.nu wait_for_server
if not (wait_for_server $server_pos $srvr $settings $curr_ip) {
print $"🛑 server ($srvr.hostname) ($curr_ip) (_ansi red_bold)not in running state(_ansi reset)"
continue
}
$curr_ip
} }
# Process server only if we have valid IP
if ($ip != null) {
let server = ($srvr | merge { ip_addresses: { pub: $ip, priv: $srvr.network_private_ip }}) let server = ($srvr | merge { ip_addresses: { pub: $ip, priv: $srvr.network_private_ip }})
let wk_server = ($root_wk_server | path join $server.hostname) let wk_server = ($root_wk_server | path join $server.hostname)
if ($wk_server | path exists ) { rm -rf $wk_server } if ($wk_server | path exists ) { rm -rf $wk_server }
^mkdir "-p" $wk_server ^mkdir "-p" $wk_server
for cluster in $server.clusters { $server.taskservs
$cluster_pos += 1 | enumerate
if $cluster_pos > $curr_cluster { break } | where {|it|
$curr_cluster += 1 let taskserv = $it.item
if $match_cluster != "" and $match_cluster != $cluster.name { continue } let matches_taskserv = ($match_taskserv == "" or $match_taskserv == $taskserv.name)
if not ((get-clusters-path) | path join $cluster.name | path exists) { let matches_profile = ($match_taskserv_profile == "" or $match_taskserv_profile == $taskserv.profile)
print $"cluster path: ((get-clusters-path) | path join $cluster.name) (_ansi red_bold)not found(_ansi reset)" $matches_taskserv and $matches_profile
continue }
} | each {|it|
if not ($wk_server | path join $cluster.name| path exists) { ^mkdir "-p" ($wk_server | path join $cluster.name) } let taskserv = $it.item
let $cluster_profile = if $cluster.profile == "" { "default" } else { $cluster.profile } let taskserv_pos = $it.index
let $cluster_install_mode = if $cluster.install_mode == "" { "library" } else { $cluster.install_mode } let taskservs_path = (get-taskservs-path)
let server_cluster_path = ($server.hostname | path join $cluster_profile)
let defs = { # Check if taskserv path exists - skip if not found
settings: $settings, server: $server, cluster: $cluster, if not ($taskservs_path | path join $taskserv.name | path exists) {
cluster_install_mode: $cluster_install_mode, cluster_profile: $cluster_profile, _print $"taskserv path: ($taskservs_path | path join $taskserv.name) (_ansi red_bold)not found(_ansi reset)"
pos: { server: $"($server_pos)", cluster: $cluster_pos}, ip: $ip } } else {
match $cluster.install_mode { # Taskserv path exists, proceed with processing
"server" | "getfile" => { if not ($wk_server | path join $taskserv.name| path exists) { ^mkdir "-p" ($wk_server | path join $taskserv.name) }
(install_from_server $defs $server_cluster_path $wk_server ) let $taskserv_profile = if $taskserv.profile == "" { "default" } else { $taskserv.profile }
}, let $taskserv_install_mode = if $taskserv.install_mode == "" { "library" } else { $taskserv.install_mode }
"library-server" => { let server_taskserv_path = ($server.hostname | path join $taskserv_profile)
(install_from_library $defs $server_cluster_path $wk_server) let defs = {
(install_from_server $defs $server_cluster_path $wk_server ) settings: $settings, server: $server, taskserv: $taskserv,
}, taskserv_install_mode: $taskserv_install_mode, taskserv_profile: $taskserv_profile,
"server-library" => { pos: { server: $"($server_pos)", taskserv: $taskserv_pos}, ip: $ip, check: $check }
(install_from_server $defs $server_cluster_path $wk_server )
(install_from_library $defs $server_cluster_path $wk_server) # Enhanced check mode
}, if $check {
"library" => { let check_result = (run-check-mode $taskserv.name $taskserv_profile $settings $server --verbose=(is-debug-enabled))
(install_from_library $defs $server_cluster_path $wk_server) if $check_result.overall_valid {
}, # Check passed, proceed (no action needed, validation was successful)
} } else {
if $clean_created_clusters == "yes" { rm -rf ($wk_server | pth join $cluster.name) } _print $"(_ansi red)⊘ Skipping deployment due to validation errors(_ansi reset)"
}
} else {
# Normal installation mode
match $taskserv.install_mode {
"server" | "getfile" => {
(install_from_server $defs $server_taskserv_path $wk_server )
},
"library-server" => {
(install_from_library $defs $server_taskserv_path $wk_server)
(install_from_server $defs $server_taskserv_path $wk_server )
},
"server-library" => {
(install_from_server $defs $server_taskserv_path $wk_server )
(install_from_library $defs $server_taskserv_path $wk_server)
},
"library" => {
(install_from_library $defs $server_taskserv_path $wk_server)
},
}
}
if $clean_created_taskservs == "yes" { rm -rf ($wk_server | pth join $taskserv.name) }
}
}
if $clean_created_taskservs == "yes" { rm -rf $wk_server }
_print $"Tasks completed on ($server.hostname)"
} }
if $clean_created_clusters == "yes" { rm -rf $wk_server }
print $"Clusters completed on ($server.hostname)"
} }
if ("/tmp/k8s_join.sh" | path exists) { cp "/tmp/k8s_join.sh" $root_wk_server ; rm -r /tmp/k8s_join.sh } if ("/tmp/k8s_join.sh" | path exists) { cp "/tmp/k8s_join.sh" $root_wk_server ; rm -r /tmp/k8s_join.sh }
if $dflt_clean_created_clusters == "yes" { rm -rf $root_wk_server } if $dflt_clean_created_taskservs == "yes" { rm -rf $root_wk_server }
print $"✅ Clusters (_ansi green_bold)completed(_ansi reset) ....." _print $"✅ Tasks (_ansi green_bold)completed(_ansi reset) ($match_server) ($match_taskserv) ($match_taskserv_profile) ....."
#use utils.nu servers_selector if not $check and ($match_server | is-empty) {
servers_selector $settings $ip_type false #use utils.nu servers_selector
servers_selector $settings $ip_type false
}
# Show next-step hints after successful taskserv installation
if not $check and ($match_taskserv | is-not-empty) {
show-next-step "taskserv_create" {name: $match_taskserv}
}
true true
} }

View file

@ -12,7 +12,7 @@ export def load-clusters [
clusters: list<string>, clusters: list<string>,
--force = false # Overwrite existing --force = false # Overwrite existing
--level: string = "auto" # "workspace", "infra", or "auto" --level: string = "auto" # "workspace", "infra", or "auto"
]: nothing -> record { ] {
# Determine target layer # Determine target layer
let layer_info = (determine-layer --workspace $target_path --infra $target_path --level $level) let layer_info = (determine-layer --workspace $target_path --infra $target_path --level $level)
let load_path = $layer_info.path let load_path = $layer_info.path
@ -55,7 +55,7 @@ export def load-clusters [
} }
# Load a single cluster # Load a single cluster
def load-single-cluster [target_path: string, name: string, force: bool, layer: string]: nothing -> record { def load-single-cluster [target_path: string, name: string, force: bool, layer: string] {
let result = (do { let result = (do {
let cluster_info = (get-cluster-info $name) let cluster_info = (get-cluster-info $name)
let target_dir = ($target_path | path join ".clusters" $name) let target_dir = ($target_path | path join ".clusters" $name)
@ -70,8 +70,8 @@ def load-single-cluster [target_path: string, name: string, force: bool, layer:
} }
} }
# Copy KCL files and directories # Copy Nickel files and directories
cp -r $cluster_info.kcl_path $target_dir cp -r $cluster_info.schema_path $target_dir
print $"✅ Loaded cluster: ($name) (type: ($cluster_info.cluster_type))" print $"✅ Loaded cluster: ($name) (type: ($cluster_info.cluster_type))"
{ {
@ -96,12 +96,12 @@ def load-single-cluster [target_path: string, name: string, force: bool, layer:
} }
} }
# Generate clusters.k import file # Generate clusters.ncl import file
def generate-clusters-imports [target_path: string, clusters: list<string>, layer: string] { def generate-clusters-imports [target_path: string, clusters: list<string>, layer: string] {
# Generate individual imports for each cluster # Generate individual imports for each cluster
let imports = ($clusters | each { |name| let imports = ($clusters | each { |name|
# Check if the cluster main file exists # Check if the cluster main file exists
let main_file = ($target_path | path join ".clusters" $name ($name + ".k")) let main_file = ($target_path | path join ".clusters" $name ($name + ".ncl"))
if ($main_file | path exists) { if ($main_file | path exists) {
$"import .clusters.($name).($name) as ($name)_cluster" $"import .clusters.($name).($name) as ($name)_cluster"
} else { } else {
@ -130,7 +130,7 @@ clusters = {
clusters" clusters"
# Save the imports file # Save the imports file
$content | save -f ($target_path | path join "clusters.k") $content | save -f ($target_path | path join "clusters.ncl")
# Also create individual alias files for easier direct imports # Also create individual alias files for easier direct imports
for $name in $clusters { for $name in $clusters {
@ -142,7 +142,7 @@ import .clusters.($name) as ($name)
# Re-export for convenience # Re-export for convenience
($name)" ($name)"
$alias_content | save -f ($target_path | path join $"cluster_($name).k") $alias_content | save -f ($target_path | path join $"cluster_($name).ncl")
} }
} }
@ -166,7 +166,7 @@ def update-clusters-manifest [target_path: string, clusters: list<string>, layer
components: $info.components components: $info.components
layer: $layer layer: $layer
loaded_at: (date now | format date '%Y-%m-%d %H:%M:%S') loaded_at: (date now | format date '%Y-%m-%d %H:%M:%S')
source_path: $info.kcl_path source_path: $info.schema_path
} }
}) })
@ -181,7 +181,7 @@ def update-clusters-manifest [target_path: string, clusters: list<string>, layer
} }
# Remove cluster from workspace # Remove cluster from workspace
export def unload-cluster [workspace: string, name: string]: nothing -> record { export def unload-cluster [workspace: string, name: string] {
let target_dir = ($workspace | path join ".clusters" $name) let target_dir = ($workspace | path join ".clusters" $name)
if not ($target_dir | path exists) { if not ($target_dir | path exists) {
@ -198,7 +198,7 @@ export def unload-cluster [workspace: string, name: string]: nothing -> record {
if ($updated_clusters | is-empty) { if ($updated_clusters | is-empty) {
rm $manifest_path rm $manifest_path
rm ($workspace | path join "clusters.k") rm ($workspace | path join "clusters.ncl")
} else { } else {
let updated_manifest = ($manifest | update loaded_clusters $updated_clusters) let updated_manifest = ($manifest | update loaded_clusters $updated_clusters)
$updated_manifest | to yaml | save $manifest_path $updated_manifest | to yaml | save $manifest_path
@ -220,7 +220,7 @@ export def unload-cluster [workspace: string, name: string]: nothing -> record {
} }
# List loaded clusters in workspace # List loaded clusters in workspace
export def list-loaded-clusters [workspace: string]: nothing -> list<record> { export def list-loaded-clusters [workspace: string] {
let manifest_path = ($workspace | path join "clusters.manifest.yaml") let manifest_path = ($workspace | path join "clusters.manifest.yaml")
if not ($manifest_path | path exists) { if not ($manifest_path | path exists) {
@ -236,7 +236,7 @@ export def clone-cluster [
workspace: string, workspace: string,
source_name: string, source_name: string,
target_name: string target_name: string
]: nothing -> record { ] {
# Check if source cluster is loaded # Check if source cluster is loaded
let loaded = (list-loaded-clusters $workspace) let loaded = (list-loaded-clusters $workspace)
let source_loaded = ($loaded | where name == $source_name | length) > 0 let source_loaded = ($loaded | where name == $source_name | length) > 0
@ -256,7 +256,7 @@ export def clone-cluster [
cp -r $source_dir $target_dir cp -r $source_dir $target_dir
# Update cluster name in schema files # Update cluster name in schema files
let schema_files = (ls ($target_dir | path join "*.k") | get name) let schema_files = (ls ($target_dir | path join "*.ncl") | get name)
for $file in $schema_files { for $file in $schema_files {
let content = (open $file) let content = (open $file)
let updated = ($content | str replace $source_name $target_name) let updated = ($content | str replace $source_name $target_name)
@ -280,4 +280,4 @@ export def clone-cluster [
status: "cloned" status: "cloned"
workspace: $workspace workspace: $workspace
} }
} }

View file

@ -2,9 +2,9 @@ use ../lib_provisioning/config/accessor.nu *
export def provisioning_options [ export def provisioning_options [
source: string source: string
]: nothing -> string { ] {
let provisioning_name = (get-provisioning-name) let provisioning_name = (get-provisioning-name)
let provisioning_path = (get-base-path) let provisioning_path = (get-config-base-path)
let provisioning_url = (get-provisioning-url) let provisioning_url = (get-provisioning-url)
( (

View file

@ -1,19 +1,24 @@
#use utils.nu cluster_get_file
#use utils/templates.nu on_template_path
use std use std
use ../lib_provisioning/config/accessor.nu [is-debug-enabled, is-debug-check-enabled] use ../lib_provisioning/config/accessor.nu *
#use utils.nu taskserv_get_file
#use utils/templates.nu on_template_path
def make_cmd_env_temp [ def make_cmd_env_temp [
defs: record defs: record
cluster_env_path: string taskserv_env_path: string
wk_vars: string wk_vars: string
]: nothing -> string { ] {
let cmd_env_temp = $"($cluster_env_path)/cmd_env_(mktemp --tmpdir-path $cluster_env_path --suffix ".sh" | path basename)" let cmd_env_temp = $"($taskserv_env_path | path join "cmd_env")_(mktemp --tmpdir-path $taskserv_env_path --suffix ".sh" | path basename)"
# export all 'PROVISIONING_' $env vars to SHELL ($"export PROVISIONING_VARS=($wk_vars)\nexport PROVISIONING_DEBUG=((is-debug-enabled))\n" +
($"export NU_LOG_LEVEL=($env.NU_LOG_LEVEL)\n" + $"export NU_LOG_LEVEL=($env.NU_LOG_LEVEL)\n" +
($env | items {|key, value| if ($key | str starts-with "PROVISIONING_") {echo $'export ($key)="($value)"\n'} } | compact --empty | to text) $"export PROVISIONING_RESOURCES=((get-provisioning-resources))\n" +
$"export PROVISIONING_SETTINGS_SRC=($defs.settings.src)\nexport PROVISIONING_SETTINGS_SRC_PATH=($defs.settings.src_path)\n" +
$"export PROVISIONING_KLOUD=($defs.settings.infra)\nexport PROVISIONING_KLOUD_PATH=($defs.settings.infra_path)\n" +
$"export PROVISIONING_USE_SOPS=((get-provisioning-use-sops))\nexport PROVISIONING_WK_ENV_PATH=($taskserv_env_path)\n" +
$"export SOPS_AGE_KEY_FILE=($env.SOPS_AGE_KEY_FILE)\nexport PROVISIONING_KAGE=($env.PROVISIONING_KAGE)\n" +
$"export SOPS_AGE_RECIPIENTS=($env.SOPS_AGE_RECIPIENTS)\n"
) | save --force $cmd_env_temp ) | save --force $cmd_env_temp
if (is-debug-enabled) { _print $"cmd_env_temp: ($cmd_env_temp)" }
$cmd_env_temp $cmd_env_temp
} }
def run_cmd [ def run_cmd [
@ -21,175 +26,239 @@ def run_cmd [
title: string title: string
where: string where: string
defs: record defs: record
cluster_env_path: string taskserv_env_path: string
wk_vars: string wk_vars: string
]: nothing -> nothing { ] {
_print $"($title) for ($defs.cluster.name) on ($defs.server.hostname) ($defs.pos.server) ..." _print (
if $defs.check { return } $"($title) for (_ansi yellow_bold)($defs.taskserv.name)(_ansi reset) (_ansi default_dimmed)on(_ansi reset) " +
let runner = (grep "^#!" $"($cluster_env_path)/($cmd_name)" | str trim) $"($defs.server.hostname) ($defs.pos.server) ..."
)
let runner = (grep "^#!" ($taskserv_env_path | path join $cmd_name) | str trim)
let run_ops = if (is-debug-enabled) { if ($runner | str contains "bash" ) { "-x" } else { "" } } else { "" } let run_ops = if (is-debug-enabled) { if ($runner | str contains "bash" ) { "-x" } else { "" } } else { "" }
let cmd_env_temp = make_cmd_env_temp $defs $cluster_env_path $wk_vars let cmd_run_file = make_cmd_env_temp $defs $taskserv_env_path $wk_vars
if ($wk_vars | path exists) { if ($cmd_run_file | path exists) and ($wk_vars | path exists) {
let run_res = if ($runner | str ends-with "bash" ) { if ($runner | str ends-with "bash" ) {
(^bash -c $"'source ($cmd_env_temp) ; bash ($run_ops) ($cluster_env_path)/($cmd_name) ($wk_vars) ($defs.pos.server) ($defs.pos.cluster) (^pwd)'" | complete) $"($run_ops) ($taskserv_env_path | path join $cmd_name) ($wk_vars) ($defs.pos.server) ($defs.pos.taskserv) (^pwd)" | save --append $cmd_run_file
} else if ($runner | str ends-with "nu" ) { } else if ($runner | str ends-with "nu" ) {
(^bash -c $"'source ($cmd_env_temp); ($env.NU) ($env.NU_ARGS) ($cluster_env_path)/($cmd_name)'" | complete) $"($env.NU) ($env.NU_ARGS) ($taskserv_env_path | path join $cmd_name)" | save --append $cmd_run_file
} else { } else {
(^bash -c $"'source ($cmd_env_temp); ($cluster_env_path)/($cmd_name) ($wk_vars)'" | complete) $"($taskserv_env_path | path join $cmd_name) ($wk_vars)" | save --append $cmd_run_file
} }
rm -f $cmd_env_temp let run_res = (^bash $cmd_run_file | complete)
if $run_res.exit_code != 0 { if $run_res.exit_code != 0 {
(throw-error $"🛑 Error server ($defs.server.hostname) cluster ($defs.cluster.name) (throw-error $"🛑 Error server ($defs.server.hostname) taskserv ($defs.taskserv.name)
($cluster_env_path)/($cmd_name) with ($wk_vars) ($defs.pos.server) ($defs.pos.cluster) (^pwd)" ($taskserv_env_path)/($cmd_name) with ($wk_vars) ($defs.pos.server) ($defs.pos.taskserv) (^pwd)"
$run_res.stdout $"($run_res.stdout)\n($run_res.stderr)\n"
$where --span (metadata $run_res).span) $where --span (metadata $run_res).span)
exit 1 exit 1
} }
if not (is-debug-enabled) { rm -f $"($cluster_env_path)/prepare" } if (is-debug-enabled) {
if ($run_res.stdout | is-not-empty) { _print $"($run_res.stdout)" }
if ($run_res.stderr | is-not-empty) { _print $"($run_res.stderr)" }
} else {
rm -f $cmd_run_file
rm -f ($taskserv_env_path | path join "prepare")
}
} }
} }
export def run_cluster_library [ export def run_taskserv_library [
defs: record defs: record
cluster_path: string taskserv_path: string
cluster_env_path: string taskserv_env_path: string
wk_vars: string wk_vars: string
]: nothing -> bool { ] {
if not ($cluster_path | path exists) { return false }
if not ($taskserv_path | path exists) { return false }
let prov_resources_path = ($defs.settings.data.prov_resources_path | default "" | str replace "~" $env.HOME) let prov_resources_path = ($defs.settings.data.prov_resources_path | default "" | str replace "~" $env.HOME)
let cluster_server_name = $defs.server.hostname let taskserv_server_name = $defs.server.hostname
rm -rf ($cluster_env_path | path join "*.k") ($cluster_env_path | path join "kcl") rm -rf ...(glob ($taskserv_env_path | path join "*.ncl")) ($taskserv_env_path |path join "nickel")
mkdir ($cluster_env_path | path join "kcl") mkdir ($taskserv_env_path | path join "nickel")
let err_out = ($cluster_env_path | path join (mktemp --tmpdir-path $cluster_env_path --suffix ".err") | path basename) let err_out = ($taskserv_env_path | path join (mktemp --tmpdir-path $taskserv_env_path --suffix ".err" | path basename))
let kcl_temp = ($cluster_env_path | path join "kcl" | path join (mktemp --tmpdir-path $cluster_env_path --suffix ".k" ) | path basename) let nickel_temp = ($taskserv_env_path | path join "nickel"| path join (mktemp --tmpdir-path $taskserv_env_path --suffix ".ncl" | path basename))
let wk_format = if $env.PROVISIONING_WK_FORMAT == "json" { "json" } else { "yaml" } let wk_format = if (get-provisioning-wk-format) == "json" { "json" } else { "yaml" }
let wk_data = { defs: $defs.settings.data, pos: $defs.pos, server: $defs.server }
# Resolve floating_ip name → actual IP from provisioning state so taskserv
# templates can use server.floating_ip_address without hardcoding.
let fip_name = ($defs.server | get -o floating_ip | default "")
let resolved_fip_address = if ($fip_name | is-not-empty) {
let state_path = ($defs.settings.infra_path | path dirname | path dirname | path join ".provisioning-state.json")
if ($state_path | path exists) {
let fips = (open $state_path | get -o bootstrap.floating_ips | default {})
# FIP names are stored with hyphens converted to underscores as keys
# e.g. "librecloud-fip-sgoyol-ingress" → key "sgoyol_ingress" (strip prefix, replace hyphens)
let fip_key = ($fip_name | str replace --regex '^librecloud-fip-' '' | str replace --all '-' '_')
$fips | get -o $fip_key | default {} | get -o ip | default ""
} else { "" }
} else { "" }
let server_ctx = if ($resolved_fip_address | is-not-empty) {
$defs.server | merge { floating_ip_address: $resolved_fip_address }
} else {
$defs.server
}
let wk_data = { # providers: $defs.settings.providers,
defs: $defs.settings.data,
pos: $defs.pos,
server: $server_ctx
}
if $wk_format == "json" { if $wk_format == "json" {
$wk_data | to json | save --force $wk_vars $wk_data | to json | save --force $wk_vars
} else { } else {
$wk_data | to yaml | save --force $wk_vars $wk_data | to yaml | save --force $wk_vars
} }
if $env.PROVISIONING_USE_KCL { if (get-use-nickel) {
cd ($defs.settings.infra_path | path join $defs.settings.infra) cd ($defs.settings.infra_path | path join $defs.settings.infra)
let kcl_cluster_path = if ($cluster_path | path join "kcl"| path join $"($defs.cluster.name).k" | path exists) { if ($nickel_temp | path exists) { rm -f $nickel_temp }
($cluster_path | path join "kcl"| path join $"($defs.cluster.name).k") let res = (^nickel import -m $wk_format $wk_vars -o $nickel_temp | complete)
} else if (($cluster_path | path dirname) | path join "kcl"| path join $"($defs.cluster.name).k" | path exists) {
(($cluster_path | path dirname) | path join "kcl"| path join $"($defs.cluster.name).k")
} else { "" }
if ($kcl_temp | path exists) { rm -f $kcl_temp }
let res = (^kcl import -m $wk_format $wk_vars -o $kcl_temp | complete)
if $res.exit_code != 0 { if $res.exit_code != 0 {
print $"❗KCL import (_ansi red_bold)($wk_vars)(_ansi reset) Errors found " _print $"❗Nickel import (_ansi red_bold)($wk_vars)(_ansi reset) Errors found "
print $res.stdout _print $res.stdout
rm -f $kcl_temp rm -f $nickel_temp
cd $env.PWD cd $env.PWD
return false return false
} }
# Very important! Remove external block for import and re-format it # Very important! Remove external block for import and re-format it
# ^sed -i "s/^{//;s/^}//" $kcl_temp # ^sed -i "s/^{//;s/^}//" $nickel_temp
open $kcl_temp -r | lines | find -v --regex "^{" | find -v --regex "^}" | save -f $kcl_temp open $nickel_temp -r | lines | find -v --regex "^{" | find -v --regex "^}" | save -f $nickel_temp
^kcl fmt $kcl_temp let res = (^nickel fmt $nickel_temp | complete)
if $kcl_cluster_path != "" and ($kcl_cluster_path | path exists) { cat $kcl_cluster_path | save --append $kcl_temp } let nickel_taskserv_path = if ($taskserv_path | path join "nickel"| path join $"($defs.taskserv.name).ncl" | path exists) {
# } else { print $"❗ No cluster kcl ($defs.cluster.k) path found " ; return false } ($taskserv_path | path join "nickel"| path join $"($defs.taskserv.name).ncl")
if $env.PROVISIONING_KEYS_PATH != "" { } else if ($taskserv_path | path dirname | path join "nickel"| path join $"($defs.taskserv.name).ncl" | path exists) {
($taskserv_path | path dirname | path join "nickel"| path join $"($defs.taskserv.name).ncl")
} else if ($taskserv_path | path dirname | path join "default" | path join "nickel"| path join $"($defs.taskserv.name).ncl" | path exists) {
($taskserv_path | path dirname | path join "default" | path join "nickel"| path join $"($defs.taskserv.name).ncl")
} else { "" }
if $nickel_taskserv_path != "" and ($nickel_taskserv_path | path exists) {
if (is-debug-enabled) {
_print $"adding task name: ($defs.taskserv.name) -> ($nickel_taskserv_path)"
}
cat $nickel_taskserv_path | save --append $nickel_temp
}
let nickel_taskserv_profile_path = if ($taskserv_path | path join "nickel"| path join $"($defs.taskserv.profile).ncl" | path exists) {
($taskserv_path | path join "nickel"| path join $"($defs.taskserv.profile).ncl")
} else if ($taskserv_path | path dirname | path join "nickel"| path join $"($defs.taskserv.profile).ncl" | path exists) {
($taskserv_path | path dirname | path join "nickel"| path join $"($defs.taskserv.profile).ncl")
} else if ($taskserv_path | path dirname | path join "default" | path join "nickel"| path join $"($defs.taskserv.profile).ncl" | path exists) {
($taskserv_path | path dirname | path join "default" | path join "nickel"| path join $"($defs.taskserv.profile).ncl")
} else { "" }
if $nickel_taskserv_profile_path != "" and ($nickel_taskserv_profile_path | path exists) {
if (is-debug-enabled) {
_print $"adding task profile: ($defs.taskserv.profile) -> ($nickel_taskserv_profile_path)"
}
cat $nickel_taskserv_profile_path | save --append $nickel_temp
}
let keys_path_config = (get-keys-path)
if $keys_path_config != "" {
#use sops on_sops #use sops on_sops
let keys_path = ($defs.settings.src_path | path join $env.PROVISIONING_KEYS_PATH) let keys_path = ($defs.settings.src_path | path join $keys_path_config)
if not ($keys_path | path exists) { if not ($keys_path | path exists) {
if (is-debug-enabled) { if (is-debug-enabled) {
print $"❗Error KEYS_PATH (_ansi red_bold)($keys_path)(_ansi reset) found " _print $"❗Error KEYS_PATH (_ansi red_bold)($keys_path)(_ansi reset) found "
} else { } else {
print $"❗Error (_ansi red_bold)KEYS_PATH(_ansi reset) not found " _print $"❗Error (_ansi red_bold)KEYS_PATH(_ansi reset) not found "
} }
return false return false
} }
(on_sops d $keys_path) | save --append $kcl_temp (on_sops d $keys_path) | save --append $nickel_temp
if ($defs.settings.src_path | path join "extensions" | path join "clusters" | path join $defs.server.hostname | path join $"($defs.cluster.name).k" | path exists ) { let nickel_defined_taskserv_path = if ($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $defs.server.hostname | path join $"($defs.taskserv.profile).ncl" | path exists ) {
cat ($defs.settings.src_path | path join "extensions" | path join "clusters" | path join $defs.server.hostname| path join $"($defs.cluster.name).k" ) | save --append $kcl_temp ($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $defs.server.hostname | path join $"($defs.taskserv.profile).ncl")
} else if ($defs.settings.src_path | path join "extensions" | path join "clusters" | path join $defs.pos.server | path join $"($defs.cluster.name).k" | path exists ) { } else if ($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $defs.server.hostname | path join $"($defs.taskserv.profile).ncl" | path exists ) {
cat ($defs.settings.src_path | path join "extensions" | path join "clusters" | path join $defs.pos.server | path join $"($defs.cluster.name).k" ) | save --append $kcl_temp ($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $defs.server.hostname | path join $"($defs.taskserv.profile).ncl")
} else if ($defs.settings.src_path | path join "extensions" | path join "clusters" | path join $"($defs.cluster.name).k" | path exists ) { } else if ($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $"($defs.taskserv.profile).ncl" | path exists ) {
cat ($defs.settings.src_path | path join "extensions" | path join "clusters" | path join $"($defs.cluster.name).k" ) | save --append $kcl_temp ($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $"($defs.taskserv.profile).ncl")
} else if ($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $defs.server.hostname | path join $"($defs.taskserv.name).ncl" | path exists ) {
($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $defs.server.hostname | path join $"($defs.taskserv.name).ncl")
} else if ($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $defs.server.hostname | path join $defs.taskserv.profile | path join $"($defs.taskserv.name).ncl" | path exists ) {
($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $defs.server.hostname | path join $defs.taskserv.profile | path join $"($defs.taskserv.name).ncl")
} else if ($defs.settings.src_path | path join "extensions" | path join "taskservs"| path join $"($defs.taskserv.name).ncl" | path exists ) {
($defs.settings.src_path | path join "extensions" | path join "taskservs"| path join $"($defs.taskserv.name).ncl")
} else { "" }
if $nickel_defined_taskserv_path != "" and ($nickel_defined_taskserv_path | path exists) {
if (is-debug-enabled) {
_print $"adding defs taskserv: ($nickel_defined_taskserv_path)"
}
cat $nickel_defined_taskserv_path | save --append $nickel_temp
} }
let res = (^kcl $kcl_temp -o $wk_vars | complete) let res = (^nickel $nickel_temp -o $wk_vars | complete)
if $res.exit_code != 0 { if $res.exit_code != 0 {
print $"❗KCL errors (_ansi red_bold)($kcl_temp)(_ansi reset) found " _print $"❗Nickel errors (_ansi red_bold)($nickel_temp)(_ansi reset) found "
print $res.stdout _print $res.stdout
_print $res.stderr
rm -f $wk_vars rm -f $wk_vars
cd $env.PWD cd $env.PWD
return false return false
} }
rm -f $kcl_temp $err_out rm -f $nickel_temp $err_out
} else if ($defs.settings.src_path | path join "extensions" | path join "clusters" | path join $"($defs.cluster.name).yaml" | path exists) { } else if ( $defs.settings.src_path | path join "extensions" | path join "taskservs"| path join $"($defs.taskserv.name).yaml" | path exists) {
cat ($defs.settings.src_path | path join "extensions" | path join "clusters" | path join $"($defs.cluster.name).yaml" ) | tee { save -a $wk_vars } | ignore cat ($defs.settings.src_path | path join "extensions" | path join "taskservs"| path join $"($defs.taskserv.name).yaml") | tee { save -a $wk_vars } | ignore
} }
cd $env.PWD cd $env.PWD
} }
(^sed -i $"s/NOW/($env.NOW)/g" $wk_vars) (^sed -i $"s/NOW/($env.NOW)/g" $wk_vars)
if $defs.cluster_install_mode == "library" { if $defs.taskserv_install_mode == "library" {
let cluster_data = (open $wk_vars) let taskserv_data = (open $wk_vars)
let verbose = if (is-debug-enabled) { true } else { false } let quiet = if (is-debug-enabled) { false } else { true }
if $cluster_data.cluster.copy_paths? != null { if $taskserv_data.taskserv? != null and $taskserv_data.taskserv.copy_paths? != null {
#use utils/files.nu * #use utils/files.nu *
for it in $cluster_data.cluster.copy_paths { for it in $taskserv_data.taskserv.copy_paths {
let it_list = ($it | split row "|" | default []) let it_list = ($it | split row "|" | default [])
let cp_source = ($it_list | try { get 0 } catch { "") } let cp_source = ($it_list | get 0? | default "")
let cp_target = ($it_list | try { get 1 } catch { "") } let cp_target = ($it_list | get 1? | default "")
if ($cp_source | path exists) { if ($cp_source | path exists) {
copy_prov_files $cp_source ($defs.settings.infra_path | path join $defs.settings.infra) $"($cluster_env_path)/($cp_target)" false $verbose copy_prov_files $cp_source "." ($taskserv_env_path | path join $cp_target) false $quiet
} else if ($prov_resources_path | path join $cp_source | path exists) {
copy_prov_files $prov_resources_path $cp_source ($taskserv_env_path | path join $cp_target) false $quiet
} else if ($"($prov_resources_path)/($cp_source)" | path exists) { } else if ($"($prov_resources_path)/($cp_source)" | path exists) {
copy_prov_files $prov_resources_path $cp_source $"($cluster_env_path)/($cp_target)" false $verbose copy_prov_file ($prov_resources_path | path join $cp_source) ($taskserv_env_path | path join $cp_target) $quiet
} else if ($cp_source | file exists) {
copy_prov_file $cp_source $"($cluster_env_path)/($cp_target)" $verbose
} else if ($"($prov_resources_path)/($cp_source)" | path exists) {
copy_prov_file $"($prov_resources_path)/($cp_source)" $"($cluster_env_path)/($cp_target)" $verbose
} }
} }
} }
} }
rm -f ($cluster_env_path | path join "kcl") ($cluster_env_path | path join "*.k") rm -f ($taskserv_env_path | path join "nickel") ...(glob $"($taskserv_env_path)/*.ncl")
on_template_path $cluster_env_path $wk_vars true true on_template_path $taskserv_env_path $wk_vars true true
if ($cluster_env_path | path join $"env-($defs.cluster.name)" | path exists) { if ($taskserv_env_path | path join $"env-($defs.taskserv.name)" | path exists) {
^sed -i 's,\t,,g;s,^ ,,g;/^$/d' ($cluster_env_path | path join $"env-($defs.cluster.name)") ^sed -i 's,\t,,g;s,^ ,,g;/^$/d' ($taskserv_env_path | path join $"env-($defs.taskserv.name)")
} }
if ($cluster_env_path | path join "prepare" | path exists) { if ($taskserv_env_path | path join "prepare" | path exists) {
run_cmd "prepare" "Prepare" "run_cluster_library" $defs $cluster_env_path $wk_vars run_cmd "prepare" "prepare" "run_taskserv_library" $defs $taskserv_env_path $wk_vars
if ($cluster_env_path | path join "resources" | path exists) { if ($taskserv_env_path | path join "resources" | path exists) {
on_template_path ($cluster_env_path | path join "resources") $wk_vars false true on_template_path ($taskserv_env_path | path join "resources") $wk_vars false true
} }
} }
if not (is-debug-enabled) { if not (is-debug-enabled) {
rm -f ($cluster_env_path | path join "*.j2") $err_out $kcl_temp rm -f ...(glob $"($taskserv_env_path)/*.j2") $err_out $nickel_temp
} }
true true
} }
export def run_cluster [ export def run_taskserv [
defs: record defs: record
cluster_path: string taskserv_path: string
env_path: string env_path: string
]: nothing -> bool { ] {
if not ($cluster_path | path exists) { return false } if not ($taskserv_path | path exists) { return false }
if $defs.check { return }
let prov_resources_path = ($defs.settings.data.prov_resources_path | default "" | str replace "~" $env.HOME) let prov_resources_path = ($defs.settings.data.prov_resources_path | default "" | str replace "~" $env.HOME)
let created_clusters_dirpath = ($defs.settings.data.created_clusters_dirpath | default "/tmp" | let taskserv_server_name = $defs.server.hostname
let str_created_taskservs_dirpath = ($defs.settings.data.created_taskservs_dirpath | default "/tmp" |
str replace "~" $env.HOME | str replace "NOW" $env.NOW | str replace "./" $"($defs.settings.src_path)/") str replace "~" $env.HOME | str replace "NOW" $env.NOW | str replace "./" $"($defs.settings.src_path)/")
let cluster_server_name = $defs.server.hostname let created_taskservs_dirpath = if ($str_created_taskservs_dirpath | str starts-with "/" ) { $str_created_taskservs_dirpath } else { $defs.settings.src_path | path join $str_created_taskservs_dirpath }
if not ( $created_taskservs_dirpath | path exists) { ^mkdir -p $created_taskservs_dirpath }
let cluster_env_path = if $defs.cluster_install_mode == "server" { $"($env_path)_($defs.cluster_install_mode)" } else { $env_path } let str_taskserv_env_path = if $defs.taskserv_install_mode == "server" { $"($env_path)_($defs.taskserv_install_mode)" } else { $env_path }
let taskserv_env_path = if ($str_taskserv_env_path | str starts-with "/" ) { $str_taskserv_env_path } else { $defs.settings.src_path | path join $str_taskserv_env_path }
if not ( $taskserv_env_path | path exists) { ^mkdir -p $taskserv_env_path }
if not ( $cluster_env_path | path exists) { ^mkdir -p $cluster_env_path } (^cp -pr ...(glob ($taskserv_path | path join "*")) $taskserv_env_path)
if not ( $created_clusters_dirpath | path exists) { ^mkdir -p $created_clusters_dirpath } rm -rf ...(glob ($taskserv_env_path | path join "*.ncl")) ($taskserv_env_path | path join "nickel")
(^cp -pr $"($cluster_path)/*" $cluster_env_path) let wk_vars = ($created_taskservs_dirpath | path join $"($defs.server.hostname).yaml")
rm -rf $"($cluster_env_path)/*.k" $"($cluster_env_path)/kcl" let require_j2 = (^ls ...(glob ($taskserv_env_path | path join "*.j2")) err> (if $nu.os-info.name == "windows" { "NUL" } else { "/dev/null" }))
let wk_vars = $"($created_clusters_dirpath)/($defs.server.hostname).yaml" let res = if $defs.taskserv_install_mode == "library" or $require_j2 != "" {
# if $defs.cluster.name == "kubernetes" and ("/tmp/k8s_join.sh" | path exists) { cp -pr "/tmp/k8s_join.sh" $cluster_env_path } (run_taskserv_library $defs $taskserv_path $taskserv_env_path $wk_vars)
let require_j2 = (^ls ($cluster_env_path | path join "*.j2") err> (if $nu.os-info.name == "windows" { "NUL" } else { "/dev/null" }))
let res = if $defs.cluster_install_mode == "library" or $require_j2 != "" {
(run_cluster_library $defs $cluster_path $cluster_env_path $wk_vars)
} }
if not $res { if not $res {
if not (is-debug-enabled) { rm -f $wk_vars } if not (is-debug-enabled) { rm -f $wk_vars }
@ -199,86 +268,86 @@ export def run_cluster [
let tar_ops = if (is-debug-enabled) { "v" } else { "" } let tar_ops = if (is-debug-enabled) { "v" } else { "" }
let bash_ops = if (is-debug-enabled) { "bash -x" } else { "" } let bash_ops = if (is-debug-enabled) { "bash -x" } else { "" }
let res_tar = (^tar -C $cluster_env_path $"-c($tar_ops)zf" $"/tmp/($defs.cluster.name).tar.gz" . | complete) let res_tar = (^tar -C $taskserv_env_path $"-c($tar_ops)zmf" (["/tmp" $"($defs.taskserv.name).tar.gz"] | path join) . | complete)
if $res_tar.exit_code != 0 { if $res_tar.exit_code != 0 {
_print ( _print (
$"🛑 Error (_ansi red_bold)tar cluster(_ansi reset) server (_ansi green_bold)($defs.server.hostname)(_ansi reset)" + $"🛑 Error (_ansi red_bold)tar taskserv(_ansi reset) server (_ansi green_bold)($defs.server.hostname)(_ansi reset)" +
$" cluster (_ansi yellow_bold)($defs.cluster.name)(_ansi reset) ($cluster_env_path) -> /tmp/($defs.cluster.name).tar.gz" $" taskserv (_ansi yellow_bold)($defs.taskserv.name)(_ansi reset) ($taskserv_env_path) -> (['/tmp' $'($defs.taskserv.name).tar.gz'] | path join)"
) )
_print $res_tar.stdout
return false return false
} }
if $defs.check { if $defs.check {
if not (is-debug-enabled) { if not (is-debug-enabled) {
rm -f $wk_vars rm -f $wk_vars
rm -f $err_out if $err_out != "" { rm -f $err_out }
rm -rf $"($cluster_env_path)/*.k" $"($cluster_env_path)/kcl" rm -rf ...(glob $"($taskserv_env_path)/*.ncl") ($taskserv_env_path | path join join "nickel")
} }
return true return true
} }
let is_local = (^ip addr | grep "inet " | grep "$defs.ip") let is_local = (^ip addr | grep "inet " | grep "$defs.ip")
if $is_local != "" and not (is-debug-check-enabled) { if $is_local != "" and not (is-debug-check-enabled) {
if $defs.cluster_install_mode == "getfile" { if $defs.taskserv_install_mode == "getfile" {
if (cluster_get_file $defs.settings $defs.cluster $defs.server $defs.ip true true) { return false } if (taskserv_get_file $defs.settings $defs.taskserv $defs.server $defs.ip true true) { return false }
return true return true
} }
rm -rf $"/tmp/($defs.cluster.name)" rm -rf (["/tmp" $defs.taskserv.name ] | path join)
mkdir $"/tmp/($defs.cluster.name)" mkdir (["/tmp" $defs.taskserv.name ] | path join)
cd $"/tmp/($defs.cluster.name)" cd (["/tmp" $defs.taskserv.name ] | path join)
tar x($tar_ops)zf $"/tmp/($defs.cluster.name).tar.gz" tar x($tar_ops)zmf (["/tmp" $"($defs.taskserv.name).tar.gz"] | path join)
let res_run = (^sudo $bash_ops $"./install-($defs.cluster.name).sh" err> $err_out | complete) let res_run = (^sudo $bash_ops $"./install-($defs.taskserv.name).sh" err> $err_out | complete)
if $res_run.exit_code != 0 { if $res_run.exit_code != 0 {
(throw-error $"🛑 Error server ($defs.server.hostname) cluster ($defs.cluster.name) (throw-error $"🛑 Error server ($defs.server.hostname) taskserv ($defs.taskserv.name)
./install-($defs.cluster.name).sh ($defs.server_pos) ($defs.cluster_pos) (^pwd)" ./install-($defs.taskserv.name).sh ($defs.server_pos) ($defs.taskserv_pos) (^pwd)"
$"($res_run.stdout)\n(cat $err_out)" $"($res_run.stdout)\n(cat $err_out)"
"run_cluster_library" --span (metadata $res_run).span) "run_taskserv_library" --span (metadata $res_run).span)
exit 1 exit 1
} }
fi fi
rm -fr $"/tmp/($defs.cluster.name).tar.gz" $"/tmp/($defs.cluster.name)" rm -fr (["/tmp" $"($defs.taskserv.name).tar.gz"] | path join) (["/tmp" $"($defs.taskserv.name)"] | path join)
} else { } else {
if $defs.cluster_install_mode == "getfile" { if $defs.taskserv_install_mode == "getfile" {
if (cluster_get_file $defs.settings $defs.cluster $defs.server $defs.ip true false) { return false } if (taskserv_get_file $defs.settings $defs.taskserv $defs.server $defs.ip true false) { return false }
return true return true
} }
if not (is-debug-check-enabled) { if not (is-debug-check-enabled) {
#use ssh.nu * #use ssh.nu *
let scp_list: list<string> = ([] | append $"/tmp/($defs.cluster.name).tar.gz") let scp_list: list<string> = ([] | append $"/tmp/($defs.taskserv.name).tar.gz")
if not (scp_to $defs.settings $defs.server $scp_list "/tmp" $defs.ip) { if not (scp_to $defs.settings $defs.server $scp_list "/tmp" $defs.ip) {
_print ( _print (
$"🛑 Error (_ansi red_bold)ssh_cp(_ansi reset) server (_ansi green_bold)($defs.server.hostname)(_ansi reset) [($defs.ip)] " + $"🛑 Error (_ansi red_bold)ssh_to(_ansi reset) server (_ansi green_bold)($defs.server.hostname)(_ansi reset) [($defs.ip)] " +
$" cluster (_ansi yellow_bold)($defs.cluster.name)(_ansi reset) /tmp/($defs.cluster.name).tar.gz" $" taskserv (_ansi yellow_bold)($defs.taskserv.name)(_ansi reset) /tmp/($defs.taskserv.name).tar.gz"
) )
return false return false
} }
# $"rm -rf /tmp/($defs.taskserv.name); mkdir -p /tmp/($defs.taskserv.name) ;" +
let run_ops = if (is-debug-enabled) { "bash -x" } else { "" }
let cmd = ( let cmd = (
$"rm -rf /tmp/($defs.cluster.name) ; mkdir /tmp/($defs.cluster.name) ; cd /tmp/($defs.cluster.name) ;" + $"rm -rf /tmp/($defs.taskserv.name); mkdir -p /tmp/($defs.taskserv.name) ;" +
$" sudo tar x($tar_ops)zf /tmp/($defs.cluster.name).tar.gz;" + $" cd /tmp/($defs.taskserv.name) ; sudo tar x($tar_ops)zmf /tmp/($defs.taskserv.name).tar.gz &&" +
$" sudo ($bash_ops) ./install-($defs.cluster.name).sh " # ($env.PROVISIONING_MATCH_CMD) " $" sudo ($run_ops) ./install-($defs.taskserv.name).sh " # ($env.PROVISIONING_MATCH_CMD) "
) )
if not (ssh_cmd $defs.settings $defs.server true $cmd $defs.ip) { if not (ssh_cmd $defs.settings $defs.server false $cmd $defs.ip) {
_print ( _print (
$"🛑 Error (_ansi red_bold)ssh_cmd(_ansi reset) server (_ansi green_bold)($defs.server.hostname)(_ansi reset) [($defs.ip)] " + $"🛑 Error (_ansi red_bold)ssh_cmd(_ansi reset) server (_ansi green_bold)($defs.server.hostname)(_ansi reset) [($defs.ip)] " +
$" cluster (_ansi yellow_bold)($defs.cluster.name)(_ansi reset) install_($defs.cluster.name).sh" $" taskserv (_ansi yellow_bold)($defs.taskserv.name)(_ansi reset) install_($defs.taskserv.name).sh"
) )
return false return false
} }
# if $defs.cluster.name == "kubernetes" { let _res_k8s = (scp_from $defs.settings $defs.server "/tmp/k8s_join.sh" "/tmp" $defs.ip) }
if not (is-debug-enabled) { if not (is-debug-enabled) {
let rm_cmd = $"sudo rm -f /tmp/($defs.cluster.name).tar.gz; sudo rm -rf /tmp/($defs.cluster.name)" let rm_cmd = $"sudo rm -f /tmp/($defs.taskserv.name).tar.gz; sudo rm -rf /tmp/($defs.taskserv.name)"
let _res = (ssh_cmd $defs.settings $defs.server true $rm_cmd $defs.ip) let _res = (ssh_cmd $defs.settings $defs.server false $rm_cmd $defs.ip)
rm -f $"/tmp/($defs.cluster.name).tar.gz" rm -f $"/tmp/($defs.taskserv.name).tar.gz"
} }
} }
} }
if ($"($cluster_path)/postrun" | path exists ) { if ($taskserv_path | path join "postrun" | path exists ) {
cp $"($cluster_path)/postrun" $"($cluster_env_path)/postrun" cp ($taskserv_path | path join "postrun") ($taskserv_env_path | path join "postrun")
run_cmd "postrun" "PostRune" "run_cluster_library" $defs $cluster_env_path $wk_vars run_cmd "postrun" "PostRune" "run_taskserv_library" $defs $taskserv_env_path $wk_vars
} }
if not (is-debug-enabled) { if not (is-debug-enabled) {
rm -f $wk_vars rm -f $wk_vars
rm -f $err_out if $err_out != "" { rm -f $err_out }
rm -rf $"($cluster_env_path)/*.k" $"($cluster_env_path)/kcl" rm -rf ...(glob $"($taskserv_env_path)/*.ncl") ($taskserv_env_path | path join join "nickel")
} }
true true
} }

View file

@ -1,284 +0,0 @@
#use utils.nu cluster_get_file
#use utils/templates.nu on_template_path
use std
use ../lib_provisioning/config/accessor.nu [is-debug-enabled, is-debug-check-enabled]
def make_cmd_env_temp [
defs: record
cluster_env_path: string
wk_vars: string
]: nothing -> string {
let cmd_env_temp = $"($cluster_env_path)/cmd_env_(mktemp --tmpdir-path $cluster_env_path --suffix ".sh" | path basename)"
# export all 'PROVISIONING_' $env vars to SHELL
($"export NU_LOG_LEVEL=($env.NU_LOG_LEVEL)\n" +
($env | items {|key, value| if ($key | str starts-with "PROVISIONING_") {echo $'export ($key)="($value)"\n'} } | compact --empty | to text)
) | save --force $cmd_env_temp
$cmd_env_temp
}
def run_cmd [
cmd_name: string
title: string
where: string
defs: record
cluster_env_path: string
wk_vars: string
]: nothing -> nothing {
_print $"($title) for ($defs.cluster.name) on ($defs.server.hostname) ($defs.pos.server) ..."
if $defs.check { return }
let runner = (grep "^#!" $"($cluster_env_path)/($cmd_name)" | str trim)
let run_ops = if (is-debug-enabled) { if ($runner | str contains "bash" ) { "-x" } else { "" } } else { "" }
let cmd_env_temp = make_cmd_env_temp $defs $cluster_env_path $wk_vars
if ($wk_vars | path exists) {
let run_res = if ($runner | str ends-with "bash" ) {
(^bash -c $"'source ($cmd_env_temp) ; bash ($run_ops) ($cluster_env_path)/($cmd_name) ($wk_vars) ($defs.pos.server) ($defs.pos.cluster) (^pwd)'" | complete)
} else if ($runner | str ends-with "nu" ) {
(^bash -c $"'source ($cmd_env_temp); ($env.NU) ($env.NU_ARGS) ($cluster_env_path)/($cmd_name)'" | complete)
} else {
(^bash -c $"'source ($cmd_env_temp); ($cluster_env_path)/($cmd_name) ($wk_vars)'" | complete)
}
rm -f $cmd_env_temp
if $run_res.exit_code != 0 {
(throw-error $"🛑 Error server ($defs.server.hostname) cluster ($defs.cluster.name)
($cluster_env_path)/($cmd_name) with ($wk_vars) ($defs.pos.server) ($defs.pos.cluster) (^pwd)"
$run_res.stdout
$where --span (metadata $run_res).span)
exit 1
}
if not (is-debug-enabled) { rm -f $"($cluster_env_path)/prepare" }
}
}
export def run_cluster_library [
defs: record
cluster_path: string
cluster_env_path: string
wk_vars: string
]: nothing -> bool {
if not ($cluster_path | path exists) { return false }
let prov_resources_path = ($defs.settings.data.prov_resources_path | default "" | str replace "~" $env.HOME)
let cluster_server_name = $defs.server.hostname
rm -rf ($cluster_env_path | path join "*.k") ($cluster_env_path | path join "kcl")
mkdir ($cluster_env_path | path join "kcl")
let err_out = ($cluster_env_path | path join (mktemp --tmpdir-path $cluster_env_path --suffix ".err") | path basename)
let kcl_temp = ($cluster_env_path | path join "kcl" | path join (mktemp --tmpdir-path $cluster_env_path --suffix ".k" ) | path basename)
let wk_format = if $env.PROVISIONING_WK_FORMAT == "json" { "json" } else { "yaml" }
let wk_data = { defs: $defs.settings.data, pos: $defs.pos, server: $defs.server }
if $wk_format == "json" {
$wk_data | to json | save --force $wk_vars
} else {
$wk_data | to yaml | save --force $wk_vars
}
if $env.PROVISIONING_USE_KCL {
cd ($defs.settings.infra_path | path join $defs.settings.infra)
let kcl_cluster_path = if ($cluster_path | path join "kcl"| path join $"($defs.cluster.name).k" | path exists) {
($cluster_path | path join "kcl"| path join $"($defs.cluster.name).k")
} else if (($cluster_path | path dirname) | path join "kcl"| path join $"($defs.cluster.name).k" | path exists) {
(($cluster_path | path dirname) | path join "kcl"| path join $"($defs.cluster.name).k")
} else { "" }
if ($kcl_temp | path exists) { rm -f $kcl_temp }
let res = (^kcl import -m $wk_format $wk_vars -o $kcl_temp | complete)
if $res.exit_code != 0 {
print $"❗KCL import (_ansi red_bold)($wk_vars)(_ansi reset) Errors found "
print $res.stdout
rm -f $kcl_temp
cd $env.PWD
return false
}
# Very important! Remove external block for import and re-format it
# ^sed -i "s/^{//;s/^}//" $kcl_temp
open $kcl_temp -r | lines | find -v --regex "^{" | find -v --regex "^}" | save -f $kcl_temp
^kcl fmt $kcl_temp
if $kcl_cluster_path != "" and ($kcl_cluster_path | path exists) { cat $kcl_cluster_path | save --append $kcl_temp }
# } else { print $"❗ No cluster kcl ($defs.cluster.k) path found " ; return false }
if $env.PROVISIONING_KEYS_PATH != "" {
#use sops on_sops
let keys_path = ($defs.settings.src_path | path join $env.PROVISIONING_KEYS_PATH)
if not ($keys_path | path exists) {
if (is-debug-enabled) {
print $"❗Error KEYS_PATH (_ansi red_bold)($keys_path)(_ansi reset) found "
} else {
print $"❗Error (_ansi red_bold)KEYS_PATH(_ansi reset) not found "
}
return false
}
(on_sops d $keys_path) | save --append $kcl_temp
if ($defs.settings.src_path | path join "extensions" | path join "extensions" | path join "clusters" | path join $defs.server.hostname | path join $"($defs.cluster.name).k" | path exists ) {
cat ($defs.settings.src_path | path join "extensions" | path join "extensions" | path join "clusters" | path join $defs.server.hostname| path join $"($defs.cluster.name).k" ) | save --append $kcl_temp
} else if ($defs.settings.src_path | path join "extensions" | path join "extensions" | path join "clusters" | path join $defs.pos.server | path join $"($defs.cluster.name).k" | path exists ) {
cat ($defs.settings.src_path | path join "extensions" | path join "extensions" | path join "clusters" | path join $defs.pos.server | path join $"($defs.cluster.name).k" ) | save --append $kcl_temp
} else if ($defs.settings.src_path | path join "extensions" | path join "extensions" | path join "clusters" | path join $"($defs.cluster.name).k" | path exists ) {
cat ($defs.settings.src_path | path join "extensions" | path join "extensions" | path join "clusters" | path join $"($defs.cluster.name).k" ) | save --append $kcl_temp
}
let res = (^kcl $kcl_temp -o $wk_vars | complete)
if $res.exit_code != 0 {
print $"❗KCL errors (_ansi red_bold)($kcl_temp)(_ansi reset) found "
print $res.stdout
rm -f $wk_vars
cd $env.PWD
return false
}
rm -f $kcl_temp $err_out
} else if ($defs.settings.src_path | path join "extensions" | path join "extensions" | path join "clusters" | path join $"($defs.cluster.name).yaml" | path exists) {
cat ($defs.settings.src_path | path join "extensions" | path join "extensions" | path join "clusters" | path join $"($defs.cluster.name).yaml" ) | tee { save -a $wk_vars } | ignore
}
cd $env.PWD
}
(^sed -i $"s/NOW/($env.NOW)/g" $wk_vars)
if $defs.cluster_install_mode == "library" {
let cluster_data = (open $wk_vars)
let verbose = if (is-debug-enabled) { true } else { false }
if $cluster_data.cluster.copy_paths? != null {
#use utils/files.nu *
for it in $cluster_data.cluster.copy_paths {
let it_list = ($it | split row "|" | default [])
let cp_source = ($it_list | get -o 0 | default "")
let cp_target = ($it_list | get -o 1 | default "")
if ($cp_source | path exists) {
copy_prov_files $cp_source ($defs.settings.infra_path | path join $defs.settings.infra) $"($cluster_env_path)/($cp_target)" false $verbose
} else if ($"($prov_resources_path)/($cp_source)" | path exists) {
copy_prov_files $prov_resources_path $cp_source $"($cluster_env_path)/($cp_target)" false $verbose
} else if ($cp_source | file exists) {
copy_prov_file $cp_source $"($cluster_env_path)/($cp_target)" $verbose
} else if ($"($prov_resources_path)/($cp_source)" | path exists) {
copy_prov_file $"($prov_resources_path)/($cp_source)" $"($cluster_env_path)/($cp_target)" $verbose
}
}
}
}
rm -f ($cluster_env_path | path join "kcl") ($cluster_env_path | path join "*.k")
on_template_path $cluster_env_path $wk_vars true true
if ($cluster_env_path | path join $"env-($defs.cluster.name)" | path exists) {
^sed -i 's,\t,,g;s,^ ,,g;/^$/d' ($cluster_env_path | path join $"env-($defs.cluster.name)")
}
if ($cluster_env_path | path join "prepare" | path exists) {
run_cmd "prepare" "Prepare" "run_cluster_library" $defs $cluster_env_path $wk_vars
if ($cluster_env_path | path join "resources" | path exists) {
on_template_path ($cluster_env_path | path join "resources") $wk_vars false true
}
}
if not (is-debug-enabled) {
rm -f ($cluster_env_path | path join "*.j2") $err_out $kcl_temp
}
true
}
export def run_cluster [
defs: record
cluster_path: string
env_path: string
]: nothing -> bool {
if not ($cluster_path | path exists) { return false }
if $defs.check { return }
let prov_resources_path = ($defs.settings.data.prov_resources_path | default "" | str replace "~" $env.HOME)
let created_clusters_dirpath = ($defs.settings.data.created_clusters_dirpath | default "/tmp" |
str replace "~" $env.HOME | str replace "NOW" $env.NOW | str replace "./" $"($defs.settings.src_path)/")
let cluster_server_name = $defs.server.hostname
let cluster_env_path = if $defs.cluster_install_mode == "server" { $"($env_path)_($defs.cluster_install_mode)" } else { $env_path }
if not ( $cluster_env_path | path exists) { ^mkdir -p $cluster_env_path }
if not ( $created_clusters_dirpath | path exists) { ^mkdir -p $created_clusters_dirpath }
(^cp -pr $"($cluster_path)/*" $cluster_env_path)
rm -rf $"($cluster_env_path)/*.k" $"($cluster_env_path)/kcl"
let wk_vars = $"($created_clusters_dirpath)/($defs.server.hostname).yaml"
# if $defs.cluster.name == "kubernetes" and ("/tmp/k8s_join.sh" | path exists) { cp -pr "/tmp/k8s_join.sh" $cluster_env_path }
let require_j2 = (^ls ($cluster_env_path | path join "*.j2") err> (if $nu.os-info.name == "windows" { "NUL" } else { "/dev/null" }))
let res = if $defs.cluster_install_mode == "library" or $require_j2 != "" {
(run_cluster_library $defs $cluster_path $cluster_env_path $wk_vars)
}
if not $res {
if not (is-debug-enabled) { rm -f $wk_vars }
return $res
}
let err_out = ($env_path | path join (mktemp --tmpdir-path $env_path --suffix ".err") | path basename)
let tar_ops = if (is-debug-enabled) { "v" } else { "" }
let bash_ops = if (is-debug-enabled) { "bash -x" } else { "" }
let res_tar = (^tar -C $cluster_env_path $"-c($tar_ops)zf" $"/tmp/($defs.cluster.name).tar.gz" . | complete)
if $res_tar.exit_code != 0 {
_print (
$"🛑 Error (_ansi red_bold)tar cluster(_ansi reset) server (_ansi green_bold)($defs.server.hostname)(_ansi reset)" +
$" cluster (_ansi yellow_bold)($defs.cluster.name)(_ansi reset) ($cluster_env_path) -> /tmp/($defs.cluster.name).tar.gz"
)
_print $res_tar.stdout
return false
}
if $defs.check {
if not (is-debug-enabled) {
rm -f $wk_vars
rm -f $err_out
rm -rf $"($cluster_env_path)/*.k" $"($cluster_env_path)/kcl"
}
return true
}
let is_local = (^ip addr | grep "inet " | grep "$defs.ip")
if $is_local != "" and not (is-debug-check-enabled) {
if $defs.cluster_install_mode == "getfile" {
if (cluster_get_file $defs.settings $defs.cluster $defs.server $defs.ip true true) { return false }
return true
}
rm -rf $"/tmp/($defs.cluster.name)"
mkdir $"/tmp/($defs.cluster.name)"
cd $"/tmp/($defs.cluster.name)"
tar x($tar_ops)zf $"/tmp/($defs.cluster.name).tar.gz"
let res_run = (^sudo $bash_ops $"./install-($defs.cluster.name).sh" err> $err_out | complete)
if $res_run.exit_code != 0 {
(throw-error $"🛑 Error server ($defs.server.hostname) cluster ($defs.cluster.name)
./install-($defs.cluster.name).sh ($defs.server_pos) ($defs.cluster_pos) (^pwd)"
$"($res_run.stdout)\n(cat $err_out)"
"run_cluster_library" --span (metadata $res_run).span)
exit 1
}
fi
rm -fr $"/tmp/($defs.cluster.name).tar.gz" $"/tmp/($defs.cluster.name)"
} else {
if $defs.cluster_install_mode == "getfile" {
if (cluster_get_file $defs.settings $defs.cluster $defs.server $defs.ip true false) { return false }
return true
}
if not (is-debug-check-enabled) {
#use ssh.nu *
let scp_list: list<string> = ([] | append $"/tmp/($defs.cluster.name).tar.gz")
if not (scp_to $defs.settings $defs.server $scp_list "/tmp" $defs.ip) {
_print (
$"🛑 Error (_ansi red_bold)ssh_cp(_ansi reset) server (_ansi green_bold)($defs.server.hostname)(_ansi reset) [($defs.ip)] " +
$" cluster (_ansi yellow_bold)($defs.cluster.name)(_ansi reset) /tmp/($defs.cluster.name).tar.gz"
)
return false
}
let cmd = (
$"rm -rf /tmp/($defs.cluster.name) ; mkdir /tmp/($defs.cluster.name) ; cd /tmp/($defs.cluster.name) ;" +
$" sudo tar x($tar_ops)zf /tmp/($defs.cluster.name).tar.gz;" +
$" sudo ($bash_ops) ./install-($defs.cluster.name).sh " # ($env.PROVISIONING_MATCH_CMD) "
)
if not (ssh_cmd $defs.settings $defs.server true $cmd $defs.ip) {
_print (
$"🛑 Error (_ansi red_bold)ssh_cmd(_ansi reset) server (_ansi green_bold)($defs.server.hostname)(_ansi reset) [($defs.ip)] " +
$" cluster (_ansi yellow_bold)($defs.cluster.name)(_ansi reset) install_($defs.cluster.name).sh"
)
return false
}
# if $defs.cluster.name == "kubernetes" { let _res_k8s = (scp_from $defs.settings $defs.server "/tmp/k8s_join.sh" "/tmp" $defs.ip) }
if not (is-debug-enabled) {
let rm_cmd = $"sudo rm -f /tmp/($defs.cluster.name).tar.gz; sudo rm -rf /tmp/($defs.cluster.name)"
let _res = (ssh_cmd $defs.settings $defs.server true $rm_cmd $defs.ip)
rm -f $"/tmp/($defs.cluster.name).tar.gz"
}
}
}
if ($"($cluster_path)/postrun" | path exists ) {
cp $"($cluster_path)/postrun" $"($cluster_env_path)/postrun"
run_cmd "postrun" "PostRune" "run_cluster_library" $defs $cluster_env_path $wk_vars
}
if not (is-debug-enabled) {
rm -f $wk_vars
rm -f $err_out
rm -rf $"($cluster_env_path)/*.k" $"($cluster_env_path)/kcl"
}
true
}

View file

@ -1,61 +1,102 @@
# Hetzner Cloud utility functions
use env.nu *
# Parse record or string to server name
#use ssh.nu * export def parse_server_identifier [input: any]: nothing -> string {
export def cluster_get_file [ if ($input | describe) == "string" {
settings: record $input
cluster: record } else if ($input | has hostname) {
server: record $input.hostname
live_ip: string } else if ($input | has name) {
req_sudo: bool $input.name
local_mode: bool } else if ($input | has id) {
]: nothing -> bool { ($input.id | into string)
let target_path = ($cluster.target_path | default "") } else {
if $target_path == "" { ($input | into string)
_print $"🛑 No (_ansi red_bold)target_path(_ansi reset) found in ($server.hostname) cluster ($cluster.name)" }
return false }
# Check if IP is valid IPv4
export def is_valid_ipv4 [ip: string]: nothing -> bool {
$ip =~ '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'
}
# Check if IP is valid IPv6
export def is_valid_ipv6 [ip: string]: nothing -> bool {
$ip =~ ':[a-f0-9]{0,4}:' or $ip =~ '^[a-f0-9]{0,4}:[a-f0-9]{0,4}:'
}
# Format record as table for display
export def format_server_table [servers: list]: nothing -> nothing {
let columns = ["id", "name", "status", "public_net", "server_type"]
let formatted = $servers | map {|s|
{
ID: ($s.id | into string)
Name: $s.name
Status: ($s.status | str capitalize)
IP: ($s.public_net.ipv4.ip | default "-")
Type: ($s.server_type.name | default "-")
Location: ($s.location.name | default "-")
}
}
$formatted | table
null
}
# Get error message from API response
export def extract_api_error [response: any]: nothing -> string {
if ($response | has error) {
if ($response.error | has message) {
$response.error.message
} else {
($response.error | into string)
}
} else if ($response | has message) {
$response.message
} else {
($response | into string)
}
}
# Validate server configuration
export def validate_server_config [server: record]: nothing -> bool {
let required = ["hostname", "server_type", "location"]
let missing = $required | where {|f| not ($server | has $f)}
if not ($missing | is-empty) {
error make {msg: $"Missing required fields: ($missing | str join ", ")"}
}
true
}
# Convert timestamp to human readable format
export def format_timestamp [timestamp: int]: nothing -> string {
let date = (now | format date "%Y-%m-%dT%H:%M:%SZ")
$"($timestamp) (UTC)"
}
# Retry function with exponential backoff (no try-catch)
export def retry_with_backoff [closure: closure, max_attempts: int = 3, initial_delay: int = 1]: nothing -> any {
mut attempts = 0
mut delay = $initial_delay
loop {
let result = (do { $closure | call } | complete)
if $result.exit_code == 0 {
return ($result.stdout)
}
$attempts += 1
if $attempts >= $max_attempts {
error make {msg: $"Operation failed after ($attempts) attempts: ($result.stderr)"}
}
print $"Attempt ($attempts) failed, retrying in ($delay) seconds..."
sleep ($delay | into duration)
$delay = $delay * 2
} }
let source_path = ($cluster.soruce_path | default "")
if $source_path == "" {
_print $"🛑 No (_ansi red_bold)source_path(_ansi reset) found in ($server.hostname) cluster ($cluster.name)"
return false
}
if $local_mode {
let res = (^cp $source_path $target_path | combine)
if $res.exit_code != 0 {
_print $"🛑 Error get_file [ local-mode ] (_ansi red_bold)($source_path) to ($target_path)(_ansi reset) in ($server.hostname) cluster ($cluster.name)"
_print $res.stdout
return false
}
return true
}
let ip = if $live_ip != "" {
$live_ip
} else {
#use ../../../providers/prov_lib/middleware.nu mw_get_ip
(mw_get_ip $settings $server $server.liveness_ip false)
}
let ssh_key_path = ($server.ssh_key_path | default "")
if $ssh_key_path == "" {
_print $"🛑 No (_ansi red_bold)ssh_key_path(_ansi reset) found in ($server.hostname) cluster ($cluster.name)"
return false
}
if not ($ssh_key_path | path exists) {
_print $"🛑 Error (_ansi red_bold)($ssh_key_path)(_ansi reset) not found for ($server.hostname) cluster ($cluster.name)"
return false
}
mut cmd = if $req_sudo { "sudo" } else { "" }
let wk_path = $"/home/($env.SSH_USER)/($source_path| path basename)"
$cmd = $"($cmd) cp ($source_path) ($wk_path); sudo chown ($env.SSH_USER) ($wk_path)"
let wk_path = $"/home/($env.SSH_USER)/($source_path | path basename)"
let res = (ssh_cmd $settings $server false $cmd $ip )
if not $res { return false }
if not (scp_from $settings $server $wk_path $target_path $ip ) {
return false
}
let rm_cmd = if $req_sudo {
$"sudo rm -f ($wk_path)"
} else {
$"rm -f ($wk_path)"
}
return (ssh_cmd $settings $server false $rm_cmd $ip )
} }

314
nulib/commands-registry.ncl Normal file
View file

@ -0,0 +1,314 @@
# Command Registry Default Values
let { make_command, .. } = import "schemas/commands_registry/defaults.ncl" in
let cmd_reg_schema = import "schemas/commands_registry/schema.ncl" in
{
commands = [
make_command {
command = "help",
aliases = ["h", "-h", "--help"],
uses_cache = true,
help_category = "infrastructure",
description = "Show help for commands",
},
make_command {
command = "platform",
aliases = ["plat", "p"],
uses_cache = true,
help_category = "platform",
description = "Manage platform services",
},
make_command {
command = "guide",
aliases = ["guides", "howto"],
uses_cache = true,
help_category = "guides",
description = "Show guides and tutorials",
},
make_command {
command = "shortcuts",
aliases = ["sc"],
uses_cache = true,
requires_args = true,
help_category = "guides",
description = "Show command shortcuts",
},
make_command {
command = "quickstart",
aliases = ["quick"],
uses_cache = true,
requires_args = true,
help_category = "guides",
description = "Quick start guide",
},
make_command {
command = "from-scratch",
aliases = ["scratch"],
uses_cache = true,
requires_args = true,
help_category = "guides",
description = "Start from scratch guide",
},
make_command {
command = "customize",
aliases = ["custom"],
uses_cache = true,
requires_args = true,
help_category = "guides",
description = "Customization guide",
},
make_command {
command = "bootstrap",
aliases = ["bstrap"],
help_category = "infrastructure",
description = "L1 Hetzner resource bootstrap (network, firewall, SSH key, Floating IPs)",
},
make_command {
command = "fip",
aliases = ["floating-ip"],
help_category = "infrastructure",
description = "Floating IP management (list, show, assign, unassign, protection)",
},
make_command {
command = "volume",
aliases = ["vol"],
help_category = "infrastructure",
description = "Volume management (list, create, attach, detach, delete)",
},
make_command {
command = "server",
aliases = ["s"],
requires_daemon = true,
requires_services = true,
requires_args = true,
help_category = "infrastructure",
description = "Server management",
},
make_command {
command = "ssh",
requires_args = true,
help_category = "infrastructure",
description = "SSH shortcut: connect to a server by hostname (e.g. prvng ssh sgoyol-1)",
},
make_command {
command = "taskserv",
aliases = ["task", "t"],
requires_args = true,
help_category = "infrastructure",
description = "Task server management",
},
make_command {
command = "component",
aliases = ["c", "comp", "cl"],
requires_args = true,
help_category = "infrastructure",
description = "Component management — list, show, and status for workspace component instances",
},
make_command {
command = "extension",
aliases = ["e", "ext"],
requires_args = true,
help_category = "infrastructure",
description = "Extension catalog — browse extensions/components/ definitions and metadata",
},
make_command {
command = "create",
aliases = ["new"],
requires_args = true,
requires_daemon = true,
requires_services = true,
help_category = "infrastructure",
description = "Create resources (server, taskserv, cluster)",
},
make_command {
command = "delete",
aliases = ["d"],
requires_args = true,
help_category = "infrastructure",
description = "Delete resources (server, taskserv, cluster)",
},
make_command {
command = "workspace",
aliases = ["ws"],
requires_daemon = true,
uses_cache = true,
requires_args = true,
help_category = "workspace",
description = "Workspace management",
},
make_command {
command = "validate",
aliases = ["val"],
uses_cache = true,
requires_args = true,
help_category = "config",
description = "Validate configuration",
},
make_command {
command = "config",
uses_cache = true,
requires_args = true,
help_category = "setup",
description = "Configuration management",
},
make_command {
command = "env",
uses_cache = true,
requires_args = true,
help_category = "config",
description = "Environment configuration",
},
make_command {
command = "alias",
aliases = ["a", "al"],
uses_cache = true,
help_category = "utils",
description = "Show command aliases — alias list (al) displays the full shortcut table",
},
make_command {
command = "show",
uses_cache = true,
requires_args = true,
help_category = "config",
description = "Show configuration",
},
make_command {
command = "setup",
aliases = ["st"],
uses_cache = true,
help_category = "setup",
description = "Initial setup",
},
make_command {
command = "state",
aliases = ["st"],
uses_cache = false,
requires_args = true,
help_category = "state",
description = "Workspace provisioning state management",
},
make_command {
command = "job",
aliases = ["j"],
requires_args = true,
uses_cache = false,
help_category = "orchestration",
description = "Orchestrator job management (list, status, monitor, submit)",
},
make_command {
command = "workflow",
aliases = ["w", "wflow"],
requires_args = true,
uses_cache = false,
help_category = "orchestration",
description = "Workspace workflow management — WorkflowDef lifecycle (list, show, run, validate, status)",
},
make_command {
command = "batch",
aliases = ["b", "bat"],
requires_daemon = true,
uses_cache = true,
requires_args = true,
help_category = "orchestration",
description = "Batch operations",
},
make_command {
command = "orchestrator",
aliases = ["o", "orch"],
requires_daemon = true,
uses_cache = true,
requires_args = true,
help_category = "orchestration",
description = "Orchestrator management",
},
make_command {
command = "module",
aliases = ["mod"],
uses_cache = true,
requires_args = true,
help_category = "development",
description = "Module management",
},
make_command {
command = "layer",
aliases = ["lyr"],
uses_cache = true,
requires_args = true,
help_category = "development",
description = "Layer management",
},
make_command {
command = "discover",
aliases = ["disc"],
uses_cache = true,
requires_args = true,
help_category = "development",
description = "Discover modules",
},
make_command {
command = "status",
uses_cache = true,
requires_args = true,
help_category = "diagnostics",
description = "Show status",
},
make_command {
command = "health",
uses_cache = true,
requires_args = true,
help_category = "diagnostics",
description = "Health check",
},
make_command {
command = "diagnostics",
aliases = ["diag"],
uses_cache = true,
requires_args = true,
help_category = "diagnostics",
description = "Run diagnostics",
},
make_command {
command = "build",
aliases = ["bd"],
requires_daemon = true,
uses_cache = true,
requires_args = true,
help_category = "build",
description = "Build operations",
},
make_command {
command = "auth",
requires_daemon = true,
uses_cache = true,
requires_args = true,
help_category = "authentication",
description = "Authentication management",
},
make_command {
command = "login",
requires_daemon = true,
uses_cache = true,
requires_args = true,
help_category = "authentication",
description = "Login",
},
make_command {
command = "integrations",
aliases = ["int"],
requires_daemon = true,
uses_cache = true,
requires_args = true,
help_category = "integrations",
description = "Integration management",
},
make_command {
command = "vm",
requires_daemon = true,
uses_cache = true,
requires_args = true,
help_category = "vm",
description = "VM management",
},
],
}

312
nulib/components/mod.nu Normal file
View file

@ -0,0 +1,312 @@
#!/usr/bin/env nu
# Component management module — list, show, status for extensions/components.
#
# Two perspectives per component:
# extension — what exists in extensions/components/{name}/ (metadata, modes, contract)
# workspace — how it's instantiated in infra/{ws}/components/{name}.ncl
#
# Ontology data (FSM state, edges) is read via ontoref when available (defensive).
use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval-soft, default-ncl-paths]
# Resolve the extensions/components/ base path.
def _comp-ext-base []: nothing -> string {
let from_env = ($env.PROVISIONING_COMPONENTS_PATH? | default "")
if ($from_env | is-not-empty) and ($from_env | path exists) { return $from_env }
let prov = ($env.PROVISIONING? | default "")
if ($prov | is-not-empty) {
let p = ($prov | path join "extensions" | path join "components")
if ($p | path exists) { return $p }
}
""
}
# Resolve the workspace root for a given workspace name.
# Checks PROVISIONING_KLOUD_PATH env, then walks known workspace directories.
def _ws-root [workspace: string]: nothing -> string {
if ($workspace | is-empty) { return "" }
let from_env = ($env.PROVISIONING_KLOUD_PATH? | default "")
if ($from_env | is-not-empty) and ($from_env | path basename) == $workspace {
return $from_env
}
let prov = ($env.PROVISIONING? | default "")
if ($prov | is-not-empty) {
let ws_root = ($prov | path dirname | path join "workspaces" | path join $workspace)
if ($ws_root | path exists) { return $ws_root }
}
""
}
# Export a Nickel file to a record. Returns null on failure.
# Uses default-ncl-paths to match the daemon's cache key derivation.
def _ncl-export [file_path: string]: nothing -> any {
let ws_root = ($file_path | path dirname | path dirname | path dirname)
ncl-eval-soft $file_path (default-ncl-paths $ws_root) null
}
# Read FSM dimension for a component from state.ncl via ontoref or raw NCL export.
def _read-fsm-state [name: string, ws_root: string]: nothing -> record {
let dim_id = $"($name)-status"
# Try ontoref first (richer output)
let onto_result = (do {
^ontoref describe state $dim_id --fmt json --workspace $ws_root
} | complete)
if $onto_result.exit_code == 0 {
let parsed = (do { $onto_result.stdout | from json } | complete)
if $parsed.exit_code == 0 { return $parsed.stdout }
}
# Fallback: export state.ncl and filter
let state_path = ($ws_root | path join ".ontology" | path join "state.ncl")
if not ($state_path | path exists) { return {} }
let prov = ($env.PROVISIONING? | default "")
let state_data = (ncl-eval-soft $state_path (default-ncl-paths $ws_root) {})
if ($state_data | is-empty) { return {} }
let dims = ($state_data | get -o dimensions | default [])
$dims | where {|d| ($d | get -o id | default "") == $dim_id } | get 0? | default {}
}
# Read ontology node and edges for a component from core.ncl.
def _read-onto-node [name: string, ws_root: string]: nothing -> record {
let core_path = ($ws_root | path join ".ontology" | path join "core.ncl")
if not ($core_path | path exists) { return { node: null, edges_from: [], edges_to: [] } }
let prov = ($env.PROVISIONING? | default "")
let data = (ncl-eval-soft $core_path (default-ncl-paths $ws_root) null)
if $data == null { return { node: null, edges_from: [], edges_to: [] } }
let nodes = ($data | get -o nodes | default [])
let edges = ($data | get -o edges | default [])
let node = ($nodes | where {|n| ($n | get -o id | default "") == $name } | get 0? | default null)
let edges_from = ($edges | where {|e| ($e | get -o from | default "") == $name })
let edges_to = ($edges | where {|e| ($e | get -o to | default "") == $name })
{ node: $node, edges_from: $edges_from, edges_to: $edges_to }
}
# List all components from extensions/components/ with optional mode filter and workspace state.
export def component-list [mode: string, workspace: string]: nothing -> nothing {
let base = (_comp-ext-base)
if ($base | is-empty) or not ($base | path exists) {
print "❌ extensions/components/ not found. Set PROVISIONING env var."
return
}
let ws_root = (_ws-root $workspace)
let show_state = ($ws_root | is-not-empty)
mut rows = []
for item in (ls $base | where type == "dir") {
let name = ($item.name | path basename)
let meta_p = ($item.name | path join "metadata.ncl")
let meta = if ($meta_p | path exists) { _ncl-export $meta_p } else { null }
let modes = if $meta != null { $meta | get -o modes | default ["taskserv"] } else { ["taskserv"] }
let version = if $meta != null { $meta | get -o version | default "" } else { "" }
let desc = if $meta != null { $meta | get -o description | default "" } else { "" }
# Mode filter
if ($mode | is-not-empty) and ($mode not-in $modes) { continue }
let state = if $show_state {
let dim = (_read-fsm-state $name $ws_root)
if ($dim | is-empty) { "—" } else {
let cur = ($dim | get -o current_state | default "—")
let des = ($dim | get -o desired_state | default "")
if ($des | is-not-empty) and $cur != $des { $"($cur) → ($des)" } else { $cur }
}
} else { "—" }
$rows = ($rows | append {
name: $name
mode: ($modes | str join "·")
state: $state
version: $version
})
}
if ($rows | is-empty) {
print "No components found."
return
}
let header = if $show_state { $"Components [workspace: ($workspace)]" } else { "Components [extension catalog]" }
print $header
print "────────────────────────────────────────────────────────────"
$rows | table
}
# Show full details for a named component.
export def component-show [name: string, workspace: string, ext_only: bool]: nothing -> nothing {
let base = (_comp-ext-base)
let ext_dir = ($base | path join $name)
if not ($ext_dir | path exists) {
print $"❌ Component '($name)' not found in extensions/components/"
return
}
let meta_p = ($ext_dir | path join "metadata.ncl")
let meta = if ($meta_p | path exists) { _ncl-export $meta_p } else { null }
# Extension section
let modes = if $meta != null { $meta | get -o modes | default ["taskserv"] } else { ["taskserv"] }
let version = if $meta != null { $meta | get -o version | default "" } else { "" }
let desc = if $meta != null { $meta | get -o description | default "" } else { "" }
let tags = if $meta != null { $meta | get -o tags | default [] | str join " · " } else { "" }
# Defaults (requires/provides/operations from nickel/defaults.ncl)
let defaults_p = ($ext_dir | path join "nickel" | path join "defaults.ncl")
let defaults = if ($defaults_p | path exists) { _ncl-export $defaults_p } else { null }
let def_rec = if $defaults != null { $defaults | get -o $name | default {} } else { {} }
let requires = ($def_rec | get -o requires | default {})
let provides = ($def_rec | get -o provides | default {})
let operations = ($def_rec | get -o operations | default {})
print $"┌─ ($name | str upcase) ─────────────────────────────────"
print $"│ ($desc)"
print $"├────────────────────────────────────────────────────────"
let modes_str = ($modes | str join " · ")
print $"│ VERSION ($version)"
print $"│ MODES ($modes_str)"
if ($tags | is-not-empty) { print $"│ TAGS ($tags)" }
# REQUIRES
let req_storage = ($requires | get -o storage | default null)
let req_ports = ($requires | get -o ports | default [])
let req_creds = ($requires | get -o credentials | default [])
if $req_storage != null or ($req_ports | is-not-empty) or ($req_creds | is-not-empty) {
print "├─── REQUIRES ───────────────────────────────────────────"
if $req_storage != null {
let persist_label = if ($req_storage.persistent? | default false) { "persistent" } else { "ephemeral" }
let stor_size = ($req_storage.size? | default "?")
print $"│ storage ($stor_size) ($persist_label)"
}
for p in $req_ports {
let pport = ($p.port? | default 0 | into string)
let pproto = ($p.protocol? | default "TCP")
let pexpose = ($p.exposure? | default "internal")
print $"│ port ($pport)/($pproto) \(($pexpose)\)"
}
if ($req_creds | is-not-empty) {
let creds_str = ($req_creds | str join " · ")
print $"│ creds ($creds_str)"
}
}
# PROVIDES
let prov_svc = ($provides | get -o service | default "")
let prov_port = ($provides | get -o port | default null)
let prov_dbs = ($provides | get -o databases | default [])
if ($prov_svc | is-not-empty) or $prov_port != null or ($prov_dbs | is-not-empty) {
print "├─── PROVIDES ───────────────────────────────────────────"
if ($prov_svc | is-not-empty) and $prov_port != null {
print $"│ service ($prov_svc):($prov_port)"
} else if ($prov_svc | is-not-empty) {
print $"│ service ($prov_svc)"
}
if ($prov_dbs | is-not-empty) {
let dbs_str = ($prov_dbs | str join " · ")
print $"│ databases ($dbs_str)"
}
}
# OPERATIONS
let ops_enabled = ($operations | transpose k v | where v == true | each {|r| $r.k })
if ($ops_enabled | is-not-empty) {
let ops_str = ($ops_enabled | str join " · ")
print "├─── OPERATIONS ─────────────────────────────────────────"
print $"│ ($ops_str)"
}
if not $ext_only and ($workspace | is-not-empty) {
let ws_root = (_ws-root $workspace)
if ($ws_root | is-not-empty) {
# Workspace instance
let comp_p = ($ws_root | path join "infra" | path join $workspace | path join "components" | path join $"($name).ncl")
let comp_data = if ($comp_p | path exists) { _ncl-export $comp_p } else { null }
let inst = if $comp_data != null { $comp_data | get -o $name | default {} } else { {} }
let inst_mode = ($inst | get -o mode | default "")
let inst_ns = ($inst | get -o namespace | default "")
let inst_tgt = ($inst | get -o target | default "")
print "├─── WORKSPACE INSTANCE ─────────────────────────────────"
if ($inst_mode | is-not-empty) { print $"│ mode ($inst_mode)" }
if ($inst_ns | is-not-empty) { print $"│ namespace ($inst_ns)" }
if ($inst_tgt | is-not-empty) { print $"│ target ($inst_tgt)" }
# FSM state
let dim = (_read-fsm-state $name $ws_root)
if not ($dim | is-empty) {
let cur = ($dim | get -o current_state | default "—")
let des = ($dim | get -o desired_state | default "")
let blk = ($dim | get -o transitions | default [] | get 0? | default {} | get -o blocker | default "")
let blk_short = ($blk | str substring 0..80)
print "├─── STATE ───────────────────────────────────────────"
print $"│ current ($cur)"
if ($des | is-not-empty) { print $"│ desired ($des)" }
if ($blk | is-not-empty) { print $"│ blocker ($blk_short)" }
}
# Ontology
let onto = (_read-onto-node $name $ws_root)
if $onto.node != null {
let node = $onto.node
let node_lvl = ($node.level? | default "?")
let node_pole = ($node.pole? | default "?")
print "├─── ONTOLOGY ────────────────────────────────────────"
print $"│ node ($name) \(($node_lvl) / ($node_pole)\)"
let arts = ($node | get -o artifact_paths | default [])
if ($arts | is-not-empty) {
let arts_str = ($arts | str join " · ")
print $"│ artifacts ($arts_str)"
}
let adrs = ($node | get -o adrs | default [])
if ($adrs | is-not-empty) {
let adrs_str = ($adrs | str join " · ")
print $"│ adrs ($adrs_str)"
}
if ($onto.edges_from | is-not-empty) {
let consumers = ($onto.edges_from | each {|e|
let eto = ($e | get -o to | default "?")
let ekind = ($e | get -o kind | default "")
$"($eto) \(($ekind)\)"
} | str join " · ")
print $"│ used-by ($consumers)"
}
if ($onto.edges_to | is-not-empty) {
let uses = ($onto.edges_to | each {|e|
let efrom = ($e | get -o from | default "?")
let ekind = ($e | get -o kind | default "")
$"($efrom) \(($ekind)\)"
} | str join " · ")
print $"│ uses ($uses)"
}
}
}
}
print "└────────────────────────────────────────────────────────"
}
# Show only FSM state for a component.
export def component-status [name: string, workspace: string]: nothing -> nothing {
if ($workspace | is-empty) {
print "❌ --workspace required for status"
return
}
let ws_root = (_ws-root $workspace)
if ($ws_root | is-empty) {
print $"❌ Workspace '($workspace)' not found"
return
}
let dim = (_read-fsm-state $name $ws_root)
if ($dim | is-empty) {
print $"No FSM dimension found for '($name)-status' in ($workspace)"
return
}
let cur = ($dim | get -o current_state | default "—")
let des = ($dim | get -o desired_state | default "—")
let blk = ($dim | get -o transitions | default [] | get 0? | default {} | get -o blocker | default "")
let cat = ($dim | get -o transitions | default [] | get 0? | default {} | get -o catalyst | default "")
print $"($name) — FSM state [($workspace)]"
print $" current: ($cur)"
print $" desired: ($des)"
if ($blk | is-not-empty) { print $" blocker: ($blk)" }
if ($cat | is-not-empty) { print $" catalyst: ($cat)" }
}

View file

@ -17,13 +17,12 @@ export def check_marimo_available []: nothing -> bool {
export def install_marimo []: nothing -> bool { export def install_marimo []: nothing -> bool {
if not (check_marimo_available) { if not (check_marimo_available) {
print "📦 Installing Marimo..." print "📦 Installing Marimo..."
let result = do { ^pip install marimo } | complete let result = (do { ^pip install marimo } | complete)
if $result.exit_code != 0 {
if $result.exit_code == 0 {
true
} else {
print "❌ Failed to install Marimo. Please install manually: pip install marimo" print "❌ Failed to install Marimo. Please install manually: pip install marimo"
false false
} else {
true
} }
} else { } else {
true true
@ -498,4 +497,4 @@ export def main [
print " ai-insights - AI-powered insights dashboard" print " ai-insights - AI-powered insights dashboard"
} }
} }
} }

View file

@ -7,7 +7,7 @@ use polars_integration.nu *
use ../lib_provisioning/utils/settings.nu * use ../lib_provisioning/utils/settings.nu *
# Log sources configuration # Log sources configuration
export def get_log_sources []: nothing -> record { export def get_log_sources [] {
{ {
system: { system: {
paths: ["/var/log/syslog", "/var/log/messages"] paths: ["/var/log/syslog", "/var/log/messages"]
@ -56,7 +56,7 @@ export def collect_logs [
--output_format: string = "dataframe" --output_format: string = "dataframe"
--filter_level: string = "info" --filter_level: string = "info"
--include_metadata = true --include_metadata = true
]: nothing -> any { ] {
print $"📊 Collecting logs from the last ($since)..." print $"📊 Collecting logs from the last ($since)..."
@ -100,7 +100,7 @@ def collect_from_source [
source: string source: string
config: record config: record
--since: string = "1h" --since: string = "1h"
]: nothing -> list { ] {
match $source { match $source {
"system" => { "system" => {
@ -125,7 +125,7 @@ def collect_from_source [
def collect_system_logs [ def collect_system_logs [
config: record config: record
--since: string = "1h" --since: string = "1h"
]: record -> list { ] {
$config.paths | each {|path| $config.paths | each {|path|
if ($path | path exists) { if ($path | path exists) {
@ -142,7 +142,7 @@ def collect_system_logs [
def collect_provisioning_logs [ def collect_provisioning_logs [
config: record config: record
--since: string = "1h" --since: string = "1h"
]: record -> list { ] {
$config.paths | each {|log_dir| $config.paths | each {|log_dir|
if ($log_dir | path exists) { if ($log_dir | path exists) {
@ -164,7 +164,7 @@ def collect_provisioning_logs [
def collect_container_logs [ def collect_container_logs [
config: record config: record
--since: string = "1h" --since: string = "1h"
]: record -> list { ] {
if ((which docker | length) > 0) { if ((which docker | length) > 0) {
collect_docker_logs --since $since collect_docker_logs --since $since
@ -177,7 +177,7 @@ def collect_container_logs [
def collect_kubernetes_logs [ def collect_kubernetes_logs [
config: record config: record
--since: string = "1h" --since: string = "1h"
]: record -> list { ] {
if ((which kubectl | length) > 0) { if ((which kubectl | length) > 0) {
collect_k8s_logs --since $since collect_k8s_logs --since $since
@ -190,7 +190,7 @@ def collect_kubernetes_logs [
def read_recent_logs [ def read_recent_logs [
file_path: string file_path: string
--since: string = "1h" --since: string = "1h"
]: string -> list { ] {
let since_timestamp = ((date now) - (parse_duration $since)) let since_timestamp = ((date now) - (parse_duration $since))
@ -213,7 +213,7 @@ def read_recent_logs [
def parse_system_log_line [ def parse_system_log_line [
line: string line: string
source_file: string source_file: string
]: nothing -> record { ] {
# Parse standard syslog format # Parse standard syslog format
let syslog_pattern = '(?P<timestamp>\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+(?P<hostname>\S+)\s+(?P<process>\S+?)(\[(?P<pid>\d+)\])?:\s*(?P<message>.*)' let syslog_pattern = '(?P<timestamp>\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+(?P<hostname>\S+)\s+(?P<process>\S+?)(\[(?P<pid>\d+)\])?:\s*(?P<message>.*)'
@ -246,7 +246,7 @@ def parse_system_log_line [
def collect_json_logs [ def collect_json_logs [
file_path: string file_path: string
--since: string = "1h" --since: string = "1h"
]: string -> list { ] {
let lines = (read_recent_logs $file_path --since $since) let lines = (read_recent_logs $file_path --since $since)
$lines | each {|line| $lines | each {|line|
@ -278,7 +278,7 @@ def collect_json_logs [
def collect_text_logs [ def collect_text_logs [
file_path: string file_path: string
--since: string = "1h" --since: string = "1h"
]: string -> list { ] {
let lines = (read_recent_logs $file_path --since $since) let lines = (read_recent_logs $file_path --since $since)
$lines | each {|line| $lines | each {|line|
@ -294,7 +294,7 @@ def collect_text_logs [
def collect_docker_logs [ def collect_docker_logs [
--since: string = "1h" --since: string = "1h"
]: nothing -> list { ] {
do { do {
let containers = (docker ps --format "{{.Names}}" | lines) let containers = (docker ps --format "{{.Names}}" | lines)
@ -322,7 +322,7 @@ def collect_docker_logs [
def collect_k8s_logs [ def collect_k8s_logs [
--since: string = "1h" --since: string = "1h"
]: nothing -> list { ] {
do { do {
let pods = (kubectl get pods -o jsonpath='{.items[*].metadata.name}' | split row " ") let pods = (kubectl get pods -o jsonpath='{.items[*].metadata.name}' | split row " ")
@ -348,7 +348,7 @@ def collect_k8s_logs [
} }
} }
def parse_syslog_timestamp [ts: string]: string -> datetime { def parse_syslog_timestamp [ts: string] {
do { do {
# Parse syslog timestamp format: "Jan 16 10:30:15" # Parse syslog timestamp format: "Jan 16 10:30:15"
let current_year = (date now | date format "%Y") let current_year = (date now | date format "%Y")
@ -360,7 +360,7 @@ def parse_syslog_timestamp [ts: string]: string -> datetime {
} }
} }
def extract_log_level [message: string]: string -> string { def extract_log_level [message: string] {
let level_patterns = { let level_patterns = {
"FATAL": "fatal" "FATAL": "fatal"
"ERROR": "error" "ERROR": "error"
@ -385,7 +385,7 @@ def extract_log_level [message: string]: string -> string {
def filter_by_level [ def filter_by_level [
logs: list logs: list
level: string level: string
]: nothing -> list { ] {
let level_order = ["trace", "debug", "info", "warn", "warning", "error", "fatal"] let level_order = ["trace", "debug", "info", "warn", "warning", "error", "fatal"]
let min_index = ($level_order | enumerate | where {|row| $row.item == $level} | get index.0) let min_index = ($level_order | enumerate | where {|row| $row.item == $level} | get index.0)
@ -396,7 +396,7 @@ def filter_by_level [
} }
} }
def parse_duration [duration: string]: string -> duration { def parse_duration [duration: string] {
match $duration { match $duration {
$dur if ($dur | str ends-with "m") => { $dur if ($dur | str ends-with "m") => {
let minutes = ($dur | str replace "m" "" | into int) let minutes = ($dur | str replace "m" "" | into int)
@ -422,7 +422,7 @@ export def analyze_logs [
--analysis_type: string = "summary" # summary, errors, patterns, performance --analysis_type: string = "summary" # summary, errors, patterns, performance
--time_window: string = "1h" --time_window: string = "1h"
--group_by: list<string> = ["service", "level"] --group_by: list<string> = ["service", "level"]
]: any -> any { ] {
match $analysis_type { match $analysis_type {
"summary" => { "summary" => {
@ -443,7 +443,7 @@ export def analyze_logs [
} }
} }
def analyze_log_summary [logs_df: any, group_cols: list<string>]: nothing -> any { def analyze_log_summary [logs_df: any, group_cols: list<string>] {
aggregate_dataframe $logs_df --group_by $group_cols --operations { aggregate_dataframe $logs_df --group_by $group_cols --operations {
count: "count" count: "count"
first_seen: "min" first_seen: "min"
@ -451,17 +451,17 @@ def analyze_log_summary [logs_df: any, group_cols: list<string>]: nothing -> any
} }
} }
def analyze_log_errors [logs_df: any]: any -> any { def analyze_log_errors [logs_df: any] {
# Filter error logs and analyze patterns # Filter error logs and analyze patterns
query_dataframe $logs_df "SELECT * FROM logs_df WHERE level IN ('error', 'fatal', 'warn')" query_dataframe $logs_df "SELECT * FROM logs_df WHERE level IN ('error', 'fatal', 'warn')"
} }
def analyze_log_patterns [logs_df: any, time_window: string]: nothing -> any { def analyze_log_patterns [logs_df: any, time_window: string] {
# Time series analysis of log patterns # Time series analysis of log patterns
time_series_analysis $logs_df --time_column "timestamp" --value_column "level" --window $time_window time_series_analysis $logs_df --time_column "timestamp" --value_column "level" --window $time_window
} }
def analyze_log_performance [logs_df: any, time_window: string]: nothing -> any { def analyze_log_performance [logs_df: any, time_window: string] {
# Analyze performance-related logs # Analyze performance-related logs
query_dataframe $logs_df "SELECT * FROM logs_df WHERE message LIKE '%performance%' OR message LIKE '%slow%'" query_dataframe $logs_df "SELECT * FROM logs_df WHERE message LIKE '%performance%' OR message LIKE '%slow%'"
} }
@ -471,7 +471,7 @@ export def generate_log_report [
logs_df: any logs_df: any
--output_path: string = "log_report.md" --output_path: string = "log_report.md"
--include_charts = false --include_charts = false
]: any -> nothing { ] {
let summary = analyze_logs $logs_df --analysis_type "summary" let summary = analyze_logs $logs_df --analysis_type "summary"
let errors = analyze_logs $logs_df --analysis_type "errors" let errors = analyze_logs $logs_df --analysis_type "errors"
@ -516,7 +516,7 @@ export def monitor_logs [
--follow = true --follow = true
--alert_level: string = "error" --alert_level: string = "error"
--callback: string = "" --callback: string = ""
]: nothing -> nothing { ] {
print $"👀 Starting real-time log monitoring (alert level: ($alert_level))..." print $"👀 Starting real-time log monitoring (alert level: ($alert_level))..."
@ -544,4 +544,4 @@ export def monitor_logs [
sleep 60sec # Check every minute sleep 60sec # Check every minute
} }
} }
} }

View file

@ -6,13 +6,13 @@
use ../lib_provisioning/utils/settings.nu * use ../lib_provisioning/utils/settings.nu *
# Check if Polars plugin is available # Check if Polars plugin is available
export def check_polars_available []: nothing -> bool { export def check_polars_available [] {
let plugins = (plugin list) let plugins = (plugin list)
($plugins | any {|p| $p.name == "polars" or $p.name == "nu_plugin_polars"}) ($plugins | any {|p| $p.name == "polars" or $p.name == "nu_plugin_polars"})
} }
# Initialize Polars plugin if available # Initialize Polars plugin if available
export def init_polars []: nothing -> bool { export def init_polars [] {
if (check_polars_available) { if (check_polars_available) {
# Polars plugin is available - return true # Polars plugin is available - return true
# Note: Actual plugin loading happens during session initialization # Note: Actual plugin loading happens during session initialization
@ -28,7 +28,7 @@ export def create_infra_dataframe [
data: list data: list
--source: string = "infrastructure" --source: string = "infrastructure"
--timestamp = true --timestamp = true
]: list -> any { ] {
let use_polars = init_polars let use_polars = init_polars
@ -56,7 +56,7 @@ export def process_logs_to_dataframe [
--time_column: string = "timestamp" --time_column: string = "timestamp"
--level_column: string = "level" --level_column: string = "level"
--message_column: string = "message" --message_column: string = "message"
]: list<string> -> any { ] {
let use_polars = init_polars let use_polars = init_polars
@ -100,7 +100,7 @@ export def process_logs_to_dataframe [
def parse_log_file [ def parse_log_file [
file_path: string file_path: string
--format: string = "auto" --format: string = "auto"
]: string -> list { ] {
if not ($file_path | path exists) { if not ($file_path | path exists) {
return [] return []
@ -167,7 +167,7 @@ def parse_log_file [
} }
# Parse syslog format line # Parse syslog format line
def parse_syslog_line [line: string]: string -> record { def parse_syslog_line [line: string] {
# Basic syslog parsing - can be enhanced # Basic syslog parsing - can be enhanced
let parts = ($line | parse --regex '(?P<timestamp>\w+\s+\d+\s+\d+:\d+:\d+)\s+(?P<host>\S+)\s+(?P<service>\S+):\s*(?P<message>.*)') let parts = ($line | parse --regex '(?P<timestamp>\w+\s+\d+\s+\d+:\d+:\d+)\s+(?P<host>\S+)\s+(?P<service>\S+):\s*(?P<message>.*)')
@ -190,7 +190,7 @@ def parse_syslog_line [line: string]: string -> record {
} }
# Standardize timestamp formats # Standardize timestamp formats
def standardize_timestamp [ts: any]: any -> datetime { def standardize_timestamp [ts: any] {
match ($ts | describe) { match ($ts | describe) {
"string" => { "string" => {
do { do {
@ -207,14 +207,14 @@ def standardize_timestamp [ts: any]: any -> datetime {
} }
# Enhance Nushell table with DataFrame-like operations # Enhance Nushell table with DataFrame-like operations
def enhance_nushell_table []: list -> list { def enhance_nushell_table [] {
let data = $in let data = $in
# Add DataFrame-like methods through custom commands # Add DataFrame-like methods through custom commands
$data | add_dataframe_methods $data | add_dataframe_methods
} }
def add_dataframe_methods []: list -> list { def add_dataframe_methods [] {
# This function adds metadata to enable DataFrame-like operations # This function adds metadata to enable DataFrame-like operations
# In a real implementation, we'd add custom commands to the scope # In a real implementation, we'd add custom commands to the scope
$in $in
@ -225,7 +225,7 @@ export def query_dataframe [
df: any df: any
query: string query: string
--use_polars = false --use_polars = false
]: any -> any { ] {
if $use_polars and (check_polars_available) { if $use_polars and (check_polars_available) {
# Use Polars query capabilities # Use Polars query capabilities
@ -236,7 +236,7 @@ export def query_dataframe [
} }
} }
def query_with_nushell [df: any, query: string]: nothing -> any { def query_with_nushell [df: any, query: string] {
# Simple SQL-like query parser for Nushell # Simple SQL-like query parser for Nushell
# This is a basic implementation - can be significantly enhanced # This is a basic implementation - can be significantly enhanced
@ -266,7 +266,7 @@ def query_with_nushell [df: any, query: string]: nothing -> any {
} }
} }
def process_where_clause [data: any, conditions: string]: nothing -> any { def process_where_clause [data: any, conditions: string] {
# Basic WHERE clause implementation # Basic WHERE clause implementation
# This would need significant enhancement for production use # This would need significant enhancement for production use
$data $data
@ -278,7 +278,7 @@ export def aggregate_dataframe [
--group_by: list<string> = [] --group_by: list<string> = []
--operations: record = {} # {column: operation} --operations: record = {} # {column: operation}
--time_bucket: string = "1h" # For time-based aggregations --time_bucket: string = "1h" # For time-based aggregations
]: any -> any { ] {
let use_polars = init_polars let use_polars = init_polars
@ -296,7 +296,7 @@ def aggregate_with_polars [
group_cols: list<string> group_cols: list<string>
operations: record operations: record
time_bucket: string time_bucket: string
]: nothing -> any { ] {
# Polars aggregation implementation # Polars aggregation implementation
if ($group_cols | length) > 0 { if ($group_cols | length) > 0 {
$df | polars group-by $group_cols | polars agg [ $df | polars group-by $group_cols | polars agg [
@ -314,7 +314,7 @@ def aggregate_with_nushell [
group_cols: list<string> group_cols: list<string>
operations: record operations: record
time_bucket: string time_bucket: string
]: nothing -> any { ] {
# Nushell aggregation implementation # Nushell aggregation implementation
if ($group_cols | length) > 0 { if ($group_cols | length) > 0 {
$df | group-by ($group_cols | str join " ") $df | group-by ($group_cols | str join " ")
@ -330,7 +330,7 @@ export def time_series_analysis [
--value_column: string = "value" --value_column: string = "value"
--window: string = "1h" --window: string = "1h"
--operations: list<string> = ["mean", "sum", "count"] --operations: list<string> = ["mean", "sum", "count"]
]: any -> any { ] {
let use_polars = init_polars let use_polars = init_polars
@ -347,7 +347,7 @@ def time_series_with_polars [
value_col: string value_col: string
window: string window: string
ops: list<string> ops: list<string>
]: nothing -> any { ] {
# Polars time series operations # Polars time series operations
$df | polars group-by $time_col | polars agg [ $df | polars group-by $time_col | polars agg [
(polars col $value_col | polars mean) (polars col $value_col | polars mean)
@ -362,7 +362,7 @@ def time_series_with_nushell [
value_col: string value_col: string
window: string window: string
ops: list<string> ops: list<string>
]: nothing -> any { ] {
# Nushell time series - basic implementation # Nushell time series - basic implementation
$df | group-by {|row| $df | group-by {|row|
# Group by time windows - simplified # Group by time windows - simplified
@ -383,7 +383,7 @@ export def export_dataframe [
df: any df: any
output_path: string output_path: string
--format: string = "csv" # csv, parquet, json, excel --format: string = "csv" # csv, parquet, json, excel
]: any -> nothing { ] {
let use_polars = init_polars let use_polars = init_polars
@ -417,7 +417,7 @@ export def export_dataframe [
export def benchmark_operations [ export def benchmark_operations [
data_size: int = 10000 data_size: int = 10000
operations: list<string> = ["filter", "group", "aggregate"] operations: list<string> = ["filter", "group", "aggregate"]
]: int -> record { ] {
print $"🔬 Benchmarking operations with ($data_size) records..." print $"🔬 Benchmarking operations with ($data_size) records..."
@ -462,7 +462,7 @@ export def benchmark_operations [
$results $results
} }
def benchmark_nushell_operations [data: list, ops: list<string>]: nothing -> any { def benchmark_nushell_operations [data: list, ops: list<string>] {
mut result = $data mut result = $data
if "filter" in $ops { if "filter" in $ops {
@ -484,7 +484,7 @@ def benchmark_nushell_operations [data: list, ops: list<string>]: nothing -> any
$result $result
} }
def benchmark_polars_operations [data: list, ops: list<string>]: nothing -> any { def benchmark_polars_operations [data: list, ops: list<string>] {
mut df = ($data | polars into-df) mut df = ($data | polars into-df)
if "filter" in $ops { if "filter" in $ops {
@ -503,4 +503,4 @@ def benchmark_polars_operations [data: list, ops: list<string>]: nothing -> any
} }
$df $df
} }

View file

@ -4,20 +4,20 @@ print "🤖 AI Integration FIXED & READY!"
print "===============================" print "==============================="
print "" print ""
print "✅ Status: All syntax errors resolved" print "✅ Status: All syntax errors resolved"
print "✅ Core functionality: AI library working" print "✅ Core functionality: AI library working"
print "✅ Implementation: All features completed" print "✅ Implementation: All features completed"
print "" print ""
print "📋 What was implemented:" print "📋 What was implemented:"
print " 1. Template Generation: AI-powered configs" print " 1. Template Generation: AI-powered configs"
print " 2. Natural Language Queries: --ai_query flag" print " 2. Natural Language Queries: --ai_query flag"
print " 3. Plugin Architecture: OpenAI/Claude/Generic" print " 3. Plugin Architecture: OpenAI/Claude/Generic"
print " 4. Webhook Integration: Chat platforms" print " 4. Webhook Integration: Chat platforms"
print "" print ""
print "🔧 To enable, set environment variable:" print "🔧 To enable, set environment variable:"
print " export OPENAI_API_KEY='your-key'" print " export OPENAI_API_KEY='your-key'"
print " export ANTHROPIC_API_KEY='your-key'" print " export ANTHROPIC_API_KEY='your-key'"
print " export LLM_API_KEY='your-key'" print " export LLM_API_KEY='your-key'"
print "" print ""
print " And enable in KCL: ai.enabled = true" print " And enable in Nickel: ai.enabled = true"
print "" print ""
print "🎯 AI integration COMPLETE!" print "🎯 AI integration COMPLETE!"

View file

@ -29,7 +29,9 @@ export-env {
($env.PROVISIONING_KLOUD_PATH? | default "") ($env.PROVISIONING_KLOUD_PATH? | default "")
} }
let config = (get-config) # Don't load config during export-env to avoid hanging on module parsing
# Config will be loaded on-demand when accessed later
let config = {}
# Try to get PROVISIONING path from config, environment, or detect from project structure # Try to get PROVISIONING path from config, environment, or detect from project structure
let provisioning_from_config = (config-get "provisioning.path" "" --config $config) let provisioning_from_config = (config-get "provisioning.path" "" --config $config)
@ -63,9 +65,16 @@ export-env {
# Just set it to a reasonable default # Just set it to a reasonable default
$env.PROVISIONING_CORE = "/usr/local/provisioning/core" $env.PROVISIONING_CORE = "/usr/local/provisioning/core"
} }
$env.PROVISIONING_PROVIDERS_PATH = ($env.PROVISIONING | path join "extensions" | path join "providers") $env.PROVISIONING_PROVIDERS_PATH = ($env.PROVISIONING | path join "extensions" | path join "providers")
$env.PROVISIONING_TASKSERVS_PATH = ($env.PROVISIONING | path join "extensions" | path join "taskservs") $env.PROVISIONING_COMPONENTS_PATH = ($env.PROVISIONING | path join "extensions" | path join "components")
$env.PROVISIONING_CLUSTERS_PATH = ($env.PROVISIONING | path join "extensions" | path join "clusters") # Keep for backward compat — points to taskservs/ if it exists, falls back to components/
let _ts_path = ($env.PROVISIONING | path join "extensions" | path join "taskservs")
$env.PROVISIONING_TASKSERVS_PATH = if ($env.PROVISIONING_COMPONENTS_PATH | path exists) {
$env.PROVISIONING_COMPONENTS_PATH
} else {
$_ts_path
}
$env.PROVISIONING_CLUSTERS_PATH = ($env.PROVISIONING | path join "extensions" | path join "clusters")
$env.PROVISIONING_RESOURCES = ($env.PROVISIONING | path join "resources" ) $env.PROVISIONING_RESOURCES = ($env.PROVISIONING | path join "resources" )
$env.PROVISIONING_NOTIFY_ICON = ($env.PROVISIONING_RESOURCES | path join "images"| path join "cloudnative.png") $env.PROVISIONING_NOTIFY_ICON = ($env.PROVISIONING_RESOURCES | path join "images"| path join "cloudnative.png")
@ -100,7 +109,7 @@ export-env {
$env.PROVISIONING_INFRA_PATH = ($env.PROVISIONING_KLOUD_PATH? | default $env.PROVISIONING_INFRA_PATH = ($env.PROVISIONING_KLOUD_PATH? | default
(config-get "paths.infra" | default $env.PWD ) | into string) (config-get "paths.infra" | default $env.PWD ) | into string)
$env.PROVISIONING_DFLT_SET = (config-get "paths.files.settings" | default "settings.k" | into string) $env.PROVISIONING_DFLT_SET = (config-get "paths.files.settings" | default "settings.ncl" | into string)
$env.NOW = (date now | format date "%Y_%m_%d_%H_%M_%S") $env.NOW = (date now | format date "%Y_%m_%d_%H_%M_%S")
$env.PROVISIONING_MATCH_DATE = ($env.PROVISIONING_MATCH_DATE? | default "%Y_%m") $env.PROVISIONING_MATCH_DATE = ($env.PROVISIONING_MATCH_DATE? | default "%Y_%m")
@ -120,10 +129,9 @@ export-env {
$env.PROVISIONING_GENERATE_DIRPATH = "generate" $env.PROVISIONING_GENERATE_DIRPATH = "generate"
$env.PROVISIONING_GENERATE_DEFSFILE = "defs.toml" $env.PROVISIONING_GENERATE_DEFSFILE = "defs.toml"
$env.PROVISIONING_KEYS_PATH = (config-get "paths.files.keys" ".keys.k" --config $config) $env.PROVISIONING_KEYS_PATH = (config-get "paths.files.keys" ".keys.ncl" --config $config)
$env.PROVISIONING_USE_KCL = if (^bash -c "type -P kcl" | is-not-empty) { true } else { false } $env.PROVISIONING_USE_NICKEL_PLUGIN = if ( (version).installed_plugins | str contains "nickel" ) { true } else { false }
$env.PROVISIONING_USE_KCL_PLUGIN = if ( (version).installed_plugins | str contains "kcl" ) { true } else { false }
#$env.PROVISIONING_J2_PARSER = ($env.PROVISIONING_$TOOLS_PATH | path join "parsetemplate.py") #$env.PROVISIONING_J2_PARSER = ($env.PROVISIONING_$TOOLS_PATH | path join "parsetemplate.py")
#$env.PROVISIONING_J2_PARSER = (^bash -c "type -P tera") #$env.PROVISIONING_J2_PARSER = (^bash -c "type -P tera")
$env.PROVISIONING_USE_TERA_PLUGIN = if ( (version).installed_plugins | str contains "tera" ) { true } else { false } $env.PROVISIONING_USE_TERA_PLUGIN = if ( (version).installed_plugins | str contains "tera" ) { true } else { false }
@ -145,18 +153,28 @@ export-env {
# This keeps the interactive experience clean while still supporting fallback to HTTP # This keeps the interactive experience clean while still supporting fallback to HTTP
$env.PROVISIONING_URL = ($env.PROVISIONING_URL? | default "https://provisioning.systems" | into string) $env.PROVISIONING_URL = ($env.PROVISIONING_URL? | default "https://provisioning.systems" | into string)
#let infra = ($env.PROVISIONING_ARGS | split row "-k" | try { get 1 } catch { | split row " " | try { get 1 } catch { null } "") } # Refactored from try-catch to do/complete for explicit error handling
#let parts_k = (do { $env.PROVISIONING_ARGS | split row "-k" | get 1 } | complete)
#let infra = if $parts_k.exit_code == 0 {
# ($parts_k.stdout | str trim)
#} else {
# let parts_space = (do { $env.PROVISIONING_ARGS | split row " " | get 1 } | complete)
# if $parts_space.exit_code == 0 { ($parts_space.stdout | str trim) } else { "" }
#}
#$env.CURR_KLOUD = if $infra == "" { (^pwd) } else { $infra } #$env.CURR_KLOUD = if $infra == "" { (^pwd) } else { $infra }
$env.PROVISIONING_USE_SOPS = (config-get "sops.use_sops" | default "age" | into string) $env.PROVISIONING_USE_SOPS = (config-get "sops.use_sops" | default "age" | into string)
$env.PROVISIONING_USE_KMS = (config-get "sops.use_kms" | default "" | into string) $env.PROVISIONING_USE_KMS = (config-get "sops.use_kms" | default "" | into string)
$env.PROVISIONING_SECRET_PROVIDER = (config-get "sops.secret_provider" | default "sops" | into string) $env.PROVISIONING_SECRET_PROVIDER = (config-get "sops.secret_provider" | default "sops" | into string)
# AI Configuration # AI Configuration
$env.PROVISIONING_AI_ENABLED = (config-get "ai.enabled" | default false | into bool | into string) $env.PROVISIONING_AI_ENABLED = (config-get "ai.enabled" | default false | into bool | into string)
$env.PROVISIONING_AI_PROVIDER = (config-get "ai.provider" | default "openai" | into string) $env.PROVISIONING_AI_PROVIDER = (config-get "ai.provider" | default "openai" | into string)
$env.PROVISIONING_LAST_ERROR = "" $env.PROVISIONING_LAST_ERROR = ""
# CLI Daemon Configuration
$env.PROVISIONING_DAEMON_URL = ($env.PROVISIONING_DAEMON_URL? | default "http://localhost:9091" | into string)
# For SOPS if settings below fails -> look at: sops_env.nu loaded when is need to set env context # For SOPS if settings below fails -> look at: sops_env.nu loaded when is need to set env context
let curr_infra = (config-get "paths.infra" "" --config $config) let curr_infra = (config-get "paths.infra" "" --config $config)
@ -196,10 +214,11 @@ export-env {
# $env.PROVISIONING_NO_TERMINAL = true # $env.PROVISIONING_NO_TERMINAL = true
# } # }
} }
# KCL Module Path Configuration # Nickel Module Path Configuration
# Set up KCL_MOD_PATH to help KCL resolve modules when running from different directories # Set up NICKEL_IMPORT_PATH to help Nickel resolve modules when running from different directories
$env.KCL_MOD_PATH = ($env.KCL_MOD_PATH? | default [] | append [ $env.NICKEL_IMPORT_PATH = ($env.NICKEL_IMPORT_PATH? | default [] | append [
($env.PROVISIONING | path join "kcl") $env.PROVISIONING
($env.PROVISIONING | path join "nickel")
($env.PROVISIONING_PROVIDERS_PATH) ($env.PROVISIONING_PROVIDERS_PATH)
$env.PWD $env.PWD
] | uniq | str join ":") ] | uniq | str join ":")
@ -242,10 +261,16 @@ export-env {
# Load providers environment settings... # Load providers environment settings...
# use ../../providers/prov_lib/env_middleware.nu # use ../../providers/prov_lib/env_middleware.nu
# Auto-load tera plugin if available for template rendering at env initialization
# Call this in a block that runs AFTER the export-env completes
if ( (version).installed_plugins | str contains "tera" ) {
(plugin use tera)
}
} }
export def "show_env" [ export def "show_env" [
]: nothing -> record { ] {
let env_vars = { let env_vars = {
PROVISIONING: $env.PROVISIONING, PROVISIONING: $env.PROVISIONING,
PROVISIONING_CORE: $env.PROVISIONING_CORE, PROVISIONING_CORE: $env.PROVISIONING_CORE,
@ -293,7 +318,7 @@ export def "show_env" [
PROVISIONING_KEYS_PATH: $env.PROVISIONING_KEYS_PATH, PROVISIONING_KEYS_PATH: $env.PROVISIONING_KEYS_PATH,
PROVISIONING_USE_KCL: $"($env.PROVISIONING_USE_KCL)", PROVISIONING_USE_nickel: $"($env.PROVISIONING_USE_nickel)",
PROVISIONING_J2_PARSER: ($env.PROVISIONING_J2_PARSER? | default ""), PROVISIONING_J2_PARSER: ($env.PROVISIONING_J2_PARSER? | default ""),
PROVISIONING_URL: $env.PROVISIONING_URL, PROVISIONING_URL: $env.PROVISIONING_URL,
@ -318,4 +343,10 @@ export def "show_env" [
} else { } else {
$env_vars $env_vars
} }
} }
# Get CLI daemon URL for template rendering and other daemon operations
# Returns the daemon endpoint, checking environment variable first, then default
export def get-cli-daemon-url [] {
$env.PROVISIONING_DAEMON_URL? | default "http://localhost:9091"
}

View file

@ -1,16 +1,143 @@
#!/usr/bin/env nu #!/usr/bin/env nu
# Minimal Help System - Fast Path without Config Loading # Minimal Help System - Fast Path with Fluent i18n Support
# This bypasses the full config system for instant help display # This bypasses the full config system for instant help display
# Uses Nushell's built-in ansi function for ANSI color codes # Uses Mozilla Fluent (.ftl) format for multilingual support
# Main help dispatcher - no config needed
def provisioning-help [category?: string = ""]: nothing -> string {
# If no category provided, show main help # Format alias: brackets in gray, inner text in category color
def format-alias [alias: string, color: string] {
if ($alias | is-empty) {
""
} else if ($alias | str starts-with "[") and ($alias | str ends-with "]") {
# Extract content between brackets (exclusive end range)
let inner = ($alias | str substring 1..<(-1))
(ansi d) + "[" + (ansi rst) + $color + $inner + (ansi rst) + (ansi d) + "]" + (ansi rst)
} else {
(ansi d) + $alias + (ansi rst)
}
}
# Format categories with tab-separated columns and colors
def format-categories [rows: list<list<string>>] {
let header = " Category\t\tAlias\t Description"
let separator = " ════════════════════════════════════════════════════════════════════"
let formatted_rows = (
$rows | each { |row|
let emoji = $row.0
let name = $row.1
let alias = $row.2
let desc = $row.3
# Assign color based on category name
let color = (match $name {
"infrastructure" => (ansi cyan)
"orchestration" => (ansi magenta)
"development" => (ansi green)
"workspace" => (ansi green)
"setup" => (ansi magenta)
"platform" => (ansi red)
"authentication" => (ansi yellow)
"plugins" => (ansi cyan)
"utilities" => (ansi green)
"tools" => (ansi yellow)
"vm" => (ansi white)
"diagnostics" => (ansi magenta)
"concepts" => (ansi yellow)
"guides" => (ansi blue)
"integrations" => (ansi cyan)
_ => ""
})
# Calculate tabs based on name length: 3 tabs for 6-10 char names, 2 tabs otherwise
let name_len = ($name | str length)
let name_tabs = match true {
_ if $name_len <= 11 => "\t\t"
_ => "\t"
}
# Format alias with brackets in gray and inner text in category color
let alias_formatted = (format-alias $alias $color)
let alias_len = ($alias | str length)
let alias_tabs = match true {
_ if ($alias_len == 8) => ""
_ if ($name_len <= 3) => "\t\t"
_ => "\t"
}
# Format: emoji + colored_name + tabs + colored_alias + tabs + description
$" ($emoji)($color)($name)((ansi rst))($name_tabs)($alias_formatted)($alias_tabs) ($desc)"
}
)
([$header, $separator] | append $formatted_rows | str join "\n")
}
# Get active locale from LANG environment variable
def get-active-locale [] {
let lang_env = ($env.LANG? | default "en_US")
let dot_idx = ($lang_env | str index-of ".")
let lang_part = (
if $dot_idx >= 0 {
$lang_env | str substring 0..<$dot_idx
} else {
$lang_env
}
)
let locale = ($lang_part | str replace "_" "-")
$locale
}
# Parse simple Fluent format and return record of strings
def parse-fluent [content: string] {
let lines = ($content | lines)
$lines | reduce -f {} { |line, strings|
if ($line | str starts-with "#") or ($line | str trim | is-empty) {
$strings
} else if ($line | str contains " = ") {
let idx = ($line | str index-of " = ")
if $idx != null {
let key = ($line | str substring 0..$idx | str trim)
let value = ($line | str substring ($idx + 3).. | str trim | str trim -c "\"")
$strings | insert $key $value
} else {
$strings
}
} else {
$strings
}
}
}
# Get a help string with fallback to English
def get-help-string [key: string] {
let locale = (get-active-locale)
# Use environment variable PROVISIONING as base path
let prov_path = ($env.PROVISIONING? | default "/usr/local/provisioning/provisioning")
let base_path = $"($prov_path)/locales"
let locale_file = $"($base_path)/($locale)/help.ftl"
let fallback_file = $"($base_path)/en-US/help.ftl"
let content = (
if ($locale_file | path exists) {
open $locale_file
} else {
open $fallback_file
}
)
let strings = (parse-fluent $content)
$strings | get $key | default "[$key]"
}
# Main help dispatcher
def provisioning-help [category?: string = ""] {
if ($category == "") { if ($category == "") {
return (help-main) return (help-main)
} }
# Try to match the category
let cat_lower = ($category | str downcase) let cat_lower = ($category | str downcase)
let result = (match $cat_lower { let result = (match $cat_lower {
"infrastructure" | "infra" => "infrastructure" "infrastructure" | "infra" => "infrastructure"
@ -29,18 +156,17 @@ def provisioning-help [category?: string = ""]: nothing -> string {
"concepts" | "concept" => "concepts" "concepts" | "concept" => "concepts"
"guides" | "guide" | "howto" => "guides" "guides" | "guide" | "howto" => "guides"
"integrations" | "integration" | "int" => "integrations" "integrations" | "integration" | "int" => "integrations"
"build" | "bi" | "build-image" => "build"
_ => "unknown" _ => "unknown"
}) })
# If unknown category, show error
if $result == "unknown" { if $result == "unknown" {
print $"❌ Unknown help category: \"($category)\"\n" print $"❌ Unknown help category: \"($category)\"\n"
print "Available help categories: infrastructure, orchestration, development, workspace, setup, platform," print "Available help categories: infrastructure, orchestration, development, workspace, setup, platform,"
print "authentication, mfa, plugins, utilities, tools, vm, diagnostics, concepts, guides, integrations" print "authentication, mfa, plugins, utilities, tools, vm, diagnostics, concepts, guides, integrations, build"
return "" return ""
} }
# Match valid category
match $result { match $result {
"infrastructure" => (help-infrastructure) "infrastructure" => (help-infrastructure)
"orchestration" => (help-orchestration) "orchestration" => (help-orchestration)
@ -58,379 +184,477 @@ def provisioning-help [category?: string = ""]: nothing -> string {
"concepts" => (help-concepts) "concepts" => (help-concepts)
"guides" => (help-guides) "guides" => (help-guides)
"integrations" => (help-integrations) "integrations" => (help-integrations)
"build" => (help-build)
_ => (help-main) _ => (help-main)
} }
} }
# Main help overview # Main help overview
def help-main []: nothing -> string { def help-main [] {
( let title = (get-help-string "help-main-title")
(ansi yellow) + (ansi bo) + "╔════════════════════════════════════════════════════════════════╗" + (ansi rst) + "\n" + let subtitle = (get-help-string "help-main-subtitle")
(ansi yellow) + (ansi bo) + "║" + (ansi rst) + " " + (ansi cyan) + (ansi bo) + "PROVISIONING SYSTEM" + (ansi rst) + " - Layered Infrastructure Automation " + (ansi yellow) + (ansi bo) + " ║" + (ansi rst) + "\n" + let categories = (get-help-string "help-main-categories")
(ansi yellow) + (ansi bo) + "╚════════════════════════════════════════════════════════════════╝" + (ansi rst) + "\n\n" + let hint = (get-help-string "help-main-categories-hint")
(ansi green) + (ansi bo) + "📚 COMMAND CATEGORIES" + (ansi rst) + " " + (ansi d) + "- Use 'provisioning help <category>' for details" + (ansi rst) + "\n\n" + let infra_desc = (get-help-string "help-main-infrastructure-desc")
let orch_desc = (get-help-string "help-main-orchestration-desc")
let dev_desc = (get-help-string "help-main-development-desc")
let ws_desc = (get-help-string "help-main-workspace-desc")
let plat_desc = (get-help-string "help-main-platform-desc")
let setup_desc = (get-help-string "help-main-setup-desc")
let auth_desc = (get-help-string "help-main-authentication-desc")
let plugins_desc = (get-help-string "help-main-plugins-desc")
let utils_desc = (get-help-string "help-main-utilities-desc")
let tools_desc = (get-help-string "help-main-tools-desc")
let vm_desc = (get-help-string "help-main-vm-desc")
let diag_desc = (get-help-string "help-main-diagnostics-desc")
let concepts_desc = (get-help-string "help-main-concepts-desc")
let guides_desc = (get-help-string "help-main-guides-desc")
let int_desc = (get-help-string "help-main-integrations-desc")
" " + (ansi cyan) + "🏗️ infrastructure" + (ansi rst) + " " + (ansi d) + "[infra]" + (ansi rst) + "\t\t Server, taskserv, cluster, VM, and infra management\n" + # Build output string
" " + (ansi magenta) + "⚡ orchestration" + (ansi rst) + " " + (ansi d) + "[orch]" + (ansi rst) + "\t\t Workflow, batch operations, and orchestrator control\n" + let header = (
" " + (ansi blue) + "🧩 development" + (ansi rst) + " " + (ansi d) + "[dev]" + (ansi rst) + "\t\t\t Module discovery, layers, versions, and packaging\n" + (ansi yellow) + "════════════════════════════════════════════════════════════════════════════" + (ansi rst) + "\n" +
" " + (ansi green) + "📁 workspace" + (ansi rst) + " " + (ansi d) + "[ws]" + (ansi rst) + "\t\t\t Workspace and template management\n" + " " + (ansi cyan) + (ansi bo) + ($title) + (ansi rst) + " - " + ($subtitle) + "\n" +
" " + (ansi magenta) + "⚙️ setup" + (ansi rst) + " " + (ansi d) + "[st]" + (ansi rst) + "\t\t\t\t System setup, configuration, and initialization\n" + (ansi yellow) + "════════════════════════════════════════════════════════════════════════════" + (ansi rst) + "\n\n"
" " + (ansi red) + "🖥️ platform" + (ansi rst) + " " + (ansi d) + "[plat]" + (ansi rst) + "\t\t\t Orchestrator, Control Center UI, MCP Server\n" +
" " + (ansi yellow) + "🔐 authentication" + (ansi rst) + " " + (ansi d) + "[auth]" + (ansi rst) + "\t\t JWT authentication, MFA, and sessions\n" +
" " + (ansi cyan) + "🔌 plugins" + (ansi rst) + " " + (ansi d) + "[plugin]" + (ansi rst) + "\t\t\t Plugin management and integration\n" +
" " + (ansi green) + "🛠️ utilities" + (ansi rst) + " " + (ansi d) + "[utils]" + (ansi rst) + "\t\t\t Cache, SOPS editing, providers, plugins, SSH\n" +
" " + (ansi yellow) + "🌉 integrations" + (ansi rst) + " " + (ansi d) + "[int]" + (ansi rst) + "\t\t\t Prov-ecosystem and provctl bridge\n" +
" " + (ansi green) + "🔍 diagnostics" + (ansi rst) + " " + (ansi d) + "[diag]" + (ansi rst) + "\t\t\t System status, health checks, and next steps\n" +
" " + (ansi magenta) + "📚 guides" + (ansi rst) + " " + (ansi d) + "[guide]" + (ansi rst) + "\t\t\t Quick guides and cheatsheets\n" +
" " + (ansi yellow) + "💡 concepts" + (ansi rst) + " " + (ansi d) + "[concept]" + (ansi rst) + "\t\t\t Understanding layers, modules, and architecture\n\n" +
(ansi green) + (ansi bo) + "🚀 QUICK START" + (ansi rst) + "\n\n" +
" 1. " + (ansi cyan) + "Understand the system" + (ansi rst) + ": provisioning help concepts\n" +
" 2. " + (ansi cyan) + "Create workspace" + (ansi rst) + ": provisioning workspace init my-infra --activate\n" +
" " + (ansi cyan) + "Or use interactive:" + (ansi rst) + " provisioning workspace init --interactive\n" +
" 3. " + (ansi cyan) + "Discover modules" + (ansi rst) + ": provisioning module discover taskservs\n" +
" 4. " + (ansi cyan) + "Create servers" + (ansi rst) + ": provisioning server create --infra my-infra\n" +
" 5. " + (ansi cyan) + "Deploy services" + (ansi rst) + ": provisioning taskserv create kubernetes\n\n" +
(ansi green) + (ansi bo) + "🔧 COMMON COMMANDS" + (ansi rst) + "\n\n" +
" provisioning server list - List all servers\n" +
" provisioning workflow list - List workflows\n" +
" provisioning module discover taskservs - Discover available taskservs\n" +
" provisioning layer show <workspace> - Show layer resolution\n" +
" provisioning config validate - Validate configuration\n" +
" provisioning help <category> - Get help on a topic\n\n" +
(ansi green) + (ansi bo) + " HELP TOPICS" + (ansi rst) + "\n\n" +
" provisioning help infrastructure " + (ansi d) + "[or: infra]" + (ansi rst) + " - Server/cluster lifecycle\n" +
" provisioning help orchestration " + (ansi d) + "[or: orch]" + (ansi rst) + " - Workflows and batch operations\n" +
" provisioning help development " + (ansi d) + "[or: dev]" + (ansi rst) + " - Module system and tools\n" +
" provisioning help workspace " + (ansi d) + "[or: ws]" + (ansi rst) + " - Workspace management\n" +
" provisioning help setup " + (ansi d) + "[or: st]" + (ansi rst) + " - System setup and configuration\n" +
" provisioning help platform " + (ansi d) + "[or: plat]" + (ansi rst) + " - Platform services\n" +
" provisioning help authentication " + (ansi d) + "[or: auth]" + (ansi rst) + " - Authentication system\n" +
" provisioning help utilities " + (ansi d) + "[or: utils]" + (ansi rst) + " - Cache, SOPS, providers, utilities\n" +
" provisioning help guides " + (ansi d) + "[or: guide]" + (ansi rst) + " - Step-by-step guides\n"
) )
let categories_header = (
(ansi green) + (ansi bo) + "📚 " + ($categories) + (ansi rst) + " " + (ansi d) + "- " + ($hint) + (ansi rst) + "\n\n"
)
# Build category rows: [emoji, name, alias, description]
let rows = [
["🏗️", "infrastructure", "[infra]", $infra_desc],
["⚡", "orchestration", "[orch]", $orch_desc],
["🧩", "development", "[dev]", $dev_desc],
["📁", "workspace", "[ws]", $ws_desc],
["⚙️", "setup", "[st]", $setup_desc],
["🖥️", "platform", "[plat]", $plat_desc],
["🔐", "authentication", "[auth]", $auth_desc],
["🔌", "plugins", "[plugin]", $plugins_desc],
["🛠️", "utilities", "[utils]", $utils_desc],
["🌉", "tools", "", $tools_desc],
["🔍", "vm", "", $vm_desc],
["📚", "diagnostics", "[diag]", $diag_desc],
["💡", "concepts", "", $concepts_desc],
["📖", "guides", "[guide]", $guides_desc],
["🌐", "integrations", "[int]", $int_desc],
["📦", "build", "[bi]", "Role image build, state, and watch"],
]
let categories_table = (format-categories $rows)
print ($header + $categories_header + $categories_table)
} }
# Infrastructure help # Infrastructure help
def help-infrastructure []: nothing -> string { def help-infrastructure [] {
let title = (get-help-string "help-infrastructure-title")
let intro = (get-help-string "help-infra-intro")
let server_header = (get-help-string "help-infra-server-header")
let server_create = (get-help-string "help-infra-server-create")
let server_list = (get-help-string "help-infra-server-list")
let server_delete = (get-help-string "help-infra-server-delete")
let server_ssh = (get-help-string "help-infra-server-ssh")
let server_price = (get-help-string "help-infra-server-price")
let taskserv_header = (get-help-string "help-infra-taskserv-header")
let taskserv_create = (get-help-string "help-infra-taskserv-create")
let taskserv_delete = (get-help-string "help-infra-taskserv-delete")
let taskserv_list = (get-help-string "help-infra-taskserv-list")
let taskserv_generate = (get-help-string "help-infra-taskserv-generate")
let taskserv_updates = (get-help-string "help-infra-taskserv-updates")
let cluster_header = (get-help-string "help-infra-cluster-header")
let cluster_create = (get-help-string "help-infra-cluster-create")
let cluster_delete = (get-help-string "help-infra-cluster-delete")
let cluster_list = (get-help-string "help-infra-cluster-list")
( (
(ansi yellow) + (ansi bo) + "INFRASTRUCTURE MANAGEMENT" + (ansi rst) + "\n\n" + (ansi yellow) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
"Manage servers, taskservs, clusters, and VMs across your infrastructure.\n\n" + ($intro) + "\n\n" +
(ansi green) + (ansi bo) + "SERVER COMMANDS" + (ansi rst) + "\n" + (ansi green) + (ansi bo) + ($server_header) + (ansi rst) + "\n" +
" provisioning server create --infra <name> - Create new server\n" + $" provisioning server create --infra <name> - ($server_create)\n" +
" provisioning server list - List all servers\n" + $" provisioning server list - ($server_list)\n" +
" provisioning server delete <server> - Delete a server\n" + $" provisioning server delete <server> - ($server_delete)\n" +
" provisioning server ssh <server> - SSH into server\n" + $" provisioning server ssh <server> - ($server_ssh)\n" +
" provisioning server price - Show server pricing\n\n" + $" provisioning server price - ($server_price)\n\n" +
(ansi green) + (ansi bo) + "TASKSERV COMMANDS" + (ansi rst) + "\n" + (ansi green) + (ansi bo) + ($taskserv_header) + (ansi rst) + "\n" +
" provisioning taskserv create <type> - Create taskserv\n" + $" provisioning taskserv create <type> - ($taskserv_create)\n" +
" provisioning taskserv delete <type> - Delete taskserv\n" + $" provisioning taskserv delete <type> - ($taskserv_delete)\n" +
" provisioning taskserv list - List taskservs\n" + $" provisioning taskserv list - ($taskserv_list)\n" +
" provisioning taskserv generate <type> - Generate taskserv config\n" + $" provisioning taskserv generate <type> - ($taskserv_generate)\n" +
" provisioning taskserv check-updates - Check for updates\n\n" + $" provisioning taskserv check-updates - ($taskserv_updates)\n\n" +
(ansi green) + (ansi bo) + "CLUSTER COMMANDS" + (ansi rst) + "\n" + (ansi green) + (ansi bo) + ($cluster_header) + (ansi rst) + "\n" +
" provisioning cluster create <name> - Create cluster\n" + $" provisioning cluster create <name> - ($cluster_create)\n" +
" provisioning cluster delete <name> - Delete cluster\n" + $" provisioning cluster delete <name> - ($cluster_delete)\n" +
" provisioning cluster list - List clusters\n" $" provisioning cluster list - ($cluster_list)\n"
) )
} }
# Orchestration help # Orchestration help
def help-orchestration []: nothing -> string { def help-orchestration [] {
let title = (get-help-string "help-orchestration-title")
let intro = (get-help-string "help-orch-intro")
let workflows_header = (get-help-string "help-orch-workflows-header")
let workflow_list = (get-help-string "help-orch-workflow-list")
let workflow_status = (get-help-string "help-orch-workflow-status")
let workflow_monitor = (get-help-string "help-orch-workflow-monitor")
let workflow_stats = (get-help-string "help-orch-workflow-stats")
let batch_header = (get-help-string "help-orch-batch-header")
let batch_submit = (get-help-string "help-orch-batch-submit")
let batch_list = (get-help-string "help-orch-batch-list")
let batch_status = (get-help-string "help-orch-batch-status")
let control_header = (get-help-string "help-orch-control-header")
let orch_start = (get-help-string "help-orch-start")
let orch_stop = (get-help-string "help-orch-stop")
( (
(ansi yellow) + (ansi bo) + "ORCHESTRATION AND WORKFLOWS" + (ansi rst) + "\n\n" + (ansi yellow) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
"Manage workflows, batch operations, and orchestrator services.\n\n" + ($intro) + "\n\n" +
(ansi green) + (ansi bo) + "WORKFLOW COMMANDS" + (ansi rst) + "\n" + (ansi green) + (ansi bo) + ($workflows_header) + (ansi rst) + "\n" +
" provisioning workflow list - List workflows\n" + $" provisioning workflow list - ($workflow_list)\n" +
" provisioning workflow status <id> - Get workflow status\n" + $" provisioning workflow status <id> - ($workflow_status)\n" +
" provisioning workflow monitor <id> - Monitor workflow progress\n" + $" provisioning workflow monitor <id> - ($workflow_monitor)\n" +
" provisioning workflow stats - Show workflow statistics\n\n" + $" provisioning workflow stats - ($workflow_stats)\n\n" +
(ansi green) + (ansi bo) + "BATCH COMMANDS" + (ansi rst) + "\n" + (ansi green) + (ansi bo) + ($batch_header) + (ansi rst) + "\n" +
" provisioning batch submit <file> - Submit batch workflow\n" + $" provisioning batch submit <file> - ($batch_submit)\n" +
" provisioning batch list - List batches\n" + $" provisioning batch list - ($batch_list)\n" +
" provisioning batch status <id> - Get batch status\n\n" + $" provisioning batch status <id> - ($batch_status)\n\n" +
(ansi green) + (ansi bo) + "ORCHESTRATOR COMMANDS" + (ansi rst) + "\n" + (ansi green) + (ansi bo) + ($control_header) + (ansi rst) + "\n" +
" provisioning orchestrator start - Start orchestrator\n" + $" provisioning orchestrator start - ($orch_start)\n" +
" provisioning orchestrator stop - Stop orchestrator\n" $" provisioning orchestrator stop - ($orch_stop)\n"
)
}
# Setup help with full Fluent support
def help-setup [] {
let title = (get-help-string "help-setup-title")
let intro = (get-help-string "help-setup-intro")
let initial = (get-help-string "help-setup-initial")
let system = (get-help-string "help-setup-system")
let system_desc = (get-help-string "help-setup-system-desc")
let workspace_header = (get-help-string "help-setup-workspace-header")
let workspace_cmd = (get-help-string "help-setup-workspace-cmd")
let workspace_desc = (get-help-string "help-setup-workspace-desc")
let workspace_init = (get-help-string "help-setup-workspace-init")
let provider_header = (get-help-string "help-setup-provider-header")
let provider_cmd = (get-help-string "help-setup-provider-cmd")
let provider_desc = (get-help-string "help-setup-provider-desc")
let provider_support = (get-help-string "help-setup-provider-support")
let platform_header = (get-help-string "help-setup-platform-header")
let platform_cmd = (get-help-string "help-setup-platform-cmd")
let platform_desc = (get-help-string "help-setup-platform-desc")
let platform_services = (get-help-string "help-setup-platform-services")
let modes = (get-help-string "help-setup-modes")
let interactive = (get-help-string "help-setup-interactive")
let config = (get-help-string "help-setup-config")
let defaults = (get-help-string "help-setup-defaults")
let phases = (get-help-string "help-setup-phases")
let phase_1 = (get-help-string "help-setup-phase-1")
let phase_2 = (get-help-string "help-setup-phase-2")
let phase_3 = (get-help-string "help-setup-phase-3")
let phase_4 = (get-help-string "help-setup-phase-4")
let phase_5 = (get-help-string "help-setup-phase-5")
let security = (get-help-string "help-setup-security")
let security_vault = (get-help-string "help-setup-security-vault")
let security_sops = (get-help-string "help-setup-security-sops")
let security_cedar = (get-help-string "help-setup-security-cedar")
let examples = (get-help-string "help-setup-examples")
let example_system = (get-help-string "help-setup-example-system")
let example_workspace = (get-help-string "help-setup-example-workspace")
let example_provider = (get-help-string "help-setup-example-provider")
let example_platform = (get-help-string "help-setup-example-platform")
(
(ansi magenta) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
($intro) + "\n\n" +
(ansi green) + (ansi bo) + ($initial) + (ansi rst) + "\n" +
" provisioning setup system - " + ($system) + "\n" +
" " + ($system_desc) + "\n\n" +
(ansi green) + (ansi bo) + ($workspace_header) + (ansi rst) + "\n" +
" " + ($workspace_cmd) + " - " + ($workspace_desc) + "\n" +
" " + ($workspace_init) + "\n\n" +
(ansi green) + (ansi bo) + ($provider_header) + (ansi rst) + "\n" +
" " + ($provider_cmd) + " - " + ($provider_desc) + "\n" +
" " + ($provider_support) + "\n\n" +
(ansi green) + (ansi bo) + ($platform_header) + (ansi rst) + "\n" +
" " + ($platform_cmd) + " - " + ($platform_desc) + "\n" +
" " + ($platform_services) + "\n\n" +
(ansi green) + (ansi bo) + ($modes) + (ansi rst) + "\n" +
" " + ($interactive) + "\n" +
" " + ($config) + "\n" +
" " + ($defaults) + "\n\n" +
(ansi cyan) + ($phases) + (ansi rst) + "\n" +
" " + ($phase_1) + "\n" +
" " + ($phase_2) + "\n" +
" " + ($phase_3) + "\n" +
" " + ($phase_4) + "\n" +
" " + ($phase_5) + "\n\n" +
(ansi cyan) + ($security) + (ansi rst) + "\n" +
" " + ($security_vault) + "\n" +
" " + ($security_sops) + "\n" +
" " + ($security_cedar) + "\n\n" +
(ansi green) + (ansi bo) + ($examples) + (ansi rst) + "\n" +
" " + ($example_system) + "\n" +
" " + ($example_workspace) + "\n" +
" " + ($example_provider) + "\n" +
" " + ($example_platform) + "\n"
) )
} }
# Development help # Development help
def help-development []: nothing -> string { def help-development [] {
let title = (get-help-string "help-development-title")
let intro = (get-help-string "help-development-intro")
let more_info = (get-help-string "help-more-info")
( (
(ansi yellow) + (ansi bo) + "DEVELOPMENT AND MODULES" + (ansi rst) + "\n\n" + (ansi blue) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
"Manage modules, layers, versions, and packaging.\n\n" + ($intro) + "\n\n" +
($more_info) + "\n"
(ansi green) + (ansi bo) + "MODULE COMMANDS" + (ansi rst) + "\n" +
" provisioning module discover <type> - Discover available modules\n" +
" provisioning module load <name> - Load a module\n" +
" provisioning module list - List loaded modules\n\n" +
(ansi green) + (ansi bo) + "LAYER COMMANDS" + (ansi rst) + "\n" +
" provisioning layer show <workspace> - Show layer resolution\n" +
" provisioning layer test <layer> - Test a layer\n"
) )
} }
# Workspace help # Workspace help
def help-workspace []: nothing -> string { def help-workspace [] {
let title = (get-help-string "help-workspace-title")
let intro = (get-help-string "help-workspace-intro")
let more_info = (get-help-string "help-more-info")
( (
(ansi yellow) + (ansi bo) + "WORKSPACE MANAGEMENT" + (ansi rst) + "\n\n" + (ansi green) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
"Initialize, switch, and manage workspaces.\n\n" + ($intro) + "\n\n" +
($more_info) + "\n"
(ansi green) + (ansi bo) + "WORKSPACE COMMANDS" + (ansi rst) + "\n" +
" provisioning workspace init [name] - Initialize new workspace\n" +
" provisioning workspace list - List all workspaces\n" +
" provisioning workspace active - Show active workspace\n" +
" provisioning workspace activate <name> - Activate workspace\n"
) )
} }
# Platform help # Platform help
def help-platform []: nothing -> string { def help-platform [] {
( (
(ansi yellow) + (ansi bo) + "PLATFORM SERVICES" + (ansi rst) + "\n\n" + (ansi red) + (ansi bo) + "🖥️ PLATFORM SERVICES" + (ansi rst) + "\n\n" +
"Manage orchestrator, control center, and MCP services.\n\n" +
(ansi green) + (ansi bo) + "ORCHESTRATOR SERVICE" + (ansi rst) + "\n" + (ansi green) + (ansi bo) + "[Control Center]" + (ansi rst) + " " + (ansi cyan) + (ansi bo) + "🌐 Web UI + Policy Engine" + (ansi rst) + "\n" +
" provisioning orchestrator start - Start orchestrator\n" + " " + (ansi blue) + "control-center server" + (ansi rst) + "\t\t\t - Start Cedar policy engine " + (ansi cyan) + "--port 8080" + (ansi rst) + "\n" +
" provisioning orchestrator status - Check status\n" " " + (ansi blue) + "control-center policy validate" + (ansi rst) + "\t - Validate Cedar policies\n" +
) " " + (ansi blue) + "control-center policy test" + (ansi rst) + "\t\t - Test policies with data\n" +
} " " + (ansi blue) + "control-center compliance soc2" + (ansi rst) + "\t - SOC2 compliance check\n" +
" " + (ansi blue) + "control-center compliance hipaa" + (ansi rst) + "\t - HIPAA compliance check\n\n" +
# Setup help (ansi cyan) + (ansi bo) + " 🎨 Features:" + (ansi rst) + "\n" +
def help-setup []: nothing -> string { " • " + (ansi green) + "Web-based UI" + (ansi rst) + "\t - WASM-powered control center interface\n" +
( " • " + (ansi green) + "Policy Engine" + (ansi rst) + "\t - Cedar policy evaluation and versioning\n" +
(ansi magenta) + (ansi bo) + "SYSTEM SETUP & CONFIGURATION" + (ansi rst) + "\n\n" + " • " + (ansi green) + "Compliance" + (ansi rst) + "\t - SOC2 Type II and HIPAA validation\n" +
"Initialize and configure the provisioning system.\n\n" + " • " + (ansi green) + "Security" + (ansi rst) + "\t\t - JWT auth, MFA, RBAC, anomaly detection\n" +
" • " + (ansi green) + "Audit Trail" + (ansi rst) + "\t - Complete compliance audit logging\n\n" +
(ansi green) + (ansi bo) + "INITIAL SETUP" + (ansi rst) + "\n" + (ansi green) + (ansi bo) + "[Orchestrator]" + (ansi rst) + " Hybrid Rust/Nushell Coordination\n" +
" provisioning setup system - Complete system setup wizard\n" + " " + (ansi blue) + "orchestrator start" + (ansi rst) + " - Start orchestrator [--background]\n" +
" Interactive TUI mode (default), auto-detect OS, setup platform services\n\n" + " " + (ansi blue) + "orchestrator stop" + (ansi rst) + " - Stop orchestrator\n" +
" " + (ansi blue) + "orchestrator status" + (ansi rst) + " - Check if running\n" +
" " + (ansi blue) + "orchestrator health" + (ansi rst) + " - Health check with diagnostics\n" +
" " + (ansi blue) + "orchestrator logs" + (ansi rst) + " - View logs [--follow]\n\n" +
(ansi green) + (ansi bo) + "WORKSPACE SETUP" + (ansi rst) + "\n" + (ansi green) + (ansi bo) + "[MCP Server]" + (ansi rst) + " AI-Assisted DevOps Integration\n" +
" provisioning setup workspace <name> - Create new workspace\n" + " " + (ansi blue) + "mcp-server start" + (ansi rst) + " - Start MCP server [--debug]\n" +
" Initialize workspace structure, set active providers\n\n" + " " + (ansi blue) + "mcp-server status" + (ansi rst) + " - Check server status\n\n" +
(ansi green) + (ansi bo) + "PROVIDER SETUP" + (ansi rst) + "\n" + (ansi cyan) + (ansi bo) + " 🤖 Features:" + (ansi rst) + "\n" +
" provisioning setup provider <name> - Configure cloud provider\n" + " • " + (ansi green) + "AI-Powered Parsing" + (ansi rst) + " - Natural language to infrastructure\n" +
" Supported: upcloud, aws, hetzner, local\n\n" + " • " + (ansi green) + "Multi-Provider" + (ansi rst) + "\t - AWS, UpCloud, Local support\n" +
" • " + (ansi green) + "Ultra-Fast" + (ansi rst) + "\t - Microsecond latency, 1000x faster than Python\n" +
" • " + (ansi green) + "Type Safe" + (ansi rst) + "\t\t - Compile-time guarantees with zero runtime errors\n\n" +
(ansi green) + (ansi bo) + "PLATFORM SETUP" + (ansi rst) + "\n" + (ansi green) + (ansi bo) + "🌐 REST API ENDPOINTS" + (ansi rst) + "\n\n" +
" provisioning setup platform - Setup platform services\n" + (ansi yellow) + "Control Center" + (ansi rst) + " - " + (ansi d) + "http://localhost:8080" + (ansi rst) + "\n" +
" Orchestrator, Control Center, KMS Service, MCP Server\n\n" + " • POST /policies/evaluate - Evaluate policy decisions\n" +
" • GET /policies - List all policies\n" +
" • GET /compliance/soc2 - SOC2 compliance check\n" +
" • GET /anomalies - List detected anomalies\n\n" +
(ansi green) + (ansi bo) + "SETUP MODES" + (ansi rst) + "\n" + (ansi yellow) + "Orchestrator" + (ansi rst) + " - " + (ansi d) + "http://localhost:8080" + (ansi rst) + "\n" +
" --interactive - Beautiful TUI wizard (default)\n" + " • GET /health - Health check\n" +
" --config <file> - Load settings from TOML/YAML file\n" + " • GET /tasks - List all tasks\n" +
" --defaults - Auto-detect and use sensible defaults\n\n" + " • POST /workflows/servers/create - Server workflow\n" +
" • POST /workflows/batch/submit - Batch workflow\n\n" +
(ansi cyan) + "SETUP PHASES:" + (ansi rst) + "\n" + (ansi d) + "💡 Control Center provides a " + (ansi cyan) + (ansi bo) + "web-based UI" + (ansi rst) + (ansi d) + " for managing policies!\n" +
" 1. System Setup - Initialize OS-appropriate paths and services\n" + " Access at: " + (ansi cyan) + "http://localhost:8080" + (ansi rst) + (ansi d) + " after starting the server\n" +
" 2. Workspace - Create infrastructure project workspace\n" + " Example: provisioning control-center server --port 8080" + (ansi rst) + "\n"
" 3. Providers - Register cloud providers with credentials\n" +
" 4. Platform - Launch orchestration and control services\n" +
" 5. Validation - Verify all components working\n\n" +
(ansi cyan) + "SECURITY:" + (ansi rst) + "\n" +
" • RustyVault: Primary credentials storage (encrypt/decrypt at rest)\n" +
" • SOPS/Age: Bootstrap encryption for RustyVault key only\n" +
" • Cedar: Fine-grained access policies\n\n" +
(ansi green) + (ansi bo) + "QUICK START EXAMPLES" + (ansi rst) + "\n" +
" provisioning setup system --interactive # TUI setup (recommended)\n" +
" provisioning setup workspace myproject # Create workspace\n" +
" provisioning setup provider upcloud # Configure provider\n" +
" provisioning setup platform --mode solo # Setup services\n"
) )
} }
# Authentication help # Authentication help
def help-authentication []: nothing -> string { def help-authentication [] {
let title = (get-help-string "help-authentication-title")
let intro = (get-help-string "help-authentication-intro")
let more_info = (get-help-string "help-more-info")
( (
(ansi yellow) + (ansi bo) + "AUTHENTICATION AND SECURITY" + (ansi rst) + "\n\n" + (ansi yellow) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
"Manage user authentication, MFA, and security.\n\n" + ($intro) + "\n\n" +
($more_info) + "\n"
(ansi green) + (ansi bo) + "LOGIN AND SESSIONS" + (ansi rst) + "\n" +
" provisioning login - Login to system\n" +
" provisioning logout - Logout from system\n"
) )
} }
# MFA help # MFA help
def help-mfa []: nothing -> string { def help-mfa [] {
let title = (get-help-string "help-mfa-title")
let intro = (get-help-string "help-mfa-intro")
let more_info = (get-help-string "help-more-info")
( (
(ansi yellow) + (ansi bo) + "MULTI-FACTOR AUTHENTICATION" + (ansi rst) + "\n\n" + (ansi yellow) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
"Setup and manage MFA methods.\n\n" + ($intro) + "\n\n" +
($more_info) + "\n"
(ansi green) + (ansi bo) + "TOTP (Time-based One-Time Password)" + (ansi rst) + "\n" +
" provisioning mfa totp enroll - Enroll in TOTP\n" +
" provisioning mfa totp verify <code> - Verify TOTP code\n"
) )
} }
# Plugins help # Plugins help
def help-plugins []: nothing -> string { def help-plugins [] {
let title = (get-help-string "help-plugins-title")
let intro = (get-help-string "help-plugins-intro")
let more_info = (get-help-string "help-more-info")
( (
(ansi yellow) + (ansi bo) + "PLUGIN MANAGEMENT" + (ansi rst) + "\n\n" + (ansi cyan) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
"Install, configure, and manage Nushell plugins.\n\n" + ($intro) + "\n\n" +
($more_info) + "\n"
(ansi green) + (ansi bo) + "PLUGIN COMMANDS" + (ansi rst) + "\n" +
" provisioning plugin list - List installed plugins\n" +
" provisioning plugin install <name> - Install plugin\n"
) )
} }
# Utilities help # Utilities help
def help-utilities []: nothing -> string { def help-utilities [] {
let title = (get-help-string "help-utilities-title")
let intro = (get-help-string "help-utilities-intro")
let more_info = (get-help-string "help-more-info")
( (
(ansi yellow) + (ansi bo) + "UTILITIES & TOOLS" + (ansi rst) + "\n\n" + (ansi green) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
"Cache management, secrets, providers, and miscellaneous tools.\n\n" + ($intro) + "\n\n" +
($more_info) + "\n"
(ansi green) + (ansi bo) + "CACHE COMMANDS" + (ansi rst) + "\n" +
" provisioning cache status - Show cache status and statistics\n" +
" provisioning cache config show - Display all cache settings\n" +
" provisioning cache config get <setting> - Get specific cache setting\n" +
" provisioning cache config set <setting> <val> - Set cache setting\n" +
" provisioning cache list [--type TYPE] - List cached items\n" +
" provisioning cache clear [--type TYPE] - Clear cache\n\n" +
(ansi green) + (ansi bo) + "OTHER UTILITIES" + (ansi rst) + "\n" +
" provisioning sops <file> - Edit encrypted file\n" +
" provisioning encrypt <file> - Encrypt configuration\n" +
" provisioning decrypt <file> - Decrypt configuration\n" +
" provisioning providers list - List available providers\n" +
" provisioning plugin list - List installed plugins\n" +
" provisioning ssh <host> - Connect to server\n\n" +
(ansi cyan) + "Cache Features:" + (ansi rst) + "\n" +
" • Intelligent TTL management (KCL: 30m, SOPS: 15m, Final: 5m)\n" +
" • 95-98% faster config loading\n" +
" • SOPS cache with 0600 permissions\n" +
" • Works without active workspace\n\n" +
(ansi cyan) + "Cache Configuration:" + (ansi rst) + "\n" +
" provisioning cache config set ttl_kcl 3000 # Set KCL TTL\n" +
" provisioning cache config set enabled false # Disable cache\n"
) )
} }
# Tools help # Tools help
def help-tools []: nothing -> string { def help-tools [] {
let title = (get-help-string "help-tools-title")
let intro = (get-help-string "help-tools-intro")
let more_info = (get-help-string "help-more-info")
( (
(ansi yellow) + (ansi bo) + "TOOLS & DEPENDENCIES" + (ansi rst) + "\n\n" + (ansi yellow) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
"Tool and dependency management for provisioning system.\n\n" + ($intro) + "\n\n" +
($more_info) + "\n"
(ansi green) + (ansi bo) + "INSTALLATION" + (ansi rst) + "\n" +
" provisioning tools install - Install all tools\n" +
" provisioning tools install <tool> - Install specific tool\n" +
" provisioning tools install --update - Force reinstall all tools\n\n" +
(ansi green) + (ansi bo) + "VERSION MANAGEMENT" + (ansi rst) + "\n" +
" provisioning tools check - Check all tool versions\n" +
" provisioning tools versions - Show configured versions\n" +
" provisioning tools check-updates - Check for available updates\n" +
" provisioning tools apply-updates - Apply configuration updates\n\n" +
(ansi green) + (ansi bo) + "TOOL INFORMATION" + (ansi rst) + "\n" +
" provisioning tools show - Display tool information\n" +
" provisioning tools show all - Show all tools\n" +
" provisioning tools show provider - Show provider information\n\n" +
(ansi green) + (ansi bo) + "PINNING" + (ansi rst) + "\n" +
" provisioning tools pin <tool> - Pin tool to current version\n" +
" provisioning tools unpin <tool> - Unpin tool\n\n" +
(ansi cyan) + "Examples:" + (ansi rst) + "\n" +
" provisioning tools check # Check all versions\n" +
" provisioning tools check hcloud # Check hcloud status\n" +
" provisioning tools check-updates # Check for updates\n" +
" provisioning tools install # Install all tools\n"
) )
} }
# VM help # VM help
def help-vm []: nothing -> string { def help-vm [] {
let title = (get-help-string "help-vm-title")
let intro = (get-help-string "help-vm-intro")
let more_info = (get-help-string "help-more-info")
( (
(ansi yellow) + (ansi bo) + "VIRTUAL MACHINE OPERATIONS" + (ansi rst) + "\n\n" + (ansi green) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
"Manage virtual machines and hypervisors.\n\n" + ($intro) + "\n\n" +
($more_info) + "\n"
(ansi green) + (ansi bo) + "VM COMMANDS" + (ansi rst) + "\n" +
" provisioning vm create <name> - Create VM\n" +
" provisioning vm delete <name> - Delete VM\n"
) )
} }
# Diagnostics help # Diagnostics help
def help-diagnostics []: nothing -> string { def help-diagnostics [] {
let title = (get-help-string "help-diagnostics-title")
let intro = (get-help-string "help-diagnostics-intro")
let more_info = (get-help-string "help-more-info")
( (
(ansi yellow) + (ansi bo) + "DIAGNOSTICS AND HEALTH CHECKS" + (ansi rst) + "\n\n" + (ansi magenta) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
"Check system status and diagnose issues.\n\n" + ($intro) + "\n\n" +
($more_info) + "\n"
(ansi green) + (ansi bo) + "STATUS COMMANDS" + (ansi rst) + "\n" +
" provisioning status - Overall system status\n" +
" provisioning health - Health check\n"
) )
} }
# Concepts help # Concepts help
def help-concepts []: nothing -> string { def help-concepts [] {
let title = (get-help-string "help-concepts-title")
let intro = (get-help-string "help-concepts-intro")
let more_info = (get-help-string "help-more-info")
( (
(ansi yellow) + (ansi bo) + "PROVISIONING CONCEPTS" + (ansi rst) + "\n\n" + (ansi yellow) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
"Learn about the core concepts of the provisioning system.\n\n" + ($intro) + "\n\n" +
($more_info) + "\n"
(ansi green) + (ansi bo) + "FUNDAMENTAL CONCEPTS" + (ansi rst) + "\n" +
" workspace - A logical grouping of infrastructure\n" +
" infrastructure - Configuration for a specific deployment\n" +
" layer - Composable configuration units\n" +
" taskserv - Infrastructure services (Kubernetes, etc.)\n"
) )
} }
# Guides help # Guides help
def help-guides []: nothing -> string { def help-guides [] {
let title = (get-help-string "help-guides-title")
let intro = (get-help-string "help-guides-intro")
let more_info = (get-help-string "help-more-info")
( (
(ansi yellow) + (ansi bo) + "QUICK GUIDES AND CHEATSHEETS" + (ansi rst) + "\n\n" + (ansi blue) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
"Step-by-step guides for common tasks.\n\n" + ($intro) + "\n\n" +
($more_info) + "\n"
(ansi green) + (ansi bo) + "GETTING STARTED" + (ansi rst) + "\n" +
" provisioning guide from-scratch - Deploy from scratch\n" +
" provisioning guide quickstart - Quick reference\n" +
" provisioning guide setup-system - Complete system setup guide\n\n" +
(ansi green) + (ansi bo) + "SETUP GUIDES" + (ansi rst) + "\n" +
" provisioning guide setup-workspace - Create and configure workspaces\n" +
" provisioning guide setup-providers - Configure cloud providers\n" +
" provisioning guide setup-platform - Setup platform services\n\n" +
(ansi green) + (ansi bo) + "INFRASTRUCTURE MANAGEMENT" + (ansi rst) + "\n" +
" provisioning guide update - Update existing infrastructure safely\n" +
" provisioning guide customize - Customize with layers and templates\n\n" +
(ansi green) + (ansi bo) + "QUICK COMMANDS" + (ansi rst) + "\n" +
" provisioning sc - Quick command reference (fastest)\n" +
" provisioning guide list - Show all available guides\n"
) )
} }
# Integrations help # Integrations help
def help-integrations []: nothing -> string { def help-integrations [] {
let title = (get-help-string "help-integrations-title")
let intro = (get-help-string "help-integrations-intro")
let more_info = (get-help-string "help-more-info")
( (
(ansi yellow) + (ansi bo) + "ECOSYSTEM AND INTEGRATIONS" + (ansi rst) + "\n\n" + (ansi cyan) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
"Integration with external systems and tools.\n\n" + ($intro) + "\n\n" +
($more_info) + "\n"
)
}
(ansi green) + (ansi bo) + "ECOSYSTEM COMPONENTS" + (ansi rst) + "\n" + # Build help — role image management
" ProvCtl - Provisioning Control tool\n" + def help-build [] {
" Orchestrator - Workflow engine\n" (
(ansi yellow) + (ansi bo) + "🏗️ BUILD — Role Image Management" + (ansi rst) + "\n\n" +
(ansi d) + "Pre-built provider snapshots (nixos-generators → Hetzner snapshot).\n" +
"Snapshot IDs and freshness tracked in ~/.config/provisioning/images/.\n" +
"Server creation runs a pre-flight check before rendering templates." + (ansi rst) + "\n\n" +
(ansi green) + (ansi bo) + "[Image Lifecycle]" + (ansi rst) + "\n" +
" " + (ansi blue) + "build image create <role>" + (ansi rst) + " - Build snapshot for role, save state\n" +
" Options: --infra <path> --check --provider <p>\n" +
" " + (ansi blue) + "build image list" + (ansi rst) + " - Show all role states (provider, snapshot_id, fresh)\n" +
" Options: --provider <p>\n" +
" " + (ansi blue) + "build image update <role>" + (ansi rst) + " - Delete stale snapshot and rebuild\n" +
" Options: --infra <path> --provider <p> --check\n" +
" " + (ansi blue) + "build image delete <role>" + (ansi rst) + " - Remove snapshot from provider + local state\n" +
" Options: --provider <p> --yes\n\n" +
(ansi green) + (ansi bo) + "[Monitoring]" + (ansi rst) + "\n" +
" " + (ansi blue) + "build image watch" + (ansi rst) + " - Poll freshness of all role images (loop)\n" +
" Options: --interval <min> --auto-build --notify-only\n" +
" --provider <p> --infra <path>\n\n" +
(ansi green) + (ansi bo) + "[Shortcuts]" + (ansi rst) + "\n" +
" " + (ansi d) + "b, build" + (ansi rst) + " → build domain\n" +
" " + (ansi d) + "bi, build-image" + (ansi rst) + " → build image\n\n" +
(ansi green) + (ansi bo) + "[Examples]" + (ansi rst) + "\n" +
" provisioning build image list\n" +
" provisioning build image create cp --infra workspaces/librecloud_hetzner/infra/wuji --check\n" +
" provisioning build image create cp --infra workspaces/librecloud_hetzner/infra/wuji\n" +
" provisioning build image update worker --infra workspaces/librecloud_hetzner/infra/wuji\n" +
" provisioning build image delete storage --yes\n" +
" provisioning build image watch --interval 30 --auto-build\n\n" +
(ansi green) + (ansi bo) + "[State Files]" + (ansi rst) + "\n" +
" Location: ~/.config/provisioning/images/<provider>-<role>.ncl\n" +
" Schema: provisioning/schemas/infrastructure/images/\n" +
" Workspace roles: workspaces/librecloud_hetzner/infra/wuji/images.ncl\n"
) )
} }
@ -440,5 +664,3 @@ def main [...args: string] {
let help_text = (provisioning-help $category) let help_text = (provisioning-help $category)
print $help_text print $help_text
} }
# NOTE: No entry point needed - functions are called directly from bash script

165
nulib/images/create.nu Normal file
View file

@ -0,0 +1,165 @@
# Image create — render build template, execute, capture snapshot ID, persist state.
use ./state.nu *
use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval]
# Load the ImageRole definition from the workspace images.ncl for a given role name.
def load-image-role [infra: string, role: string]: nothing -> record {
let images_ncl = ($infra | path join "images.ncl")
if not ($images_ncl | path exists) {
error make { msg: $"images.ncl not found at ($images_ncl)" }
}
let data = (ncl-eval $images_ncl [])
let roles = ($data | get image_roles? | default {})
let role_def = ($roles | get -o $role)
if ($role_def | is-empty) {
error make { msg: $"Role '($role)' not defined in ($images_ncl)" }
}
$role_def
}
# Build template context and render via tera plugin.
def render-build-template [role_def: record, infra: string, check: bool]: nothing -> string {
let tera_loaded = (plugin list | where name == "tera" | length) > 0
if not $tera_loaded { plugin use tera }
let provider = ($role_def | get provider? | default "hetzner")
let tpl_name = ($role_def | get template_name? | default "hetzner_build_image.j2")
let tpl_path = ($env.PROVISIONING | path join "extensions" | path join "providers"
| path join $provider | path join "templates" | path join $tpl_name)
if not ($tpl_path | path exists) {
error make { msg: $"Build template not found: ($tpl_path)" }
}
# Calculate flake directory: go up 2 levels from infra/wuji to workspace root, then add nixos
let infra_expanded = ($infra | path expand)
let workspace_root = ($infra_expanded | path dirname | path dirname)
let flake_dir = ($workspace_root | path join "nixos")
let ctx = {
image_role: $role_def,
ssh_key: ($role_def | get ssh_key? | default ""),
location: ($role_def | get location? | default "nbg1"),
flake_dir: $flake_dir,
now: ((date now) | format date "%Y-%m-%dT%H:%M:%SZ"),
provisioning_version: ($env.PROVISIONING_VERSION? | default "0.0.0"),
check: $check,
}
$ctx | tera-render $tpl_path
}
# Parse the SNAPSHOT_ID=<id> line from build script stdout.
def extract-snapshot-id [output: string]: nothing -> string {
let line = ($output | lines | find "SNAPSHOT_ID=" | first?)
if ($line | is-empty) {
error make { msg: "Build script did not emit SNAPSHOT_ID=<id>" }
}
$line | str replace "SNAPSHOT_ID=" "" | str trim
}
export def image-create [
role: string
--infra: string = ""
--check
] {
let infra_path = if ($infra | is-empty) {
let ws = ($env.PROVISIONING_WORKSPACE? | default "")
if ($ws | is-empty) {
error make { msg: "Specify --infra <path> or set PROVISIONING_WORKSPACE" }
}
$ws | path join "infra"
} else {
let expanded = ($infra | path expand)
# Detect if we're in a project subdirectory and path was duplicated
# E.g., ran from /project/workspaces with --infra workspaces/... → /project/workspaces/workspaces/...
if ($expanded | str contains "workspaces/workspaces") or ($expanded | str contains "infra/infra") {
let cwd = (pwd)
let infra_parts = ($infra | split row "/")
let first_part = ($infra_parts | get 0)
# If we're in a subdirectory that matches the first part of --infra, strip it
if ($cwd | str contains $first_part) {
let adjusted = ($infra_parts | skip 1 | str join "/")
let adjusted_path = ($adjusted | path expand)
if ($adjusted_path | path exists) {
$adjusted_path
} else {
error make {
msg: $"Path duplication detected in: ($expanded)\n\nYou appear to be in a subdirectory. Either:\n 1. Run from project root: cd ($env.HOME)/project-provisioning\n 2. Use absolute path: --infra ($env.HOME)/project-provisioning/workspaces/...\n 3. Use relative from current dir: --infra librecloud_hetzner/infra/wuji"
}
}
} else {
$expanded
}
} else {
$expanded
}
}
let role_def = (load-image-role $infra_path $role)
let provider = ($role_def | get provider? | default "hetzner")
print $"Building image role '($role)' for provider '($provider)'"
if $check {
let script = (render-build-template $role_def $infra_path true)
print "── [check mode] rendered build script ──"
print $script
print "── no snapshot created ──"
return
}
let script = (render-build-template $role_def $infra_path false)
let tmp_dir = ($env.TMPDIR? | default "/tmp")
let tmp_path = ($tmp_dir | path join $"build_image_($provider)_($role).sh")
$script | save --force $tmp_path
^chmod +x $tmp_path
print $"Executing build script: ($tmp_path)"
print ""
# Execute script - redirect output to log file for visibility
let tmp_log = ($tmp_dir | path join $"build_image_($provider)_($role).log")
# Run bash script via shell, capturing output to log file
# Don't use Nushell's external command error handling - let shell handle it
^sh -c $"bash -x ($tmp_path) >($tmp_log) 2>&1 || true"
# ALWAYS print build output, even if bash failed
if ($tmp_log | path exists) {
print ""
print "=== Build Output ==="
print (open $tmp_log)
print ""
}
# Check if script had any error (look for error: in output)
if ($tmp_log | path exists) {
let log_content = (open $tmp_log)
if ($log_content | str contains "error:") {
print "❌ BUILD FAILED - see output above for details"
exit 1
}
}
let snapshot_id = (extract-snapshot-id (open $tmp_log))
print $"Snapshot created: ($snapshot_id)"
let os_base = ($role_def | get os_base? | default "debian-12")
let labels = ($role_def | get labels? | default {})
image-state-write $provider $role {
provider: $provider,
role: $role,
snapshot_id: $snapshot_id,
built_at: ((date now) | format date "%Y-%m-%dT%H:%M:%SZ"),
last_used: null,
os_base: $os_base,
labels: $labels,
}
print $"State saved: (image-state-path $provider $role)"
}

37
nulib/images/delete.nu Normal file
View file

@ -0,0 +1,37 @@
# Image delete — remove Hetzner snapshot and clear local state file.
use ./state.nu *
export def image-delete [
role: string
--provider: string = "hetzner"
--yes
] {
let state = (image-state-read $provider $role)
if $state.snapshot_id == "SNAPSHOT_PENDING" {
print $"Role '($role)' has no snapshot to delete."
return
}
if not $yes {
print $"About to delete snapshot ($state.snapshot_id) for role '($provider)/($role)'"
let answer = (input "Confirm? [y/N] ")
if ($answer | str downcase | str trim) != "y" {
print "Aborted."
return
}
}
let result = (^hcloud image delete $state.snapshot_id | complete)
if $result.exit_code != 0 {
error make { msg: $"hcloud image delete failed: ($result.stderr)" }
}
let path = (image-state-path $provider $role)
if ($path | path exists) {
rm $path
}
print $"Deleted snapshot ($state.snapshot_id) and removed state for '($provider)/($role)'."
}

27
nulib/images/list.nu Normal file
View file

@ -0,0 +1,27 @@
# Image list — display current state of all role image snapshots.
use ./state.nu *
export def image-list [--provider: string = ""]: nothing -> list<record> {
let states = (image-state-list --provider $provider)
if ($states | length) == 0 {
print "No image role states found."
print "Build one with: provisioning build image create <role> --infra <path>"
return []
}
let rows = ($states | each {|s|
let fresh = (do {
image-state-is-fresh $s.provider $s.role
} catch { false })
{
provider: $s.provider,
role: $s.role,
snapshot_id: $s.snapshot_id,
built_at: ($s.built_at? | default "—"),
fresh: $fresh,
os_base: ($s.os_base? | default "—"),
}
})
$rows | table
$rows
}

109
nulib/images/state.nu Normal file
View file

@ -0,0 +1,109 @@
# Image state management — read/write role image state from ~/.config/provisioning/images/
use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval-soft]
export def image-state-path [provider: string, role: string]: nothing -> string {
let dir = ($env.HOME | path join ".config" | path join "provisioning" | path join "images")
$dir | path join $"($provider)-($role).ncl"
}
export def image-state-dir []: nothing -> string {
$env.HOME | path join ".config" | path join "provisioning" | path join "images"
}
# Read state file. Returns a record with ImageRoleState fields.
# If the file does not exist, returns a pending-state record.
export def image-state-read [provider: string, role: string]: nothing -> record {
let path = (image-state-path $provider $role)
if not ($path | path exists) {
return {
provider: $provider,
role: $role,
snapshot_id: "SNAPSHOT_PENDING",
built_at: null,
last_used: null,
os_base: "unknown",
labels: {},
}
}
let result = (ncl-eval-soft $path [] (error make { msg: $"Failed to parse image state ($path)" }))
$result
}
# Write state file as a Nickel record literal.
export def image-state-write [provider: string, role: string, state: record]: nothing -> nothing {
let dir = (image-state-dir)
let path = (image-state-path $provider $role)
if not ($dir | path exists) {
^mkdir -p $dir
}
let built_at_val = if ($state.built_at? | is-empty) { "null" } else { $"\"($state.built_at)\"" }
let last_used_val = if ($state.last_used? | is-empty) { "null" } else { $"\"($state.last_used)\"" }
let labels_str = (
$state.labels?
| default {}
| items {|k, v| $" ($k) = \"($v)\"," }
| str join "\n"
)
let content = $"
\{
provider = \"($state.provider)\",
role = \"($state.role)\",
snapshot_id = \"($state.snapshot_id)\",
built_at = ($built_at_val),
last_used = ($last_used_val),
os_base = \"($state.os_base | default "unknown")\",
labels = \{
($labels_str)
\},
\}
" | str trim
$content | save --force $path
}
# List state files. Optionally filter by provider.
export def image-state-list [--provider: string = ""]: nothing -> list<record> {
let dir = (image-state-dir)
if not ($dir | path exists) {
return []
}
let files = (ls $dir | where name =~ '\.ncl$' | get name)
let states = ($files | each {|f|
ncl-eval-soft $f [] null
} | where { $in != null })
if ($provider | is-empty) {
$states
} else {
$states | where provider == $provider
}
}
# Returns true if the snapshot exists and is within freshness_days of built_at.
export def image-state-is-fresh [provider: string, role: string]: nothing -> bool {
let state = (image-state-read $provider $role)
if $state.snapshot_id == "SNAPSHOT_PENDING" { return false }
if ($state.built_at | is-empty) { return false }
let freshness_days = 30
let built = ($state.built_at | into datetime)
let age_days = ((date now) - $built | into duration | $in / 1day)
$age_days <= $freshness_days
}
# Update only the snapshot_id and built_at fields in an existing state file.
export def image-state-set-snapshot [provider: string, role: string, snapshot_id: string]: nothing -> nothing {
let existing = (image-state-read $provider $role)
let updated = ($existing | merge {
snapshot_id: $snapshot_id,
built_at: ((date now) | format date "%Y-%m-%dT%H:%M:%SZ"),
})
image-state-write $provider $role $updated
}
# Touch last_used timestamp for the given role state.
export def image-state-touch-used [provider: string, role: string]: nothing -> nothing {
let existing = (image-state-read $provider $role)
let updated = ($existing | merge {
last_used: ((date now) | format date "%Y-%m-%dT%H:%M:%SZ"),
})
image-state-write $provider $role $updated
}

22
nulib/images/update.nu Normal file
View file

@ -0,0 +1,22 @@
# Image update — delete old snapshot then rebuild role image.
use ./state.nu *
use ./delete.nu *
use ./create.nu *
export def image-update [
role: string
--provider: string = "hetzner"
--infra: string = ""
--check
] {
let state = (image-state-read $provider $role)
if $state.snapshot_id != "SNAPSHOT_PENDING" {
print $"Removing stale snapshot ($state.snapshot_id) for '($provider)/($role)'..."
image-delete $role --provider $provider --yes
} else {
print $"No existing snapshot — proceeding with fresh build."
}
image-create $role --infra $infra --check=$check
}

49
nulib/images/watch.nu Normal file
View file

@ -0,0 +1,49 @@
# Image watch — periodic freshness monitor for role image snapshots.
use ./state.nu *
use ./create.nu *
# Poll all role image states every N minutes and report stale snapshots.
export def image-watch [
--interval: int = 60
--auto-build
--notify-only
--provider: string = ""
--infra: string = ""
] {
print $"Image watch started (interval: ($interval)m, auto-build: ($auto_build))"
print "Press Ctrl-C to stop."
print ""
loop {
let states = (image-state-list --provider $provider)
let now_str = ((date now) | format date "%Y-%m-%dT%H:%M:%SZ")
print $"[($now_str)] Checking ($states | length) role image(s)..."
for state in $states {
let fresh = (do {
image-state-is-fresh $state.provider $state.role
} catch { false })
if $state.snapshot_id == "SNAPSHOT_PENDING" {
print $"[PENDING] ($state.provider)/($state.role) — no snapshot built"
} else if not $fresh {
let built = ($state.built_at? | default "unknown")
print $"[STALE] ($state.provider)/($state.role) — last built: ($built) snapshot: ($state.snapshot_id)"
if $auto_build and not $notify_only {
print $" → auto-building ($state.role)..."
do {
image-create $state.role --infra $infra
} catch { |e|
print $" ✗ build failed: ($e.msg)"
}
}
} else {
print $"[OK] ($state.provider)/($state.role) — snapshot: ($state.snapshot_id)"
}
}
print ""
sleep ($interval * 60sec)
}
}

View file

@ -1,5 +1,6 @@
use lib_provisioning * # Star-import removed (ADR-025 Phase 4). File still invoked by legacy
use ../lib_provisioning/user/config.nu [get-active-workspace get-workspace-path] # `provisioning infra` runner; proper thin handler refactor pending.
use lib_provisioning/user/config.nu [get-active-workspace get-workspace-path]
# Removed broken imports - these modules don't exist # Removed broken imports - these modules don't exist
# use create.nu * # use create.nu *
# use servers/delete.nu * # use servers/delete.nu *
@ -37,9 +38,9 @@ export def "main list" [
# List directory contents, filter for directories that: # List directory contents, filter for directories that:
# 1. Do not start with underscore (not hidden/system) # 1. Do not start with underscore (not hidden/system)
# 2. Are directories # 2. Are directories
# 3. Contain a settings.k file (marks it as a real infra) # 3. Contain a settings.ncl file (marks it as a real infra)
let infras = (ls -s $infra_dir | where {|it| let infras = (ls -s $infra_dir | where {|it|
((($it.name | str starts-with "_") == false) and ($it.type == "dir") and (($infra_dir | path join $it.name "settings.k") | path exists)) ((($it.name | str starts-with "_") == false) and ($it.type == "dir") and (($infra_dir | path join $it.name "settings.ncl") | path exists))
} | each {|it| $it.name} | sort) } | each {|it| $it.name} | sort)
if ($infras | length) > 0 { if ($infras | length) > 0 {
@ -109,7 +110,7 @@ export def "main validate" [
# List available infras # List available infras
if ($infra_dir | path exists) { if ($infra_dir | path exists) {
let infras = (ls -s $infra_dir | where {|it| let infras = (ls -s $infra_dir | where {|it|
((($it.name | str starts-with "_") == false) and ($it.type == "dir") and (($infra_dir | path join $it.name "settings.k") | path exists)) ((($it.name | str starts-with "_") == false) and ($it.type == "dir") and (($infra_dir | path join $it.name "settings.ncl") | path exists))
} | each {|it| $it.name} | sort) } | each {|it| $it.name} | sort)
for infra in $infras { for infra in $infras {
@ -127,8 +128,8 @@ export def "main validate" [
} }
# Load infrastructure configuration files # Load infrastructure configuration files
let settings_file = ($target_path | path join "settings.k") let settings_file = ($target_path | path join "settings.ncl")
let servers_file = ($target_path | path join "defs" "servers.k") let servers_file = ($target_path | path join "defs" "servers.ncl")
if not ($settings_file | path exists) { if not ($settings_file | path exists) {
_print $"❌ Settings file not found: ($settings_file)" _print $"❌ Settings file not found: ($settings_file)"
@ -161,7 +162,7 @@ export def "main validate" [
# Extract hostname - look for: hostname = "..." # Extract hostname - look for: hostname = "..."
let hostname = if ($block | str contains "hostname =") { let hostname = if ($block | str contains "hostname =") {
let lines = ($block | split row "\n" | where { |l| (($l | str contains "hostname =") and not ($l | str starts-with "#")) }) let lines = ($block | lines | where { |l| (($l | str contains "hostname =") and not ($l | str starts-with "#")) })
if ($lines | length) > 0 { if ($lines | length) > 0 {
let line = ($lines | first) let line = ($lines | first)
let match = ($line | split row "\"" | get 1? | default "") let match = ($line | split row "\"" | get 1? | default "")
@ -179,7 +180,7 @@ export def "main validate" [
# Extract plan - look for: plan = "..." (not commented, prefer last one) # Extract plan - look for: plan = "..." (not commented, prefer last one)
let plan = if ($block | str contains "plan =") { let plan = if ($block | str contains "plan =") {
let lines = ($block | split row "\n" | where { |l| (($l | str contains "plan =") and ($l | str contains "\"") and not ($l | str starts-with "#")) }) let lines = ($block | lines | where { |l| (($l | str contains "plan =") and ($l | str contains "\"") and not ($l | str starts-with "#")) })
if ($lines | length) > 0 { if ($lines | length) > 0 {
let line = ($lines | last) let line = ($lines | last)
($line | split row "\"" | get 1? | default "") ($line | split row "\"" | get 1? | default "")
@ -192,7 +193,7 @@ export def "main validate" [
# Extract total storage - look for: total = ... # Extract total storage - look for: total = ...
let storage = if ($block | str contains "total =") { let storage = if ($block | str contains "total =") {
let lines = ($block | split row "\n" | where { |l| (($l | str contains "total =") and not ($l | str starts-with "#")) }) let lines = ($block | lines | where { |l| (($l | str contains "total =") and not ($l | str starts-with "#")) })
if ($lines | length) > 0 { if ($lines | length) > 0 {
let line = ($lines | first) let line = ($lines | first)
let value = ($line | str trim | split row "=" | get 1? | str trim) let value = ($line | str trim | split row "=" | get 1? | str trim)
@ -206,7 +207,7 @@ export def "main validate" [
# Extract IP - look for: network_private_ip = "..." # Extract IP - look for: network_private_ip = "..."
let ip = if ($block | str contains "network_private_ip =") { let ip = if ($block | str contains "network_private_ip =") {
let lines = ($block | split row "\n" | where { |l| (($l | str contains "network_private_ip =") and not ($l | str starts-with "#")) }) let lines = ($block | lines | where { |l| (($l | str contains "network_private_ip =") and not ($l | str starts-with "#")) })
if ($lines | length) > 0 { if ($lines | length) > 0 {
let line = ($lines | first) let line = ($lines | first)
($line | split row "\"" | get 1? | default "") ($line | split row "\"" | get 1? | default "")
@ -220,7 +221,7 @@ export def "main validate" [
# Extract taskservs - look for all lines with {name = "..."} within taskservs array # Extract taskservs - look for all lines with {name = "..."} within taskservs array
let taskservs_list = if ($block | str contains "taskservs = [") { let taskservs_list = if ($block | str contains "taskservs = [") {
let taskservs_section = ($block | split row "taskservs = [" | get 1? | split row "]" | first | default "") let taskservs_section = ($block | split row "taskservs = [" | get 1? | split row "]" | first | default "")
let lines = ($taskservs_section | split row "\n" | where { |l| (($l | str contains "name =") and not ($l | str starts-with "#")) }) let lines = ($taskservs_section | lines | where { |l| (($l | str contains "name =") and not ($l | str starts-with "#")) })
let taskservs = ($lines | each { |l| let taskservs = ($lines | each { |l|
let parts = ($l | split row "name =") let parts = ($l | split row "name =")
let value_part = if ($parts | length) > 1 { ($parts | get 1) } else { "" } let value_part = if ($parts | length) > 1 { ($parts | get 1) } else { "" }

View file

@ -1,6 +1,320 @@
#!/usr/bin/env nu const LOG_ANSI = {
"CRITICAL": (ansi red_bold),
"ERROR": (ansi red),
"WARNING": (ansi yellow),
"INFO": (ansi default),
"DEBUG": (ansi default_dimmed)
}
# KMS Service Module export def log-ansi [] {$LOG_ANSI}
# Unified interface for Key Management Service operations
export use service.nu * const LOG_LEVEL = {
"CRITICAL": 50,
"ERROR": 40,
"WARNING": 30,
"INFO": 20,
"DEBUG": 10
}
export def log-level [] {$LOG_LEVEL}
const LOG_PREFIX = {
"CRITICAL": "CRT",
"ERROR": "ERR",
"WARNING": "WRN",
"INFO": "INF",
"DEBUG": "DBG"
}
export def log-prefix [] {$LOG_PREFIX}
const LOG_SHORT_PREFIX = {
"CRITICAL": "C",
"ERROR": "E",
"WARNING": "W",
"INFO": "I",
"DEBUG": "D"
}
export def log-short-prefix [] {$LOG_SHORT_PREFIX}
const LOG_FORMATS = {
log: "%ANSI_START%%DATE%|%LEVEL%|%MSG%%ANSI_STOP%"
date: "%Y-%m-%dT%H:%M:%S%.3f"
}
export-env {
$env.NU_LOG_FORMAT = $env.NU_LOG_FORMAT? | default $LOG_FORMATS.log
$env.NU_LOG_DATE_FORMAT = $env.NU_LOG_DATE_FORMAT? | default $LOG_FORMATS.date
}
const LOG_TYPES = {
"CRITICAL": {
"ansi": $LOG_ANSI.CRITICAL,
"level": $LOG_LEVEL.CRITICAL,
"prefix": $LOG_PREFIX.CRITICAL,
"short_prefix": $LOG_SHORT_PREFIX.CRITICAL
},
"ERROR": {
"ansi": $LOG_ANSI.ERROR,
"level": $LOG_LEVEL.ERROR,
"prefix": $LOG_PREFIX.ERROR,
"short_prefix": $LOG_SHORT_PREFIX.ERROR
},
"WARNING": {
"ansi": $LOG_ANSI.WARNING,
"level": $LOG_LEVEL.WARNING,
"prefix": $LOG_PREFIX.WARNING,
"short_prefix": $LOG_SHORT_PREFIX.WARNING
},
"INFO": {
"ansi": $LOG_ANSI.INFO,
"level": $LOG_LEVEL.INFO,
"prefix": $LOG_PREFIX.INFO,
"short_prefix": $LOG_SHORT_PREFIX.INFO
},
"DEBUG": {
"ansi": $LOG_ANSI.DEBUG,
"level": $LOG_LEVEL.DEBUG,
"prefix": $LOG_PREFIX.DEBUG,
"short_prefix": $LOG_SHORT_PREFIX.DEBUG
}
}
def parse-string-level [
level: string
] {
let level = ($level | str upcase)
if $level in [$LOG_PREFIX.CRITICAL $LOG_SHORT_PREFIX.CRITICAL "CRIT" "CRITICAL"] {
$LOG_LEVEL.CRITICAL
} else if $level in [$LOG_PREFIX.ERROR $LOG_SHORT_PREFIX.ERROR "ERROR"] {
$LOG_LEVEL.ERROR
} else if $level in [$LOG_PREFIX.WARNING $LOG_SHORT_PREFIX.WARNING "WARN" "WARNING"] {
$LOG_LEVEL.WARNING
} else if $level in [$LOG_PREFIX.DEBUG $LOG_SHORT_PREFIX.DEBUG "DEBUG"] {
$LOG_LEVEL.DEBUG
} else {
$LOG_LEVEL.INFO
}
}
def parse-int-level [
level: int,
--short (-s)
] {
if $level >= $LOG_LEVEL.CRITICAL {
if $short {
$LOG_SHORT_PREFIX.CRITICAL
} else {
$LOG_PREFIX.CRITICAL
}
} else if $level >= $LOG_LEVEL.ERROR {
if $short {
$LOG_SHORT_PREFIX.ERROR
} else {
$LOG_PREFIX.ERROR
}
} else if $level >= $LOG_LEVEL.WARNING {
if $short {
$LOG_SHORT_PREFIX.WARNING
} else {
$LOG_PREFIX.WARNING
}
} else if $level >= $LOG_LEVEL.INFO {
if $short {
$LOG_SHORT_PREFIX.INFO
} else {
$LOG_PREFIX.INFO
}
} else {
if $short {
$LOG_SHORT_PREFIX.DEBUG
} else {
$LOG_PREFIX.DEBUG
}
}
}
def current-log-level [] {
let env_level = ($env.NU_LOG_LEVEL? | default $LOG_LEVEL.INFO)
let result = (do { $env_level | into int } | complete)
if $result.exit_code == 0 { $result.stdout } else { parse-string-level $env_level }
}
def now [] {
date now | format date ($env.NU_LOG_DATE_FORMAT? | default $LOG_FORMATS.date)
}
def handle-log [
message: string,
formatting: record,
format_string: string,
short: bool
] {
let log_format = $format_string | default -e $env.NU_LOG_FORMAT? | default $LOG_FORMATS.log
let prefix = if $short {
$formatting.short_prefix
} else {
$formatting.prefix
}
custom $message $log_format $formatting.level --level-prefix $prefix --ansi $formatting.ansi
}
# Logging module
#
# Log formatting placeholders:
# - %MSG%: message to be logged
# - %DATE%: date of log
# - %LEVEL%: string prefix for the log level
# - %ANSI_START%: ansi formatting
# - %ANSI_STOP%: literally (ansi reset)
#
# Note: All placeholders are optional, so "" is still a valid format
#
# Example: $"%ANSI_START%%DATE%|%LEVEL%|(ansi u)%MSG%%ANSI_STOP%"
export def main [] {}
# Log a critical message
export def critical [
message: string, # A message
--short (-s) # Whether to use a short prefix
--format (-f): string # A format (for further reference: help std log)
] {
let format = $format | default ""
handle-log $message ($LOG_TYPES.CRITICAL) $format $short
}
# Log an error message
export def error [
message: string, # A message
--short (-s) # Whether to use a short prefix
--format (-f): string # A format (for further reference: help std log)
] {
let format = $format | default ""
handle-log $message ($LOG_TYPES.ERROR) $format $short
}
# Log a warning message
export def warning [
message: string, # A message
--short (-s) # Whether to use a short prefix
--format (-f): string # A format (for further reference: help std log)
] {
let format = $format | default ""
handle-log $message ($LOG_TYPES.WARNING) $format $short
}
# Log an info message
export def info [
message: string, # A message
--short (-s) # Whether to use a short prefix
--format (-f): string # A format (for further reference: help std log)
] {
let format = $format | default ""
handle-log $message ($LOG_TYPES.INFO) $format $short
}
# Log a debug message
export def debug [
message: string, # A message
--short (-s) # Whether to use a short prefix
--format (-f): string # A format (for further reference: help std log)
] {
let format = $format | default ""
handle-log $message ($LOG_TYPES.DEBUG) $format $short
}
def log-level-deduction-error [
type: string
span: record<start: int, end: int>
log_level: int
] {
error make {
msg: $"(ansi red_bold)Cannot deduce ($type) for given log level: ($log_level).(ansi reset)"
label: {
text: ([
"Invalid log level."
$" Available log levels in log-level:"
($LOG_LEVEL | to text | lines | each {|it| $" ($it)" } | to text)
] | str join "\n")
span: $span
}
}
}
# Log a message with a specific format and verbosity level, with either configurable or auto-deduced %LEVEL% and %ANSI_START% placeholder extensions
export def custom [
message: string, # A message
format: string, # A format (for further reference: help std log)
log_level: int # A log level (has to be one of the log-level values for correct ansi/prefix deduction)
--level-prefix (-p): string # %LEVEL% placeholder extension
--ansi (-a): string # %ANSI_START% placeholder extension
] {
if (current-log-level) > ($log_level) {
return
}
let valid_levels_for_defaulting = [
$LOG_LEVEL.CRITICAL
$LOG_LEVEL.ERROR
$LOG_LEVEL.WARNING
$LOG_LEVEL.INFO
$LOG_LEVEL.DEBUG
]
let prefix = if ($level_prefix | is-empty) {
if ($log_level not-in $valid_levels_for_defaulting) {
log-level-deduction-error "log level prefix" (metadata $log_level).span $log_level
}
parse-int-level $log_level
} else {
$level_prefix
}
let use_color = ($env.config?.use_ansi_coloring? | $in != false)
let ansi = if not $use_color {
""
} else if ($ansi | is-empty) {
if ($log_level not-in $valid_levels_for_defaulting) {
log-level-deduction-error "ansi" (metadata $log_level).span $log_level
}
(
$LOG_TYPES
| values
| each {|record|
if ($record.level == $log_level) {
$record.ansi
}
} | first
)
} else {
$ansi
}
print --stderr (
$format
| str replace --all "%MSG%" $message
| str replace --all "%DATE%" (now)
| str replace --all "%LEVEL%" $prefix
| str replace --all "%ANSI_START%" $ansi
| str replace --all "%ANSI_STOP%" (ansi reset)
)
}
def "nu-complete log-level" [] {
$LOG_LEVEL | transpose description value
}
# Change logging level
export def --env set-level [level: int@"nu-complete log-level"] {
# Keep it as a string so it can be passed to child processes
$env.NU_LOG_LEVEL = $level | into string
}

181
nulib/lib_minimal.nu Normal file
View file

@ -0,0 +1,181 @@
#!/usr/bin/env nu
# Minimal Library - Fast path for interactive commands
# NO config loading, NO platform bootstrap
# Follows: @.claude/guidelines/nushell/NUSHELL_GUIDELINES.md
# Error handling: Result pattern (hybrid, no try-catch)
use lib_provisioning/result.nu *
# Get user config path (centralized location)
# Rule 2: Single purpose function
# Cross-platform support (macOS, Linux, Windows)
def get-user-config-path [] {
let home = $env.HOME
let os_name = (uname | get operating-system | str downcase)
let config_path = match $os_name {
"darwin" => $"($home)/Library/Application Support/provisioning/user_config.yaml",
_ => $"($home)/.config/provisioning/user_config.yaml"
}
$config_path | path expand
}
# List all registered workspaces
# Rule 1: Explicit types, Rule 4: Early returns
# Rule 2: Single purpose - only list workspaces
# Result: {ok: list, err: null} on success; {ok: null, err: message} on error
export def workspace-list [] {
let user_config = (get-user-config-path)
# Guard: Early return if config doesn't exist
if not ($user_config | path exists) {
return (ok [])
}
# Guard: File is guaranteed to exist, open directly (no try-catch)
let config = (open $user_config)
let active = ($config | get --optional active_workspace | default "")
let workspaces = ($config | get --optional workspaces | default [])
# Guard: No workspaces registered
if ($workspaces | length) == 0 {
return (ok [])
}
# Pure transformation
let result = ($workspaces | each {|ws|
{
name: $ws.name
path: $ws.path
active: ($ws.name == $active)
last_used: ($ws | get --optional last_used | default "Never")
}
})
ok $result
}
# Get active workspace name
# Rule 1: Explicit types, Rule 4: Early returns
# Result: {ok: string, err: null} on success; {ok: null, err: message} on error
export def workspace-active [] {
let user_config = (get-user-config-path)
# Guard: Config doesn't exist
if not ($user_config | path exists) {
return (ok "")
}
# Guard: File exists, read directly
let active_name = (open $user_config | get --optional active_workspace | default "")
ok $active_name
}
# Get workspace info by name
# Rule 1: Explicit types, Rule 4: Early returns
# Result: {ok: record, err: null} on success; {ok: null, err: message} on error
export def workspace-info [name: string] {
# Guard: Input validation
if ($name | is-empty) {
return (err "workspace name is required")
}
let user_config = (get-user-config-path)
# Guard: Config doesn't exist
if not ($user_config | path exists) {
return (ok {name: $name, path: "", exists: false})
}
# Guard: File exists, read directly
let config = (open $user_config)
let workspaces = ($config | get --optional workspaces | default [])
let ws = ($workspaces | where { $in.name == $name } | first)
# Guard: Workspace not found
if ($ws | is-empty) {
return (ok {name: $name, path: "", exists: false, default_infra: "", infrastructures: []})
}
# Collect infra dirs with server counts
let infra_root = ($ws.path | path join 'infra')
let infrastructures = if ($infra_root | path exists) {
ls $infra_root
| where type == 'dir'
| each {|inf|
let inf_name = ($inf.name | path basename)
let sf_direct = ($infra_root | path join $inf_name | path join 'servers.ncl')
let sf_defs = ($infra_root | path join $inf_name | path join 'defs' | path join 'servers.ncl')
let sf = if ($sf_direct | path exists) { $sf_direct } else { $sf_defs }
let server_count = if ($sf | path exists) {
open $sf --raw | split row "\n" | where {|l| $l =~ 'hostname\s*=\s*"' } | length
} else { 0 }
{ name: $inf_name, servers: $server_count }
}
} else { [] }
ok {
name: $ws.name
path: $ws.path
exists: true
last_used: ($ws | get --optional last_used | default "Never")
default_infra: ($ws | get --optional default_infra | default "")
infrastructures: $infrastructures
}
}
# Quick status check (orchestrator health + active workspace)
# Rule 1: Explicit types, Rule 4: Early returns
# Result: {ok: record, err: null} on success; {ok: null, err: message} on error
export def status-quick [] {
# Guard: HTTP check with do/complete pattern (no try-catch)
let health_result = (do { http get --max-time 2sec "http://localhost:9090/health" } | complete)
let orch_health = if ($health_result.exit_code == 0) { $health_result.stdout } else { null }
let orch_status = if ($orch_health != null) { "running" } else { "stopped" }
# Guard: Get active workspace safely
let ws_result = (workspace-active)
let active_ws = (if (is-ok $ws_result) { $ws_result.ok } else { "" })
# Pure transformation
ok {
orchestrator: $orch_status
workspace: $active_ws
timestamp: (date now | format date "%Y-%m-%d %H:%M:%S")
}
}
# Display essential environment variables
# Rule 1: Explicit types, Rule 8: Pure function (read-only)
# Result: {ok: record, err: null} on success; {ok: null, err: message} on error
export def env-quick [] {
# Pure transformation with optional operator
let vars = {
PROVISIONING_ROOT: ($env.PROVISIONING_ROOT? | default "not set")
PROVISIONING_ENV: ($env.PROVISIONING_ENV? | default "not set")
PROVISIONING_DEBUG: ($env.PROVISIONING_DEBUG? | default "false")
HOME: $env.HOME
PWD: $env.PWD
}
ok $vars
}
# Show quick help for fast-path commands
# Rule 1: Explicit types, Rule 8: Pure function
export def quick-help [] {
"Provisioning CLI - Fast Path Commands
Quick Commands (< 100ms):
workspace list List all registered workspaces
workspace active Show currently active workspace
status Quick health check
env Show essential environment variables
help [command] Show help for a command
For full help:
provisioning help Show all available commands
provisioning help <command> Show help for specific command"
}

View file

@ -5,20 +5,23 @@ This module provides comprehensive AI capabilities for the provisioning system,
## Features ## Features
### 🤖 **Core AI Capabilities** ### 🤖 **Core AI Capabilities**
- Natural language KCL file generation
- Natural language Nickel file generation
- Intelligent template creation - Intelligent template creation
- Infrastructure query processing - Infrastructure query processing
- Configuration validation and improvement - Configuration validation and improvement
- Chat/webhook integration - Chat/webhook integration
### 📝 **KCL Generation Types** ### 📝 **Nickel Generation Types**
- **Server Configurations** (`servers.k`) - Generate server definitions with storage, networking, and services
- **Provider Defaults** (`*_defaults.k`) - Create provider-specific default settings - **Server Configurations** (`servers.ncl`) - Generate server definitions with storage, networking, and services
- **Settings Configuration** (`settings.k`) - Generate main infrastructure settings - **Provider Defaults** (`*_defaults.ncl`) - Create provider-specific default settings
- **Settings Configuration** (`settings.ncl`) - Generate main infrastructure settings
- **Cluster Configuration** - Kubernetes and container orchestration setups - **Cluster Configuration** - Kubernetes and container orchestration setups
- **Task Services** - Individual service configurations - **Task Services** - Individual service configurations
### 🔧 **AI Providers Supported** ### 🔧 **AI Providers Supported**
- **OpenAI** (GPT-4, GPT-3.5) - **OpenAI** (GPT-4, GPT-3.5)
- **Anthropic Claude** (Claude-3.5 Sonnet, Claude-3) - **Anthropic Claude** (Claude-3.5 Sonnet, Claude-3)
- **Generic/Local** (Ollama, local LLM APIs) - **Generic/Local** (Ollama, local LLM APIs)
@ -26,8 +29,9 @@ This module provides comprehensive AI capabilities for the provisioning system,
## Configuration ## Configuration
### Environment Variables ### Environment Variables
```bash ```bash
# Enable AI functionality #Enable AI functionality
export PROVISIONING_AI_ENABLED=true export PROVISIONING_AI_ENABLED=true
# Set provider # Set provider
@ -44,8 +48,9 @@ export PROVISIONING_AI_TEMPERATURE="0.3"
export PROVISIONING_AI_MAX_TOKENS="2048" export PROVISIONING_AI_MAX_TOKENS="2048"
``` ```
### KCL Configuration ### Nickel Configuration
```kcl
```nickel
import settings import settings
settings.Settings { settings.Settings {
@ -63,6 +68,7 @@ settings.Settings {
``` ```
### YAML Configuration (`ai.yaml`) ### YAML Configuration (`ai.yaml`)
```yaml ```yaml
enabled: true enabled: true
provider: "openai" provider: "openai"
@ -80,28 +86,30 @@ enable_webhook_ai: false
### 🎯 **Command Line Interface** ### 🎯 **Command Line Interface**
#### Generate Infrastructure with AI #### Generate Infrastructure with AI
```bash ```bash
# Interactive generation #Interactive generation
./provisioning ai generate --interactive ./provisioning ai generate --interactive
# Generate specific configurations # Generate specific configurations
./provisioning ai gen -t server -p upcloud -i "3 Kubernetes nodes with Ceph storage" -o servers.k ./provisioning ai gen -t server -p upcloud -i "3 Kubernetes nodes with Ceph storage" -o servers.ncl
./provisioning ai gen -t defaults -p aws -i "Production environment in us-west-2" -o aws_defaults.k ./provisioning ai gen -t defaults -p aws -i "Production environment in us-west-2" -o aws_defaults.ncl
./provisioning ai gen -t settings -i "E-commerce platform with secrets management" -o settings.k ./provisioning ai gen -t settings -i "E-commerce platform with secrets management" -o settings.ncl
# Enhanced generation with validation # Enhanced generation with validation
./provisioning generate-ai servers "High-availability Kubernetes cluster with 3 control planes and 5 workers" --validate --provider upcloud ./provisioning generate-ai servers "High-availability Kubernetes cluster with 3 control planes and 5 workers" --validate --provider upcloud
# Improve existing configurations # Improve existing configurations
./provisioning ai improve -i existing_servers.k -o improved_servers.k ./provisioning ai improve -i existing_servers.ncl -o improved_servers.ncl
# Validate and fix KCL files # Validate and fix Nickel files
./provisioning ai validate -i servers.k ./provisioning ai validate -i servers.ncl
``` ```
#### Interactive AI Chat #### Interactive AI Chat
```bash ```bash
# Start chat session #Start chat session
./provisioning ai chat ./provisioning ai chat
# Single query # Single query
@ -116,21 +124,23 @@ enable_webhook_ai: false
### 🧠 **Programmatic API** ### 🧠 **Programmatic API**
#### Generate KCL Files #### Generate Nickel Files
```nushell ```nushell
use lib_provisioning/ai/templates.nu * use lib_provisioning/ai/templates.nu *
# Generate server configuration # Generate server configuration
let servers = (generate_server_kcl "3 Kubernetes nodes for production workloads" "upcloud" "servers.k") let servers = (generate_server_nickel "3 Kubernetes nodes for production workloads" "upcloud" "servers.ncl")
# Generate provider defaults # Generate provider defaults
let defaults = (generate_defaults_kcl "High-availability setup in EU region" "aws" "aws_defaults.k") let defaults = (generate_defaults_nickel "High-availability setup in EU region" "aws" "aws_defaults.ncl")
# Generate complete infrastructure # Generate complete infrastructure
let result = (generate_full_infra_ai "E-commerce platform with database and caching" "upcloud" "" false) let result = (generate_full_infra_ai "E-commerce platform with database and caching" "upcloud" "" false)
``` ```
#### Process Natural Language Queries #### Process Natural Language Queries
```nushell ```nushell
use lib_provisioning/ai/lib.nu * use lib_provisioning/ai/lib.nu *
@ -141,12 +151,13 @@ let response = (ai_process_query "Show me all servers with high CPU usage")
let template = (ai_generate_template "Docker Swarm cluster with monitoring" "cluster") let template = (ai_generate_template "Docker Swarm cluster with monitoring" "cluster")
# Validate configurations # Validate configurations
let validation = (validate_and_fix_kcl "servers.k") let validation = (validate_and_fix_nickel "servers.ncl")
``` ```
### 🌐 **Webhook Integration** ### 🌐 **Webhook Integration**
#### HTTP Webhook #### HTTP Webhook
```bash ```bash
curl -X POST http://your-server/webhook \ curl -X POST http://your-server/webhook \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
@ -158,8 +169,9 @@ curl -X POST http://your-server/webhook \
``` ```
#### Slack Integration #### Slack Integration
```nushell ```nushell
# Process Slack webhook payload #Process Slack webhook payload
let slack_payload = { let slack_payload = {
text: "generate upcloud defaults for development", text: "generate upcloud defaults for development",
user_id: "U123456", user_id: "U123456",
@ -170,8 +182,9 @@ let response = (process_slack_webhook $slack_payload)
``` ```
#### Discord Integration #### Discord Integration
```nushell ```nushell
# Process Discord webhook #Process Discord webhook
let discord_payload = { let discord_payload = {
content: "show infrastructure status", content: "show infrastructure status",
author: { id: "123456789" }, author: { id: "123456789" },
@ -186,6 +199,7 @@ let response = (process_discord_webhook $discord_payload)
### 🏗️ **Infrastructure Generation Examples** ### 🏗️ **Infrastructure Generation Examples**
#### 1. Kubernetes Cluster Setup #### 1. Kubernetes Cluster Setup
```bash ```bash
./provisioning generate-ai servers " ./provisioning generate-ai servers "
High-availability Kubernetes cluster with: High-availability Kubernetes cluster with:
@ -194,10 +208,11 @@ High-availability Kubernetes cluster with:
- Dedicated storage nodes with Ceph - Dedicated storage nodes with Ceph
- Private networking with load balancer - Private networking with load balancer
- Monitoring and logging stack - Monitoring and logging stack
" --provider upcloud --output k8s_cluster_servers.k --validate " --provider upcloud --output k8s_cluster_servers.ncl --validate
``` ```
#### 2. AWS Production Environment #### 2. AWS Production Environment
```bash ```bash
./provisioning generate-ai defaults " ./provisioning generate-ai defaults "
AWS production environment configuration: AWS production environment configuration:
@ -209,10 +224,11 @@ AWS production environment configuration:
- ElastiCache for caching - ElastiCache for caching
- CloudFront CDN - CloudFront CDN
- Route53 DNS management - Route53 DNS management
" --provider aws --output aws_prod_defaults.k " --provider aws --output aws_prod_defaults.ncl
``` ```
#### 3. Development Environment #### 3. Development Environment
```bash ```bash
./provisioning generate-ai infra " ./provisioning generate-ai infra "
Development environment for a microservices application: Development environment for a microservices application:
@ -244,7 +260,7 @@ Development environment for a microservices application:
**AI:** *"Perfect! I'll generate an UpCloud configuration with monitoring. Here's your infrastructure setup:* **AI:** *"Perfect! I'll generate an UpCloud configuration with monitoring. Here's your infrastructure setup:*
```kcl ```nickel
import upcloud_prov import upcloud_prov
servers = [ servers = [
// Load balancer // Load balancer
@ -259,11 +275,13 @@ servers = [
] ]
``` ```
*This configuration includes 7 servers optimized for high availability and performance. Would you like me to explain any specific part or generate additional configurations?"* *This configuration includes 7 servers optimized for high availability and performance.
Would you like me to explain any specific part or generate additional configurations?"*
### 🚀 **Advanced Features** ### 🚀 **Advanced Features**
#### Interactive Configuration Builder #### Interactive Configuration Builder
```bash ```bash
./provisioning ai generate --interactive ./provisioning ai generate --interactive
``` ```
@ -278,12 +296,13 @@ This launches an interactive session that asks specific questions to build optim
6. **Budget Constraints** - Cost optimization preferences 6. **Budget Constraints** - Cost optimization preferences
#### Configuration Optimization #### Configuration Optimization
```bash ```bash
# Analyze and improve existing configurations #Analyze and improve existing configurations
./provisioning ai improve existing_config.k --output optimized_config.k ./provisioning ai improve existing_config.ncl --output optimized_config.ncl
# Get AI suggestions for performance improvements # Get AI suggestions for performance improvements
./provisioning ai query --prompt "How can I optimize this configuration for better performance?" --context file:servers.k ./provisioning ai query --prompt "How can I optimize this configuration for better performance?" --context file:servers.ncl
``` ```
## Integration with Existing Workflows ## Integration with Existing Workflows
@ -291,14 +310,14 @@ This launches an interactive session that asks specific questions to build optim
### 🔄 **Workflow Integration** ### 🔄 **Workflow Integration**
1. **Generate** configurations with AI 1. **Generate** configurations with AI
2. **Validate** using KCL compiler 2. **Validate** using Nickel compiler
3. **Review** and customize as needed 3. **Review** and customize as needed
4. **Apply** using provisioning commands 4. **Apply** using provisioning commands
5. **Monitor** and iterate 5. **Monitor** and iterate
```bash ```bash
# Complete workflow example #Complete workflow example
./provisioning generate-ai servers "Production Kubernetes cluster" --validate --output servers.k ./provisioning generate-ai servers "Production Kubernetes cluster" --validate --output servers.ncl
./provisioning server create --check # Review before creation ./provisioning server create --check # Review before creation
./provisioning server create # Actually create infrastructure ./provisioning server create # Actually create infrastructure
``` ```
@ -314,7 +333,7 @@ This launches an interactive session that asks specific questions to build optim
### 🧪 **Testing & Development** ### 🧪 **Testing & Development**
```bash ```bash
# Test AI functionality #Test AI functionality
./provisioning ai test ./provisioning ai test
# Test webhook processing # Test webhook processing
@ -327,28 +346,32 @@ This launches an interactive session that asks specific questions to build optim
## Architecture ## Architecture
### 🏗️ **Module Structure** ### 🏗️ **Module Structure**
```
```text
ai/ ai/
├── lib.nu # Core AI functionality and API integration ├── lib.nu # Core AI functionality and API integration
├── templates.nu # KCL template generation functions ├── templates.nu # Nickel template generation functions
├── webhook.nu # Chat/webhook processing ├── webhook.nu # Chat/webhook processing
├── mod.nu # Module exports ├── mod.nu # Module exports
└── README.md # This documentation └── README.md # This documentation
``` ```
### 🔌 **Integration Points** ### 🔌 **Integration Points**
- **Settings System** - AI configuration management - **Settings System** - AI configuration management
- **Secrets Management** - Integration with SOPS/KMS for secure API keys - **Secrets Management** - Integration with SOPS/KMS for secure API keys
- **Template Engine** - Enhanced with AI-generated content - **Template Engine** - Enhanced with AI-generated content
- **Validation System** - Automated KCL syntax checking - **Validation System** - Automated Nickel syntax checking
- **CLI Commands** - Natural language command processing - **CLI Commands** - Natural language command processing
### 🌊 **Data Flow** ### 🌊 **Data Flow**
1. **Input** - Natural language description or chat message 1. **Input** - Natural language description or chat message
2. **Intent Detection** - Parse and understand user requirements 2. **Intent Detection** - Parse and understand user requirements
3. **Context Building** - Gather relevant infrastructure context 3. **Context Building** - Gather relevant infrastructure context
4. **AI Processing** - Generate appropriate KCL configurations 4. **AI Processing** - Generate appropriate Nickel configurations
5. **Validation** - Syntax and semantic validation 5. **Validation** - Syntax and semantic validation
6. **Output** - Formatted KCL files and user feedback 6. **Output** - Formatted Nickel files and user feedback
This AI integration transforms the provisioning system into an intelligent infrastructure automation platform that understands natural language and generates production-ready configurations. This AI integration transforms the provisioning system into an intelligent infrastructure automation platform
that understands natural language and generates production-ready configurations.

View file

@ -1,51 +0,0 @@
AI capabilities have been successfully implemented as an optional running mode with support for OpenAI, Claude, and generic LLM
providers! Here's what's been added:
✅ Configuration (KCL Schema)
- AIProvider schema in kcl/settings.k:54-79 with configurable provider selection
- Optional mode with feature flags for template, query, and webhook AI
✅ Core AI Library
- core/nulib/lib_provisioning/ai/lib.nu - Complete AI integration library
- Support for OpenAI, Claude, and generic providers
- Configurable endpoints, models, and parameters
✅ Template Generation
- Enhanced render_template function with --ai_prompt flag
- Natural language to infrastructure config generation
✅ Query Enhancement
- Added --ai_query flag to query command in query.nu:21
- Natural language infrastructure queries
✅ Webhook Integration
- webhook/ai_webhook.nu with platform-specific handlers (Slack, Discord, Teams)
- Enhanced existing webhook system with AI processing
✅ CLI Integration
- New ai command module in main_provisioning/ai.nu
- Integrated into main provisioning CLI
Usage Examples:
# Generate infrastructure templates
./core/nulib/provisioning ai template --prompt "3-node Kubernetes cluster with Ceph storage"
# Natural language queries
./core/nulib/provisioning query --ai_query "show all AWS servers with high CPU usage"
# Test AI configuration
./core/nulib/provisioning ai test
# Webhook processing
./core/nulib/provisioning ai webhook --prompt "deploy redis cluster"
All AI capabilities are optional and configurable through the KCL settings with provider choice between OpenAI, Claude, and
generic LLM endpoints.

View file

@ -1,44 +0,0 @@
✅ AI Integration Complete
All 4 requested features implemented as optional running mode:
1. Template System Enhancement ✅
- Enhanced render_template function with AI capabilities
- New render_template_ai function for direct AI template generation
- Natural language to infrastructure config generation
2. Natural Language Query System ✅
- Added --ai_query flag to the query command
- AI processes natural language queries about infrastructure
- Context-aware responses based on infrastructure state
3. Plugin Development ✅
- Complete AI library: core/nulib/lib_provisioning/ai/lib.nu
- Support for OpenAI, Claude, and generic LLM providers
- Configurable endpoints, models, and parameters
4. Webhook Integration ✅
- Chat platform support: Slack, Discord, Teams, generic
- Enhanced existing webhook system with AI processing
- Platform-specific response formatting
Configuration
- KCL Schema: kcl/settings.k:54-79 - AIProvider configuration
- Optional Mode: Enable/disable via ai.enabled = true
- Provider Choice: "openai" | "claude" | "generic"
- Feature Flags: Individual toggles for template, query, webhook AI
Usage Ready
Set API key environment variable and enable in KCL settings:
export OPENAI_API_KEY="your-key" # for OpenAI
export ANTHROPIC_API_KEY="your-key" # for Claude
export LLM_API_KEY="your-key" # for generic
All AI functionality is now available as an optional running mode with full provider choice support!

View file

@ -1,130 +0,0 @@
Perfect! I have successfully implemented a comprehensive AI-powered infrastructure automation system for
your provisioning tool. Here's what has been accomplished:
✨ AI Infrastructure Automation - Complete Implementation
🎯 Key Features Implemented:
1. 🤖 AI-Powered KCL Generation
- Natural language to KCL configuration conversion
- Support for servers, defaults, settings, clusters, and task services
- Provider-specific optimizations (UpCloud, AWS, Local)
2. 🧠 Multiple AI Provider Support
- OpenAI (GPT-4, GPT-3.5)
- Anthropic Claude (Claude-3.5 Sonnet)
- Generic/Local LLM APIs (Ollama, etc.)
3. 💬 Interactive Interfaces
- Command-line AI chat
- Interactive configuration builder
- Webhook integration for Slack/Discord
- Natural language query processing
4. 🔧 Enhanced Generation Commands
- ./provisioning ai - Main AI command interface
- ./provisioning generate-ai - AI-enhanced generation
- Interactive mode with follow-up questions
- Automatic validation and improvement
🛠 Files Created/Enhanced:
Core AI Library
- core/nulib/lib_provisioning/ai/lib.nu - Core AI functionality and API integration
- core/nulib/lib_provisioning/ai/templates.nu - KCL template generation
- core/nulib/lib_provisioning/ai/webhook.nu - Chat/webhook processing
- core/nulib/lib_provisioning/ai/mod.nu - Module exports
Command Interface
- core/nulib/main_provisioning/ai.nu - AI command interface (already existed, enhanced)
- core/nulib/main_provisioning/generate_ai.nu - Enhanced generation commands
Configuration Files
- kcl/settings.k - Added AIProvider schema (already existed)
- templates/ai.yaml - AI configuration template
- templates/default_context.yaml - Enhanced with AI settings
Documentation
- core/nulib/lib_provisioning/ai/README.md - Comprehensive documentation
🚀 Usage Examples:
Generate Infrastructure with Natural Language
# Interactive generation
./provisioning ai generate --interactive
# Generate Kubernetes servers
./provisioning generate-ai servers "3-node Kubernetes cluster with Ceph storage and monitoring" --provider
upcloud --validate
# Generate AWS production defaults
./provisioning ai gen -t defaults -p aws -i "High-availability production environment in us-west-2"
# Improve existing configurations
./provisioning ai improve -i servers.k -o optimized_servers.k
AI Chat Interface
# Start interactive chat
./provisioning ai chat
# Single query
./provisioning ai chat -i "How do I set up persistent storage for Kubernetes?"
# Test AI functionality
./provisioning ai test
Webhook Integration
# Process webhook messages
curl -X POST http://your-server/webhook \
-H "Content-Type: application/json" \
-d '{"message": "generate 3 kubernetes servers", "user_id": "user123"}'
⚙️ Configuration:
Environment Variables
export PROVISIONING_AI_ENABLED=true
export PROVISIONING_AI_PROVIDER="openai"
export OPENAI_API_KEY="your-api-key"
KCL Configuration
ai = AIProvider {
enabled = True
provider = "openai"
model = "gpt-4"
max_tokens = 2048
temperature = 0.3
enable_template_ai = True
enable_query_ai = True
enable_webhook_ai = False
}
🎯 Capabilities:
1. Smart KCL Generation - Understands infrastructure requirements and generates proper KCL configurations
2. Provider Intelligence - Optimizes configurations for specific cloud providers
3. Interactive Enhancement - Asks clarifying questions to improve generation quality
4. Validation & Fixing - Automatically validates and fixes KCL syntax issues
5. Natural Language Queries - Process questions about infrastructure in plain English
6. Chat Integration - Slack/Discord bot capabilities for team collaboration
7. Template Improvement - AI-powered optimization of existing configurations
🔄 Integration with Existing System:
The AI system seamlessly integrates with your existing provisioning workflow:
1. Generate configurations with AI
2. Validate using existing KCL tools
3. Apply using standard provisioning commands
4. Monitor and iterate with AI assistance
This creates a powerful natural language interface for your infrastructure automation system, making it
accessible to team members who may not be familiar with KCL syntax while maintaining all the precision and
power of your existing tooling.
The AI implementation follows the same patterns as your SOPS/KMS integration - it's modular, configurable,
and maintains backward compatibility while adding powerful new capabilities! 🚀

View file

@ -44,7 +44,7 @@ export def get_ai_config [] {
$settings.data.ai $settings.data.ai
} }
# Check if AI is enabled and configured # Check if AI is enabled and configured
export def is_ai_enabled [] { export def is_ai_enabled [] {
let config = (get_ai_config) let config = (get_ai_config)
$config.enabled and ($env.OPENAI_API_KEY? != null or $env.ANTHROPIC_API_KEY? != null or $env.LLM_API_KEY? != null) $config.enabled and ($env.OPENAI_API_KEY? != null or $env.ANTHROPIC_API_KEY? != null or $env.LLM_API_KEY? != null)
@ -58,16 +58,16 @@ export def get_provider_config [provider: string] {
# Build API request headers # Build API request headers
export def build_headers [config: record] { export def build_headers [config: record] {
let provider_config = (get_provider_config $config.provider) let provider_config = (get_provider_config $config.provider)
# Get API key from environment variables based on provider # Get API key from environment variables based on provider
let api_key = match $config.provider { let api_key = match $config.provider {
"openai" => $env.OPENAI_API_KEY? "openai" => $env.OPENAI_API_KEY?
"claude" => $env.ANTHROPIC_API_KEY? "claude" => $env.ANTHROPIC_API_KEY?
_ => $env.LLM_API_KEY? _ => $env.LLM_API_KEY?
} }
let auth_value = $provider_config.auth_prefix + ($api_key | default "") let auth_value = $provider_config.auth_prefix + ($api_key | default "")
{ {
"Content-Type": "application/json" "Content-Type": "application/json"
($provider_config.auth_header): $auth_value ($provider_config.auth_header): $auth_value
@ -89,7 +89,7 @@ export def ai_request [
] { ] {
let headers = (build_headers $config) let headers = (build_headers $config)
let url = (build_endpoint $config $path) let url = (build_endpoint $config $path)
http post $url --headers $headers --max-time ($config.timeout * 1000) $payload http post $url --headers $headers --max-time ($config.timeout * 1000) $payload
} }
@ -101,11 +101,11 @@ export def ai_complete [
--temperature: float --temperature: float
] { ] {
let config = (get_ai_config) let config = (get_ai_config)
if not (is_ai_enabled) { if not (is_ai_enabled) {
return "AI is not enabled or configured. Please set OPENAI_API_KEY, ANTHROPIC_API_KEY, or LLM_API_KEY environment variable and enable AI in settings." return "AI is not enabled or configured. Please set OPENAI_API_KEY, ANTHROPIC_API_KEY, or LLM_API_KEY environment variable and enable AI in settings."
} }
let messages = if ($system_prompt | is-empty) { let messages = if ($system_prompt | is-empty) {
[{role: "user", content: $prompt}] [{role: "user", content: $prompt}]
} else { } else {
@ -114,21 +114,21 @@ export def ai_complete [
{role: "user", content: $prompt} {role: "user", content: $prompt}
] ]
} }
let payload = { let payload = {
model: ($config.model? | default (get_provider_config $config.provider).default_model) model: ($config.model? | default (get_provider_config $config.provider).default_model)
messages: $messages messages: $messages
max_tokens: ($max_tokens | default $config.max_tokens) max_tokens: ($max_tokens | default $config.max_tokens)
temperature: ($temperature | default $config.temperature) temperature: ($temperature | default $config.temperature)
} }
let endpoint = match $config.provider { let endpoint = match $config.provider {
"claude" => "/messages" "claude" => "/messages"
_ => "/chat/completions" _ => "/chat/completions"
} }
let response = (ai_request $config $endpoint $payload) let response = (ai_request $config $endpoint $payload)
# Extract content based on provider # Extract content based on provider
match $config.provider { match $config.provider {
"claude" => { "claude" => {
@ -153,25 +153,25 @@ export def ai_generate_template [
description: string description: string
template_type: string = "server" template_type: string = "server"
] { ] {
let system_prompt = $"You are an infrastructure automation expert. Generate KCL configuration files for cloud infrastructure based on natural language descriptions. let system_prompt = $"You are an infrastructure automation expert. Generate Nickel configuration files for cloud infrastructure based on natural language descriptions.
Template Type: ($template_type) Template Type: ($template_type)
Available Providers: AWS, UpCloud, Local Available Providers: AWS, UpCloud, Local
Available Services: Kubernetes, containerd, Cilium, Ceph, PostgreSQL, Gitea, HAProxy Available Services: Kubernetes, containerd, Cilium, Ceph, PostgreSQL, Gitea, HAProxy
Generate valid KCL code that follows these patterns: Generate valid Nickel code that follows these patterns:
- Use proper KCL schema definitions - Use proper Nickel schema definitions
- Include provider-specific configurations - Include provider-specific configurations
- Add appropriate comments - Add appropriate comments
- Follow existing naming conventions - Follow existing naming conventions
- Include security best practices - Include security best practices
Return only the KCL configuration code, no explanations." Return only the Nickel configuration code, no explanations."
if not (get_ai_config).enable_template_ai { if not (get_ai_config).enable_template_ai {
return "AI template generation is disabled" return "AI template generation is disabled"
} }
ai_complete $description --system_prompt $system_prompt ai_complete $description --system_prompt $system_prompt
} }
@ -195,13 +195,13 @@ Be concise and practical. Focus on infrastructure operations and management."
if not (get_ai_config).enable_query_ai { if not (get_ai_config).enable_query_ai {
return "AI query processing is disabled" return "AI query processing is disabled"
} }
let enhanced_query = if ($context | is-empty) { let enhanced_query = if ($context | is-empty) {
$query $query
} else { } else {
$"Context: ($context | to json)\n\nQuery: ($query)" $"Context: ($context | to json)\n\nQuery: ($query)"
} }
ai_complete $enhanced_query --system_prompt $system_prompt ai_complete $enhanced_query --system_prompt $system_prompt
} }
@ -215,7 +215,7 @@ export def ai_process_webhook [
Help users with: Help users with:
- Infrastructure provisioning and management - Infrastructure provisioning and management
- Server operations and troubleshooting - Server operations and troubleshooting
- Kubernetes cluster management - Kubernetes cluster management
- Service deployment and configuration - Service deployment and configuration
@ -228,34 +228,34 @@ Channel: ($channel)"
if not (get_ai_config).enable_webhook_ai { if not (get_ai_config).enable_webhook_ai {
return "AI webhook processing is disabled" return "AI webhook processing is disabled"
} }
ai_complete $message --system_prompt $system_prompt ai_complete $message --system_prompt $system_prompt
} }
# Validate AI configuration # Validate AI configuration
export def validate_ai_config [] { export def validate_ai_config [] {
let config = (get_ai_config) let config = (get_ai_config)
mut issues = [] mut issues = []
if $config.enabled { if $config.enabled {
if ($config.api_key? == null) { if ($config.api_key? == null) {
$issues = ($issues | append "API key not configured") $issues = ($issues | append "API key not configured")
} }
if $config.provider not-in ($AI_PROVIDERS | columns) { if $config.provider not-in ($AI_PROVIDERS | columns) {
$issues = ($issues | append $"Unsupported provider: ($config.provider)") $issues = ($issues | append $"Unsupported provider: ($config.provider)")
} }
if $config.max_tokens < 1 { if $config.max_tokens < 1 {
$issues = ($issues | append "max_tokens must be positive") $issues = ($issues | append "max_tokens must be positive")
} }
if $config.temperature < 0.0 or $config.temperature > 1.0 { if $config.temperature < 0.0 or $config.temperature > 1.0 {
$issues = ($issues | append "temperature must be between 0.0 and 1.0") $issues = ($issues | append "temperature must be between 0.0 and 1.0")
} }
} }
{ {
valid: ($issues | is-empty) valid: ($issues | is-empty)
issues: $issues issues: $issues
@ -270,11 +270,11 @@ export def test_ai_connection [] {
message: "AI is not enabled or configured" message: "AI is not enabled or configured"
} }
} }
let response = (ai_complete "Test connection - respond with 'OK'" --max_tokens 10) let response = (ai_complete "Test connection - respond with 'OK'" --max_tokens 10)
{ {
success: true success: true
message: "AI connection test completed" message: "AI connection test completed"
response: $response response: $response
} }
} }

View file

@ -1 +1,6 @@
export use lib.nu * # ai/ subsystem facade — selective re-exports (ADR-025 Phase 3 Layer 3).
export use lib.nu [
get_ai_config is_ai_enabled get_provider_config build_headers build_endpoint
ai_request ai_complete ai_generate_template ai_process_query
ai_process_webhook validate_ai_config test_ai_connection
]

View file

@ -3,10 +3,12 @@
# Token-optimized agent for progressive version caching with infra-aware hierarchy # Token-optimized agent for progressive version caching with infra-aware hierarchy
# Usage: nu agent.nu <command> [args] # Usage: nu agent.nu <command> [args]
use cache_manager.nu * # Selective imports (ADR-025 Phase 3 Layer 2).
use version_loader.nu * # version_loader and grace_checker star-imports were dead — dropped.
use grace_checker.nu * use lib_provisioning/cache/cache_manager.nu [
use batch_updater.nu * clear-cache-system get-cached-version init-cache-system show-cache-status
]
use lib_provisioning/cache/batch_updater.nu [batch-update-cache sync-cache-from-sources]
# Main agent entry point # Main agent entry point
def main [ def main [
@ -60,4 +62,4 @@ def main [
exit 1 exit 1
} }
} }
} }

View file

@ -42,7 +42,7 @@ def process-batch [components: list<string>] {
# Sync cache from sources (rebuild cache) # Sync cache from sources (rebuild cache)
export def sync-cache-from-sources [] { export def sync-cache-from-sources [] {
print "🔄 Syncing cache from KCL sources..." print "🔄 Syncing cache from Nickel sources..."
# Clear existing cache # Clear existing cache
clear-cache-system clear-cache-system
@ -164,4 +164,4 @@ export def optimize-cache [] {
# Import required functions # Import required functions
use cache_manager.nu [cache-version, clear-cache-system, init-cache-system, get-infra-cache-path, get-provisioning-cache-path] use cache_manager.nu [cache-version, clear-cache-system, init-cache-system, get-infra-cache-path, get-provisioning-cache-path]
use version_loader.nu [batch-load-versions, get-all-components] use version_loader.nu [batch-load-versions, get-all-components]
use grace_checker.nu [get-expired-entries, get-components-needing-update, invalidate-cache-entry] use grace_checker.nu [get-expired-entries, get-components-needing-update, invalidate-cache-entry]

View file

@ -7,7 +7,7 @@ use grace_checker.nu is-cache-valid?
# Get version with progressive cache hierarchy # Get version with progressive cache hierarchy
export def get-cached-version [ export def get-cached-version [
component: string # Component name (e.g., kubernetes, containerd) component: string # Component name (e.g., kubernetes, containerd)
]: nothing -> string { ] {
# Cache hierarchy: infra -> provisioning -> source # Cache hierarchy: infra -> provisioning -> source
# 1. Try infra cache first (project-specific) # 1. Try infra cache first (project-specific)
@ -42,7 +42,7 @@ export def get-cached-version [
} }
# Get version from infra cache # Get version from infra cache
def get-infra-cache [component: string]: nothing -> string { def get-infra-cache [component: string] {
let cache_path = (get-infra-cache-path) let cache_path = (get-infra-cache-path)
let cache_file = ($cache_path | path join "versions.json") let cache_file = ($cache_path | path join "versions.json")
@ -56,12 +56,14 @@ def get-infra-cache [component: string]: nothing -> string {
} }
let cache_data = ($result.stdout | from json) let cache_data = ($result.stdout | from json)
let version_data = ($cache_data | try { get $component } catch { {}) } let version_result = (do { $cache_data | get $component } | complete)
($version_data | try { get current } catch { "") } let version_data = if $version_result.exit_code == 0 { $version_result.stdout } else { {} }
let current_result = (do { $version_data | get current } | complete)
if $current_result.exit_code == 0 { $current_result.stdout } else { "" }
} }
# Get version from provisioning cache # Get version from provisioning cache
def get-provisioning-cache [component: string]: nothing -> string { def get-provisioning-cache [component: string] {
let cache_path = (get-provisioning-cache-path) let cache_path = (get-provisioning-cache-path)
let cache_file = ($cache_path | path join "versions.json") let cache_file = ($cache_path | path join "versions.json")
@ -75,8 +77,10 @@ def get-provisioning-cache [component: string]: nothing -> string {
} }
let cache_data = ($result.stdout | from json) let cache_data = ($result.stdout | from json)
let version_data = ($cache_data | try { get $component } catch { {}) } let version_result = (do { $cache_data | get $component } | complete)
($version_data | try { get current } catch { "") } let version_data = if $version_result.exit_code == 0 { $version_result.stdout } else { {} }
let current_result = (do { $version_data | get current } | complete)
if $current_result.exit_code == 0 { $current_result.stdout } else { "" }
} }
# Cache version data # Cache version data
@ -117,7 +121,7 @@ export def cache-version [
} }
# Get cache paths from config # Get cache paths from config
export def get-infra-cache-path []: nothing -> string { export def get-infra-cache-path [] {
use ../config/accessor.nu config-get use ../config/accessor.nu config-get
let infra_path = (config-get "paths.infra" "") let infra_path = (config-get "paths.infra" "")
let current_infra = (config-get "infra.current" "default") let current_infra = (config-get "infra.current" "default")
@ -129,12 +133,12 @@ export def get-infra-cache-path []: nothing -> string {
$infra_path | path join $current_infra "cache" $infra_path | path join $current_infra "cache"
} }
export def get-provisioning-cache-path []: nothing -> string { export def get-provisioning-cache-path [] {
use ../config/accessor.nu config-get use ../config/accessor.nu config-get
config-get "cache.path" ".cache/versions" config-get "cache.path" ".cache/versions"
} }
def get-default-grace-period []: nothing -> int { def get-default-grace-period [] {
use ../config/accessor.nu config-get use ../config/accessor.nu config-get
config-get "cache.grace_period" 86400 config-get "cache.grace_period" 86400
} }
@ -200,4 +204,4 @@ export def show-cache-status [] {
} else { } else {
print "⚙️ Provisioning cache: not found" print "⚙️ Provisioning cache: not found"
} }
} }

View file

@ -5,7 +5,7 @@
export def is-cache-valid? [ export def is-cache-valid? [
component: string # Component name component: string # Component name
cache_type: string # "infra" or "provisioning" cache_type: string # "infra" or "provisioning"
]: nothing -> bool { ] {
let cache_path = if $cache_type == "infra" { let cache_path = if $cache_type == "infra" {
get-infra-cache-path get-infra-cache-path
} else { } else {
@ -24,14 +24,17 @@ export def is-cache-valid? [
} }
let cache_data = ($result.stdout | from json) let cache_data = ($result.stdout | from json)
let version_data = ($cache_data | try { get $component } catch { {}) } let vd_result = (do { $cache_data | get $component } | complete)
let version_data = if $vd_result.exit_code == 0 { $vd_result.stdout } else { {} }
if ($version_data | is-empty) { if ($version_data | is-empty) {
return false return false
} }
let cached_at = ($version_data | try { get cached_at } catch { "") } let ca_result = (do { $version_data | get cached_at } | complete)
let grace_period = ($version_data | try { get grace_period } catch { (get-default-grace-period)) } let cached_at = if $ca_result.exit_code == 0 { $ca_result.stdout } else { "" }
let gp_result = (do { $version_data | get grace_period } | complete)
let grace_period = if $gp_result.exit_code == 0 { $gp_result.stdout } else { (get-default-grace-period) }
if ($cached_at | is-empty) { if ($cached_at | is-empty) {
return false return false
@ -54,7 +57,7 @@ export def is-cache-valid? [
# Get expired cache entries # Get expired cache entries
export def get-expired-entries [ export def get-expired-entries [
cache_type: string # "infra" or "provisioning" cache_type: string # "infra" or "provisioning"
]: nothing -> list<string> { ] {
let cache_path = if $cache_type == "infra" { let cache_path = if $cache_type == "infra" {
get-infra-cache-path get-infra-cache-path
} else { } else {
@ -80,7 +83,7 @@ export def get-expired-entries [
} }
# Get components that need update check (check_latest = true and expired) # Get components that need update check (check_latest = true and expired)
export def get-components-needing-update []: nothing -> list<string> { export def get-components-needing-update [] {
let components = [] let components = []
# Check infra cache # Check infra cache
@ -98,7 +101,7 @@ export def get-components-needing-update []: nothing -> list<string> {
} }
# Get components with check_latest = true # Get components with check_latest = true
def get-check-latest-components [cache_type: string]: nothing -> list<string> { def get-check-latest-components [cache_type: string] {
let cache_path = if $cache_type == "infra" { let cache_path = if $cache_type == "infra" {
get-infra-cache-path get-infra-cache-path
} else { } else {
@ -120,7 +123,8 @@ def get-check-latest-components [cache_type: string]: nothing -> list<string> {
$cache_data | columns | where { |component| $cache_data | columns | where { |component|
let comp_data = ($cache_data | get $component) let comp_data = ($cache_data | get $component)
($comp_data | try { get check_latest } catch { false) } let cl_result = (do { $comp_data | get check_latest } | complete)
if $cl_result.exit_code == 0 { $cl_result.stdout } else { false }
} }
} }
@ -150,7 +154,7 @@ export def invalidate-cache-entry [
} }
# Helper functions (same as in cache_manager.nu) # Helper functions (same as in cache_manager.nu)
def get-infra-cache-path []: nothing -> string { def get-infra-cache-path [] {
use ../config/accessor.nu config-get use ../config/accessor.nu config-get
let infra_path = (config-get "paths.infra" "") let infra_path = (config-get "paths.infra" "")
let current_infra = (config-get "infra.current" "default") let current_infra = (config-get "infra.current" "default")
@ -162,12 +166,12 @@ def get-infra-cache-path []: nothing -> string {
$infra_path | path join $current_infra "cache" $infra_path | path join $current_infra "cache"
} }
def get-provisioning-cache-path []: nothing -> string { def get-provisioning-cache-path [] {
use ../config/accessor.nu config-get use ../config/accessor.nu config-get
config-get "cache.path" ".cache/versions" config-get "cache.path" ".cache/versions"
} }
def get-default-grace-period []: nothing -> int { def get-default-grace-period [] {
use ../config/accessor.nu config-get use ../config/accessor.nu config-get
config-get "cache.grace_period" 86400 config-get "cache.grace_period" 86400
} }

View file

@ -1,10 +1,10 @@
# Version Loader - Load versions from KCL sources # Version Loader - Load versions from Nickel sources
# Token-optimized loader for version data from various sources # Token-optimized loader for version data from various sources
# Load version from source (KCL files) # Load version from source (Nickel files)
export def load-version-from-source [ export def load-version-from-source [
component: string # Component name component: string # Component name
]: nothing -> string { ] {
# Try different source locations # Try different source locations
let taskserv_version = (load-taskserv-version $component) let taskserv_version = (load-taskserv-version $component)
if ($taskserv_version | is-not-empty) { if ($taskserv_version | is-not-empty) {
@ -24,18 +24,18 @@ export def load-version-from-source [
"" ""
} }
# Load taskserv version from version.k files # Load taskserv version from version.ncl files
def load-taskserv-version [component: string]: nothing -> string { def load-taskserv-version [component: string] {
# Find version.k file for component # Find version.ncl file for component
let version_files = [ let version_files = [
$"taskservs/($component)/kcl/version.k" $"taskservs/($component)/nickel/version.ncl"
$"taskservs/($component)/default/kcl/version.k" $"taskservs/($component)/default/nickel/version.ncl"
$"taskservs/($component)/kcl/($component).k" $"taskservs/($component)/nickel/($component).ncl"
] ]
for file in $version_files { for file in $version_files {
if ($file | path exists) { if ($file | path exists) {
let version = (extract-version-from-kcl $file $component) let version = (extract-version-from-nickel $file $component)
if ($version | is-not-empty) { if ($version | is-not-empty) {
return $version return $version
} }
@ -46,11 +46,11 @@ def load-taskserv-version [component: string]: nothing -> string {
} }
# Load core tool version # Load core tool version
def load-core-version [component: string]: nothing -> string { def load-core-version [component: string] {
let core_file = "core/versions.k" let core_file = "core/versions.ncl"
if ($core_file | path exists) { if ($core_file | path exists) {
let version = (extract-core-version-from-kcl $core_file $component) let version = (extract-core-version-from-nickel $core_file $component)
if ($version | is-not-empty) { if ($version | is-not-empty) {
return $version return $version
} }
@ -60,19 +60,19 @@ def load-core-version [component: string]: nothing -> string {
} }
# Load provider tool version # Load provider tool version
def load-provider-version [component: string]: nothing -> string { def load-provider-version [component: string] {
# Check provider directories # Check provider directories
let providers = ["aws", "upcloud", "local"] let providers = ["aws", "upcloud", "local"]
for provider in $providers { for provider in $providers {
let provider_files = [ let provider_files = [
$"providers/($provider)/kcl/versions.k" $"providers/($provider)/nickel/versions.ncl"
$"providers/($provider)/versions.k" $"providers/($provider)/versions.ncl"
] ]
for file in $provider_files { for file in $provider_files {
if ($file | path exists) { if ($file | path exists) {
let version = (extract-version-from-kcl $file $component) let version = (extract-version-from-nickel $file $component)
if ($version | is-not-empty) { if ($version | is-not-empty) {
return $version return $version
} }
@ -83,19 +83,19 @@ def load-provider-version [component: string]: nothing -> string {
"" ""
} }
# Extract version from KCL file (taskserv format) # Extract version from Nickel file (taskserv format)
def extract-version-from-kcl [file: string, component: string]: nothing -> string { def extract-version-from-nickel [file: string, component: string] {
let kcl_result = (^kcl $file | complete) let decl_result = (^nickel $file | complete)
if $kcl_result.exit_code != 0 { if $decl_result.exit_code != 0 {
return "" return ""
} }
if ($kcl_result.stdout | is-empty) { if ($decl_result.stdout | is-empty) {
return "" return ""
} }
let parse_result = (do { $kcl_result.stdout | from yaml } | complete) let parse_result = (do { $decl_result.stdout | from yaml } | complete)
if $parse_result.exit_code != 0 { if $parse_result.exit_code != 0 {
return "" return ""
} }
@ -110,17 +110,20 @@ def extract-version-from-kcl [file: string, component: string]: nothing -> strin
] ]
for key in $version_keys { for key in $version_keys {
let version_data = ($result | try { get $key } catch { {}) } let lookup_result = (do { $result | get $key } | complete)
let version_data = if $lookup_result.exit_code == 0 { $lookup_result.stdout } else { {} }
if ($version_data | is-not-empty) { if ($version_data | is-not-empty) {
# Try TaskservVersion format first # Try TaskservVersion format first
let current_version = ($version_data | try { get version.current } catch { "") } let cv_result = (do { $version_data | get version.current } | complete)
let current_version = if $cv_result.exit_code == 0 { $cv_result.stdout } else { "" }
if ($current_version | is-not-empty) { if ($current_version | is-not-empty) {
return $current_version return $current_version
} }
# Try simple format # Try simple format
let simple_version = ($version_data | try { get current } catch { "") } let sv_result = (do { $version_data | get current } | complete)
let simple_version = if $sv_result.exit_code == 0 { $sv_result.stdout } else { "" }
if ($simple_version | is-not-empty) { if ($simple_version | is-not-empty) {
return $simple_version return $simple_version
} }
@ -135,19 +138,19 @@ def extract-version-from-kcl [file: string, component: string]: nothing -> strin
"" ""
} }
# Extract version from core versions.k file # Extract version from core versions.ncl file
def extract-core-version-from-kcl [file: string, component: string]: nothing -> string { def extract-core-version-from-nickel [file: string, component: string] {
let kcl_result = (^kcl $file | complete) let decl_result = (^nickel $file | complete)
if $kcl_result.exit_code != 0 { if $decl_result.exit_code != 0 {
return "" return ""
} }
if ($kcl_result.stdout | is-empty) { if ($decl_result.stdout | is-empty) {
return "" return ""
} }
let parse_result = (do { $kcl_result.stdout | from yaml } | complete) let parse_result = (do { $decl_result.stdout | from yaml } | complete)
if $parse_result.exit_code != 0 { if $parse_result.exit_code != 0 {
return "" return ""
} }
@ -155,27 +158,31 @@ def extract-core-version-from-kcl [file: string, component: string]: nothing ->
let result = $parse_result.stdout let result = $parse_result.stdout
# Look for component in core_versions array or individual variables # Look for component in core_versions array or individual variables
let core_versions = ($result | try { get core_versions } catch { []) } let cv_result = (do { $result | get core_versions } | complete)
let core_versions = if $cv_result.exit_code == 0 { $cv_result.stdout } else { [] }
if ($core_versions | is-not-empty) { if ($core_versions | is-not-empty) {
# Array format # Array format
let component_data = ($core_versions | where name == $component | first | default {}) let component_data = ($core_versions | where name == $component | first | default {})
let version = ($component_data | try { get version.current } catch { "") } let vc_result = (do { $component_data | get version.current } | complete)
let version = if $vc_result.exit_code == 0 { $vc_result.stdout } else { "" }
if ($version | is-not-empty) { if ($version | is-not-empty) {
return $version return $version
} }
} }
# Individual variable format (e.g., nu_version, kcl_version) # Individual variable format (e.g., nu_version, nickel_version)
let var_patterns = [ let var_patterns = [
$"($component)_version" $"($component)_version"
$"($component | str replace '-' '_')_version" $"($component | str replace '-' '_')_version"
] ]
for pattern in $var_patterns { for pattern in $var_patterns {
let version_data = ($result | try { get $pattern } catch { {}) } let vd_result = (do { $result | get $pattern } | complete)
let version_data = if $vd_result.exit_code == 0 { $vd_result.stdout } else { {} }
if ($version_data | is-not-empty) { if ($version_data | is-not-empty) {
let current = ($version_data | try { get current } catch { "") } let curr_result = (do { $version_data | get current } | complete)
let current = if $curr_result.exit_code == 0 { $curr_result.stdout } else { "" }
if ($current | is-not-empty) { if ($current | is-not-empty) {
return $current return $current
} }
@ -188,7 +195,7 @@ def extract-core-version-from-kcl [file: string, component: string]: nothing ->
# Batch load multiple versions (for efficiency) # Batch load multiple versions (for efficiency)
export def batch-load-versions [ export def batch-load-versions [
components: list<string> # List of component names components: list<string> # List of component names
]: nothing -> record { ] {
mut results = {} mut results = {}
for component in $components { for component in $components {
@ -202,7 +209,7 @@ export def batch-load-versions [
} }
# Get all available components # Get all available components
export def get-all-components []: nothing -> list<string> { export def get-all-components [] {
let taskservs = (get-taskserv-components) let taskservs = (get-taskserv-components)
let core_tools = (get-core-components) let core_tools = (get-core-components)
let providers = (get-provider-components) let providers = (get-provider-components)
@ -211,8 +218,8 @@ export def get-all-components []: nothing -> list<string> {
} }
# Get taskserv components # Get taskserv components
def get-taskserv-components []: nothing -> list<string> { def get-taskserv-components [] {
let result = (do { glob "taskservs/*/kcl/version.k" } | complete) let result = (do { glob "taskservs/*/nickel/version.ncl" } | complete)
if $result.exit_code != 0 { if $result.exit_code != 0 {
return [] return []
} }
@ -223,17 +230,17 @@ def get-taskserv-components []: nothing -> list<string> {
} }
# Get core components # Get core components
def get-core-components []: nothing -> list<string> { def get-core-components [] {
if not ("core/versions.k" | path exists) { if not ("core/versions.ncl" | path exists) {
return [] return []
} }
let kcl_result = (^kcl "core/versions.k" | complete) let decl_result = (^nickel "core/versions.ncl" | complete)
if $kcl_result.exit_code != 0 or ($kcl_result.stdout | is-empty) { if $decl_result.exit_code != 0 or ($decl_result.stdout | is-empty) {
return [] return []
} }
let parse_result = (do { $kcl_result.stdout | from yaml } | complete) let parse_result = (do { $decl_result.stdout | from yaml } | complete)
if $parse_result.exit_code != 0 { if $parse_result.exit_code != 0 {
return [] return []
} }
@ -245,7 +252,7 @@ def get-core-components []: nothing -> list<string> {
} }
# Get provider components (placeholder) # Get provider components (placeholder)
def get-provider-components []: nothing -> list<string> { def get-provider-components [] {
# TODO: Implement provider component discovery # TODO: Implement provider component discovery
[] []
} }

View file

@ -1,11 +1,9 @@
export-env { # export-env block removed by ADR-025 Phase 3 blocker 4.
use ../config/accessor.nu * # The former block called check_env (a pre-flight gate) and set $env.PROVISIONING_DEBUG.
use ../lib_provisioning/cmd/lib.nu check_env # Nobody imports cmd/env.nu directly; it was only reached via the star-import chain
check_env # from lib_provisioning/mod.nu. With that chain being emptied, this block would
$env.PROVISIONING_DEBUG = if (is-debug-enabled) { # never fire at CLI start anyway. Thin handlers that need the debug flag already
true # set it explicitly via `if $debug { $env.PROVISIONING_DEBUG = true }` — and
} else { # remaining reads like `if not $env.PROVISIONING_DEBUG { ... }` are gated upstream
false # by the same flag.
}
}

View file

@ -214,7 +214,7 @@ export def "env create" [
_ => "config.user.toml.example" _ => "config.user.toml.example"
} }
let base_path = (get-base-path) let base_path = (get-config-base-path)
let source_template = ($base_path | path join $template_path) let source_template = ($base_path | path join $template_path)
if not ($source_template | path exists) { if not ($source_template | path exists) {
@ -392,4 +392,4 @@ export def "env status" [
print "No environment-specific configuration" print "No environment-specific configuration"
} }
} }
} }

View file

@ -1,18 +1,19 @@
# Made for prepare and postrun # Made for prepare and postrun
use ../config/accessor.nu * # Selective imports (ADR-025 Phase 3 Layer 2).
use ../utils/ui.nu * # config/accessor and utils/ui star-imports were dead — dropped.
use ../sops * use lib_provisioning/utils/init.nu [get-workspace-path get-provisioning-infra-path]
use lib_provisioning/sops/lib.nu [find-sops-key on_sops]
export def log_debug [ export def log_debug [
msg: string msg: string
]: nothing -> nothing { ] {
use std use std
std log debug $msg std log debug $msg
# std assert (1 == 1) # std assert (1 == 1)
} }
export def check_env [ export def check_env [
]: nothing -> nothing { ] {
let vars_path = (get-provisioning-vars) let vars_path = (get-provisioning-vars)
if ($vars_path | is-empty) { if ($vars_path | is-empty) {
_print $"🛑 Error no values found for (_ansi red_bold)PROVISIONING_VARS(_ansi reset)" _print $"🛑 Error no values found for (_ansi red_bold)PROVISIONING_VARS(_ansi reset)"
@ -47,11 +48,11 @@ export def sops_cmd [
source: string source: string
target?: string target?: string
--error_exit # error on exit --error_exit # error on exit
]: nothing -> nothing { ] {
let sops_key = (find-sops-key) let sops_key = (find-sops-key)
if ($sops_key | is-empty) { if ($sops_key | is-empty) {
$env.CURRENT_INFRA_PATH = ((get-provisioning-infra-path) | path join (get-workspace-path | path basename)) $env.CURRENT_INFRA_PATH = ((get-provisioning-infra-path) | path join (get-workspace-path | path basename))
use ../../../sops_env.nu use ../../sops_env.nu
} }
#use sops/lib.nu on_sops #use sops/lib.nu on_sops
if $error_exit { if $error_exit {
@ -62,7 +63,7 @@ export def sops_cmd [
} }
export def load_defs [ export def load_defs [
]: nothing -> record { ] {
let vars_path = (get-provisioning-vars) let vars_path = (get-provisioning-vars)
if not ($vars_path | path exists) { if not ($vars_path | path exists) {
_print $"🛑 Error file (_ansi red_bold)($vars_path)(_ansi reset) not found" _print $"🛑 Error file (_ansi red_bold)($vars_path)(_ansi reset) not found"

View file

@ -4,13 +4,13 @@
# group = "infrastructure" # group = "infrastructure"
# tags = ["metadata", "cache", "validation"] # tags = ["metadata", "cache", "validation"]
# version = "1.0.0" # version = "1.0.0"
# requires = ["kcl:0.11.2"] # requires = ["nickel:0.11.2"]
# note = "Runtime bridge between KCL metadata schema and Nushell command dispatch" # note = "Runtime bridge between Nickel metadata schema and Nushell command dispatch"
# ============================================================================ # ============================================================================
# Command Metadata Cache System # Command Metadata Cache System
# Version: 1.0.0 # Version: 1.0.0
# Purpose: Load, cache, and validate command metadata from KCL schema # Purpose: Load, cache, and validate command metadata from Nickel schema
# ============================================================================ # ============================================================================
# Get cache directory # Get cache directory
@ -27,8 +27,8 @@ def get-cache-path [] : nothing -> string {
$"(get-cache-dir)/command_metadata.json" $"(get-cache-dir)/command_metadata.json"
} }
# Get KCL commands file path # Get Nickel commands file path
def get-kcl-path [] : nothing -> string { def get-nickel-path [] : nothing -> string {
let proj = ( let proj = (
if (($env.PROVISIONING_ROOT? | is-empty)) { if (($env.PROVISIONING_ROOT? | is-empty)) {
$"($env.HOME)/project-provisioning" $"($env.HOME)/project-provisioning"
@ -36,7 +36,7 @@ def get-kcl-path [] : nothing -> string {
$env.PROVISIONING_ROOT $env.PROVISIONING_ROOT
} }
) )
$"($proj)/provisioning/kcl/commands.k" $"($proj)/provisioning/nickel/commands.ncl"
} }
# Get file modification time (macOS / Linux) # Get file modification time (macOS / Linux)
@ -57,7 +57,7 @@ def get-file-mtime [file_path: string] : nothing -> int {
# Check if cache is valid # Check if cache is valid
def is-cache-valid [] : nothing -> bool { def is-cache-valid [] : nothing -> bool {
let cache_path = (get-cache-path) let cache_path = (get-cache-path)
let kcl_path = (get-kcl-path) let schema_path = (get-nickel-path)
if not (($cache_path | path exists)) { if not (($cache_path | path exists)) {
return false return false
@ -65,33 +65,48 @@ def is-cache-valid [] : nothing -> bool {
let now = (date now | format date "%s" | into int) let now = (date now | format date "%s" | into int)
let cache_mtime = (get-file-mtime $cache_path) let cache_mtime = (get-file-mtime $cache_path)
let kcl_mtime = (get-file-mtime $kcl_path) let schema_mtime = (get-file-mtime $schema_path)
let ttl = 3600 let ttl = 3600
let cache_age = ($now - $cache_mtime) let cache_age = ($now - $cache_mtime)
let not_expired = ($cache_age < $ttl) let not_expired = ($cache_age < $ttl)
let kcl_not_modified = ($cache_mtime > $kcl_mtime) let schema_not_modified = ($cache_mtime > $schema_mtime)
($not_expired and $kcl_not_modified) ($not_expired and $schema_not_modified)
} }
# Load metadata from KCL # Load metadata from Nickel
def load-from-kcl [] : nothing -> record { def load-from-nickel [] : nothing -> record {
let kcl_path = (get-kcl-path) # Nickel metadata loading is DISABLED due to Nickel hanging issues
# All commands work with empty metadata (metadata is optional per metadata_handler.nu:28)
# This ensures CLI stays responsive even if Nickel is misconfigured
let result = (^kcl run $kcl_path -S command_registry --format json | complete) # To re-enable Nickel metadata loading in the future:
# 1. Fix the Nickel command to not hang
# 2. Add proper timeout support to Nushell 0.109
# 3. Uncomment the code below and test thoroughly
if ($result.exit_code == 0) { {
$result.stdout | from json commands: {}
} else { version: "1.0.0"
{
error: $"Failed to load KCL"
commands: {}
version: "1.0.0"
}
} }
} }
# Original implementation (disabled due to Nickel hanging):
# def load-from-nickel [] : nothing -> record {
# let schema_path = (get-nickel-path)
# let result = (^nickel run $schema_path -S command_registry --format json | complete)
# if ($result.exit_code == 0) {
# $result.stdout | from json
# } else {
# {
# error: $"Failed to load Nickel"
# commands: {}
# version: "1.0.0"
# }
# }
# }
# Save metadata to cache # Save metadata to cache
export def cache-metadata [metadata: record] : nothing -> nothing { export def cache-metadata [metadata: record] : nothing -> nothing {
let dir = (get-cache-dir) let dir = (get-cache-dir)
@ -118,13 +133,13 @@ def load-from-cache [] : nothing -> record {
# Load command metadata with caching # Load command metadata with caching
export def load-command-metadata [] : nothing -> record { export def load-command-metadata [] : nothing -> record {
# Check if cache is valid before loading from KCL # Check if cache is valid before loading from Nickel
if (is-cache-valid) { if (is-cache-valid) {
# Use cached metadata # Use cached metadata
load-from-cache load-from-cache
} else { } else {
# Load from KCL and cache it # Load from Nickel and cache it
let metadata = (load-from-kcl) let metadata = (load-from-nickel)
# Cache it for next time # Cache it for next time
cache-metadata $metadata cache-metadata $metadata
$metadata $metadata
@ -141,7 +156,7 @@ export def invalidate-cache [] : nothing -> record {
} }
} | complete) } | complete)
load-from-kcl load-from-nickel
} }
# Get metadata for specific command # Get metadata for specific command
@ -362,11 +377,11 @@ export def filter-commands [criteria: record] : nothing -> table {
# Cache statistics # Cache statistics
export def cache-stats [] : nothing -> record { export def cache-stats [] : nothing -> record {
let cache_path = (get-cache-path) let cache_path = (get-cache-path)
let kcl_path = (get-kcl-path) let schema_path = (get-nickel-path)
let now = (date now | format date "%s" | into int) let now = (date now | format date "%s" | into int)
let cache_mtime = (get-file-mtime $cache_path) let cache_mtime = (get-file-mtime $cache_path)
let kcl_mtime = (get-file-mtime $kcl_path) let schema_mtime = (get-file-mtime $schema_path)
let cache_age = (if ($cache_mtime > 0) {($now - $cache_mtime)} else {-1}) let cache_age = (if ($cache_mtime > 0) {($now - $cache_mtime)} else {-1})
let ttl_remain = (if ($cache_age >= 0) {(3600 - $cache_age)} else {0}) let ttl_remain = (if ($cache_age >= 0) {(3600 - $cache_age)} else {0})
@ -377,8 +392,8 @@ export def cache-stats [] : nothing -> record {
cache_ttl_seconds: 3600 cache_ttl_seconds: 3600
cache_ttl_remaining: (if ($ttl_remain > 0) {$ttl_remain} else {0}) cache_ttl_remaining: (if ($ttl_remain > 0) {$ttl_remain} else {0})
cache_valid: (is-cache-valid) cache_valid: (is-cache-valid)
kcl_path: $kcl_path schema_path: $schema_path
kcl_exists: ($kcl_path | path exists) schema_exists: ($schema_path | path exists)
kcl_mtime_ago: (if ($kcl_mtime > 0) {($now - $kcl_mtime)} else {-1}) schema_mtime_ago: (if ($schema_mtime > 0) {($now - $schema_mtime)} else {-1})
} }
} }

View file

@ -1,242 +0,0 @@
# Modular Configuration Loading Architecture
## Overview
The configuration system has been refactored into modular components to achieve 2-3x performance improvements for regular commands while maintaining full functionality for complex operations.
## Architecture Layers
### Layer 1: Minimal Loader (0.023s)
**File**: `loader-minimal.nu` (~150 lines)
Contains only essential functions needed for:
- Workspace detection
- Environment determination
- Project root discovery
- Fast path detection
**Exported Functions**:
- `get-active-workspace` - Get current workspace
- `detect-current-environment` - Determine dev/test/prod
- `get-project-root` - Find project directory
- `get-defaults-config-path` - Path to default config
- `check-if-sops-encrypted` - SOPS file detection
- `find-sops-config-path` - Locate SOPS config
**Used by**:
- Help commands (help infrastructure, help workspace, etc.)
- Status commands
- Workspace listing
- Quick reference operations
### Layer 2: Lazy Loader (decision layer)
**File**: `loader-lazy.nu` (~80 lines)
Smart loader that decides which configuration to load:
- Fast path for help/status commands
- Full path for operations that need config
**Key Function**:
- `command-needs-full-config` - Determines if full config required
### Layer 3: Full Loader (0.091s)
**File**: `loader.nu` (1990 lines)
Original comprehensive loader that handles:
- Hierarchical config loading
- Variable interpolation
- Config validation
- Provider configuration
- Platform configuration
**Used by**:
- Server creation
- Infrastructure operations
- Deployment commands
- Anything needing full config
## Performance Characteristics
### Benchmarks
| Operation | Time | Notes |
|-----------|------|-------|
| Workspace detection | 0.023s | 23ms for minimal load |
| Full config load | 0.091s | ~4x slower than minimal |
| Help command | 0.040s | Uses minimal loader only |
| Status command | 0.030s | Fast path, no full config |
| Server operations | 0.150s+ | Requires full config load |
### Performance Gains
- **Help commands**: 30-40% faster (40ms vs 60ms with full config)
- **Workspace operations**: 50% faster (uses minimal loader)
- **Status checks**: Nearly instant (23ms)
## Module Dependency Graph
```
Help/Status Commands
loader-lazy.nu
loader-minimal.nu (workspace, environment detection)
(no further deps)
Infrastructure/Server Commands
loader-lazy.nu
loader.nu (full configuration)
├── loader-minimal.nu (for workspace detection)
├── Interpolation functions
├── Validation functions
└── Config merging logic
```
## Usage Examples
### Fast Path (Help Commands)
```nushell
# Uses minimal loader - 23ms
./provisioning help infrastructure
./provisioning workspace list
./provisioning version
```
### Medium Path (Status Operations)
```nushell
# Uses minimal loader with some full config - ~50ms
./provisioning status
./provisioning workspace active
./provisioning config validate
```
### Full Path (Infrastructure Operations)
```nushell
# Uses full loader - ~150ms
./provisioning server create --infra myinfra
./provisioning taskserv create kubernetes
./provisioning workflow submit batch.yaml
```
## Implementation Details
### Lazy Loading Decision Logic
```nushell
# In loader-lazy.nu
let is_fast_command = (
$command == "help" or
$command == "status" or
$command == "version"
)
if $is_fast_command {
# Use minimal loader only (0.023s)
get-minimal-config
} else {
# Load full configuration (0.091s)
load-provisioning-config
}
```
### Minimal Config Structure
The minimal loader returns a lightweight config record:
```nushell
{
workspace: {
name: "librecloud"
path: "/path/to/workspace_librecloud"
}
environment: "dev"
debug: false
paths: {
base: "/path/to/workspace_librecloud"
}
}
```
This is sufficient for:
- Workspace identification
- Environment determination
- Path resolution
- Help text generation
### Full Config Structure
The full loader returns comprehensive configuration with:
- Workspace settings
- Provider configurations
- Platform settings
- Interpolated variables
- Validation results
- Environment-specific overrides
## Migration Path
### For CLI Commands
1. Commands are already categorized (help, workspace, server, etc.)
2. Help system uses fast path (minimal loader)
3. Infrastructure commands use full path (full loader)
4. No changes needed to command implementations
### For New Modules
When creating new modules:
1. Check if full config is needed
2. If not, use `loader-minimal.nu` functions only
3. If yes, use `get-config` from main config accessor
## Future Optimizations
### Phase 2: Per-Command Config Caching
- Cache full config for 60 seconds
- Reuse config across related commands
- Potential: Additional 50% improvement
### Phase 3: Configuration Profiles
- Create thin config profiles for common scenarios
- Pre-loaded templates for workspace/infra combinations
- Fast switching between profiles
### Phase 4: Parallel Config Loading
- Load workspace and provider configs in parallel
- Async validation and interpolation
- Potential: 30% improvement for full config load
## Maintenance Notes
### Adding New Functions to Minimal Loader
Only add if:
1. Used by help/status commands
2. Doesn't require full config
3. Performance-critical path
### Modifying Full Loader
- Changes are backward compatible
- Validate against existing config files
- Update tests in test suite
### Performance Testing
```bash
# Benchmark minimal loader
time nu -n -c "use loader-minimal.nu *; get-active-workspace"
# Benchmark full loader
time nu -c "use config/accessor.nu *; get-config"
# Benchmark help command
time ./provisioning help infrastructure
```
## See Also
- `loader.nu` - Full configuration loading system
- `loader-minimal.nu` - Fast path loader
- `loader-lazy.nu` - Smart loader decision logic
- `config/ARCHITECTURE.md` - Configuration architecture details

View file

@ -0,0 +1,14 @@
# Configuration Accessor - Minimal
# Workaround for Nushell 0.110.0 parser bug
export def get-config [] {
{}
}
export def config-get [path: string, default_value: any = null] {
$default_value
}
export def get-full-config [] {
{}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,83 @@
# Configuration Accessor - Core
# Provides high-level configuration access methods
# Imports temporarily disabled due to Nushell parser bug
# use ../context_manager.nu *
# Define locally to avoid import cycle
def load-provisioning-config [workspace_path: string = "", environment: string = "default", --debug, --no-cache] {
{}
}
# Get current configuration
export def get-config [--force-reload] {
load-provisioning-config
}
# Get configuration value using dot notation path
export def config-get [
path: string
default_value: any = null
--config: any = null
] {
let cfg = if ($config != null) {
$config
} else {
load-provisioning-config
}
$default_value
}
# Check if a configuration path exists
export def config-has [path: string] {
false
}
# Set configuration value
export def config-set [path: string, value: any] {
# No-op
}
# Merge configurations
export def config-merge [configs: list] {
{}
}
# Get environment configuration
export def get-environment-config [
environment: string = "default"
--config: any = null
--debug
--validate
--skip-env-detection
] {
if $debug {
print $"Getting config for environment: $environment"
}
load-provisioning-config
}
# Get full configuration
export def get-full-config [
--debug
--validate
--skip-env-detection
] {
if $debug {
print "Getting full configuration"
}
load-provisioning-config
}
# Check if config value is set
export def is-config-set [path: string] {
false
}
# Get configuration section
export def config-section [section: string] {
{}
}

View file

@ -0,0 +1,132 @@
# Module: Configuration Accessor Functions
# Purpose: Provides 60+ specific accessor functions for individual configuration paths (debug, sops, paths, output, etc.)
# Dependencies: accessor_core for get-config and config-get
# Get provisioning URL
export def get-provisioning-url [] : nothing -> string {
$env.PROVISIONING_URL? | default "https://provisioning.systems"
}
# Get components library path (extensions/components — flat structure post-migration).
# Resolution order: PROVISIONING_COMPONENTS_PATH env → paths.components config →
# derived as sibling of PROVISIONING_TASKSERVS_PATH → PROVISIONING/extensions/components
export def get-components-path [] : nothing -> string {
let from_env = ($env.PROVISIONING_COMPONENTS_PATH? | default "")
if ($from_env | is-not-empty) and ($from_env | path exists) { return $from_env }
let configured = (config-get "paths.components" "")
if ($configured | is-not-empty) and ($configured | path exists) { return $configured }
# Derive from PROVISIONING root
let prov = ($env.PROVISIONING? | default "")
if ($prov | is-not-empty) {
let derived = ($prov | path join "extensions" | path join "components")
if ($derived | path exists) { return $derived }
}
""
}
# Get taskservs library path.
# Post-migration: extensions/components/ is the primary source.
# Falls back to extensions/taskservs/ for non-migrated workspaces.
# Resolution order: PROVISIONING_TASKSERVS_PATH (if exists on disk) →
# components path → PROVISIONING/extensions/taskservs
export def get-taskservs-path [] : nothing -> string {
# Env var set by env.nu — already points to components/ post-migration
let from_env = ($env.PROVISIONING_TASKSERVS_PATH? | default "")
if ($from_env | is-not-empty) and ($from_env | path exists) { return $from_env }
# components/ explicit
let components = (get-components-path)
if ($components | is-not-empty) and ($components | path exists) { return $components }
# Legacy taskservs/
let prov = ($env.PROVISIONING? | default "")
if ($prov | is-not-empty) {
let ts = ($prov | path join "extensions" | path join "taskservs")
if ($ts | path exists) { return $ts }
}
$from_env
}
# Get run-taskservs path (workspace-side generated taskserv files)
export def get-run-taskservs-path [] : nothing -> string {
config-get "paths.run_taskservs" "taskservs"
}
# Get workspace vars format: "json" or "yaml"
export def get-provisioning-wk-format [] : nothing -> string {
$env.PROVISIONING_WK_FORMAT? | default (config-get "output.format" "yaml")
}
# Whether to use Nickel for taskserv templating — true by default, disable with PROVISIONING_USE_NICKEL=false
export def get-use-nickel [] : nothing -> bool {
($env.PROVISIONING_USE_NICKEL? | default "true") != "false"
}
# Path to SOPS keys file (for secrets decryption)
export def get-keys-path [] : nothing -> string {
config-get "paths.files.keys" ".keys.k"
}
# Path to the vars file for the current taskserv run (set by run.nu make_cmd_env_temp)
export def get-provisioning-vars [] : nothing -> string {
$env.PROVISIONING_VARS? | default ""
}
# Path to the working env directory for the current taskserv (set by run.nu make_cmd_env_temp)
export def get-provisioning-wk-env-path [] : nothing -> string {
$env.PROVISIONING_WK_ENV_PATH? | default ""
}
# Path to the extensions/providers/ tree. Resolution order:
# PROVISIONING_PROVIDERS_PATH env → paths.providers config → PROVISIONING/extensions/providers → "".
# Empty result means "no providers available"; callers must guard with `| is-empty` or `| path exists`.
export def get-providers-path [] : nothing -> string {
let from_env = ($env.PROVISIONING_PROVIDERS_PATH? | default "")
if ($from_env | is-not-empty) and ($from_env | path exists) { return $from_env }
let configured = (config-get "paths.providers" "")
if ($configured | is-not-empty) and ($configured | path exists) { return $configured }
let prov = ($env.PROVISIONING? | default "")
if ($prov | is-not-empty) {
let derived = ($prov | path join "extensions" | path join "providers")
if ($derived | path exists) { return $derived }
}
""
}
# Path to the shared provider library (extensions/providers/prov_lib/).
export def get-prov-lib-path [] : nothing -> string {
let providers = (get-providers-path)
if ($providers | is-empty) { return "" }
$providers | path join "prov_lib"
}
# Path to provisioning/core/nulib/ from the PROVISIONING root.
export def get-core-nulib-path [] : nothing -> string {
let prov = ($env.PROVISIONING? | default "")
if ($prov | is-empty) { return "" }
$prov | path join "core" | path join "nulib"
}
# Directory name where per-provider generated defs live (relative to a provider dir).
export def get-provisioning-generate-dirpath [] : nothing -> string {
$env.PROVISIONING_GENERATE_DIRPATH? | default "generate"
}
# Filename for per-provider generated defs inside get-provisioning-generate-dirpath.
export def get-provisioning-generate-defsfile [] : nothing -> string {
$env.PROVISIONING_GENERATE_DEFSFILE? | default "defs.ncl"
}
# Path to the tools required-versions file (nickel/yaml declaring required tool versions).
# Resolution: PROVISIONING_REQ_VERSIONS env → paths.req_versions config → PROVISIONING/resources/tools.yaml → "".
export def get-provisioning-req-versions [] : nothing -> string {
let from_env = ($env.PROVISIONING_REQ_VERSIONS? | default "")
if ($from_env | is-not-empty) and ($from_env | path exists) { return $from_env }
let configured = (config-get "paths.req_versions" "")
if ($configured | is-not-empty) and ($configured | path exists) { return $configured }
let prov = ($env.PROVISIONING? | default "")
if ($prov | is-not-empty) {
let derived = ($prov | path join "resources" | path join "tools.yaml")
if ($derived | path exists) { return $derived }
}
""
}

View file

@ -0,0 +1,68 @@
# Module: Configuration Accessor System
# Reads platform service endpoints from deployment-mode.ncl via the platform target module.
# All other paths return their default values.
use ../../platform/target.nu [load-deployment-mode]
# Build a service URL from a service config record (server.{host,port} or endpoint field).
def service-cfg-url [cfg: record]: nothing -> string {
let explicit = ($cfg | get -o endpoint | default "")
if ($explicit | is-not-empty) { return $explicit }
let srv = ($cfg | get -o server)
if $srv == null { return "" }
let host = ($srv | get -o host | default "127.0.0.1")
let port = ($srv | get -o port | default 0)
if $port == 0 { "" } else { $"http://($host):($port)" }
}
# Resolve known platform URL paths from deployment-mode.ncl.
# Returns null for unrecognised paths so config-get falls back to default_value.
def resolve-platform-path [deployment: record, path: string]: nothing -> any {
match $path {
"platform.orchestrator.url" => {
let svc = ($deployment | get -o orchestrator)
if $svc == null { null } else { let u = (service-cfg-url $svc); if ($u | is-empty) { null } else { $u } }
}
"platform.orchestrator.endpoint" => {
let svc = ($deployment | get -o orchestrator)
if $svc == null { null } else { let u = (service-cfg-url $svc); if ($u | is-empty) { null } else { $"($u)/health" } }
}
"platform.control_center.url" => {
let svc = ($deployment | get -o control_center)
if $svc == null { null } else { let u = (service-cfg-url $svc); if ($u | is-empty) { null } else { $u } }
}
"platform.kms.url" => {
let svc = ($deployment | get -o vault_service)
if $svc == null { null } else { let u = (service-cfg-url $svc); if ($u | is-empty) { null } else { $u } }
}
"platform.kms.endpoint" => {
let svc = ($deployment | get -o vault_service)
if $svc == null { null } else { let u = (service-cfg-url $svc); if ($u | is-empty) { null } else { $"($u)/health" } }
}
_ => { null }
}
}
export def get-config []: nothing -> record {
load-deployment-mode
}
export def config-get [path: string, default_value: any = null, --config: any = null]: nothing -> any {
let deployment = if ($config != null) { $config } else { load-deployment-mode }
let val = (resolve-platform-path $deployment $path)
if $val == null { $default_value } else { $val }
}
export def get-full-config []: nothing -> record {
load-deployment-mode
}
# Selective re-export (ADR-025 Phase 3 Layer 3).
export use ./functions.nu [
get-provisioning-url get-components-path get-taskservs-path
get-run-taskservs-path get-provisioning-wk-format get-use-nickel
get-keys-path get-provisioning-vars get-provisioning-wk-env-path
get-providers-path get-prov-lib-path get-core-nulib-path
get-provisioning-generate-dirpath get-provisioning-generate-defsfile
get-provisioning-req-versions
]

View file

@ -0,0 +1,873 @@
# Configuration Accessor Functions
# Generated from Nickel schema: {$env.PROVISIONING}/schemas/config/settings/main.ncl
# DO NOT EDIT - Generated by accessor_generator.nu v1.0.0
#
# Generator version: 1.0.0
# Generated: 2026-01-13T13:49:23Z
# Schema: {$env.PROVISIONING}/schemas/config/settings/main.ncl
# Schema Hash: e129e50bba0128e066412eb63b12f6fd0f955d43133e1826dd5dc9405b8a9647
# Accessor Count: 76
#
# This file contains 76 accessor functions automatically generated
# from the Nickel schema. Each function provides type-safe access to a
# configuration value with proper defaults.
#
# NUSHELL COMPLIANCE:
# - Rule 3: No mutable variables, uses reduce fold
# - Rule 5: Uses do-complete error handling pattern
# - Rule 8: Uses is-not-empty and each
# - Rule 9: Boolean flags without type annotations
# - Rule 11: All functions are exported
# - Rule 15: No parameterized types
#
# NICKEL COMPLIANCE:
# - Schema-first design with all fields from schema
# - Design by contract via schema validation
# - JSON output validation for schema types
# Selective imports — mirror the accessor orchestrator's re-exports (ADR-025 L2).
use lib_provisioning/config/accessor.nu [
config-get get-config get-full-config
get-provisioning-url get-components-path get-taskservs-path
get-run-taskservs-path get-provisioning-wk-format get-use-nickel
get-keys-path get-provisioning-vars get-provisioning-wk-env-path
get-providers-path get-prov-lib-path get-core-nulib-path
get-provisioning-generate-dirpath get-provisioning-generate-defsfile
get-provisioning-req-versions
]
export def get-DefaultAIProvider-enable_query_ai [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultAIProvider.enable_query_ai" true --config $cfg
}
export def get-DefaultAIProvider-enable_template_ai [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultAIProvider.enable_template_ai" true --config $cfg
}
export def get-DefaultAIProvider-enable_webhook_ai [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultAIProvider.enable_webhook_ai" false --config $cfg
}
export def get-DefaultAIProvider-enabled [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultAIProvider.enabled" false --config $cfg
}
export def get-DefaultAIProvider-max_tokens [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultAIProvider.max_tokens" 2048 --config $cfg
}
export def get-DefaultAIProvider-provider [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultAIProvider.provider" "openai" --config $cfg
}
export def get-DefaultAIProvider-temperature [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultAIProvider.temperature" 0.3 --config $cfg
}
export def get-DefaultAIProvider-timeout [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultAIProvider.timeout" 30 --config $cfg
}
export def get-DefaultKmsConfig-auth_method [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultKmsConfig.auth_method" "certificate" --config $cfg
}
export def get-DefaultKmsConfig-server_url [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultKmsConfig.server_url" "" --config $cfg
}
export def get-DefaultKmsConfig-timeout [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultKmsConfig.timeout" 30 --config $cfg
}
export def get-DefaultKmsConfig-verify_ssl [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultKmsConfig.verify_ssl" true --config $cfg
}
export def get-DefaultRunSet-inventory_file [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultRunSet.inventory_file" "./inventory.yaml" --config $cfg
}
export def get-DefaultRunSet-output_format [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultRunSet.output_format" "human" --config $cfg
}
export def get-DefaultRunSet-output_path [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultRunSet.output_path" "tmp/NOW-deploy" --config $cfg
}
export def get-DefaultRunSet-use_time [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultRunSet.use_time" true --config $cfg
}
export def get-DefaultRunSet-wait [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultRunSet.wait" true --config $cfg
}
export def get-DefaultSecretProvider-provider [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSecretProvider.provider" "sops" --config $cfg
}
export def get-DefaultSettings-cluster_admin_host [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSettings.cluster_admin_host" "" --config $cfg
}
export def get-DefaultSettings-cluster_admin_port [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSettings.cluster_admin_port" 22 --config $cfg
}
export def get-DefaultSettings-cluster_admin_user [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSettings.cluster_admin_user" "root" --config $cfg
}
export def get-DefaultSettings-clusters_paths [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSettings.clusters_paths" null --config $cfg
}
export def get-DefaultSettings-clusters_save_path [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSettings.clusters_save_path" "/${main_name}/clusters" --config $cfg
}
export def get-DefaultSettings-created_clusters_dirpath [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSettings.created_clusters_dirpath" "./tmp/NOW_clusters" --config $cfg
}
export def get-DefaultSettings-created_taskservs_dirpath [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSettings.created_taskservs_dirpath" "./tmp/NOW_deployment" --config $cfg
}
export def get-DefaultSettings-defaults_provs_dirpath [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSettings.defaults_provs_dirpath" "./defs" --config $cfg
}
export def get-DefaultSettings-defaults_provs_suffix [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSettings.defaults_provs_suffix" "_defaults.k" --config $cfg
}
export def get-DefaultSettings-main_name [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSettings.main_name" "" --config $cfg
}
export def get-DefaultSettings-main_title [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSettings.main_title" "" --config $cfg
}
export def get-DefaultSettings-prov_clusters_path [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSettings.prov_clusters_path" "./clusters" --config $cfg
}
export def get-DefaultSettings-prov_data_dirpath [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSettings.prov_data_dirpath" "./data" --config $cfg
}
export def get-DefaultSettings-prov_data_suffix [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSettings.prov_data_suffix" "_settings.k" --config $cfg
}
export def get-DefaultSettings-prov_local_bin_path [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSettings.prov_local_bin_path" "./bin" --config $cfg
}
export def get-DefaultSettings-prov_resources_path [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSettings.prov_resources_path" "./resources" --config $cfg
}
export def get-DefaultSettings-servers_paths [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSettings.servers_paths" null --config $cfg
}
export def get-DefaultSettings-servers_wait_started [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSettings.servers_wait_started" 27 --config $cfg
}
export def get-DefaultSettings-settings_path [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSettings.settings_path" "./settings.yaml" --config $cfg
}
export def get-DefaultSopsConfig-use_age [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "DefaultSopsConfig.use_age" true --config $cfg
}
export def get-defaults-ai_provider-enable_query_ai [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.ai_provider.enable_query_ai" true --config $cfg
}
export def get-defaults-ai_provider-enable_template_ai [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.ai_provider.enable_template_ai" true --config $cfg
}
export def get-defaults-ai_provider-enable_webhook_ai [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.ai_provider.enable_webhook_ai" false --config $cfg
}
export def get-defaults-ai_provider-enabled [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.ai_provider.enabled" false --config $cfg
}
export def get-defaults-ai_provider-max_tokens [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.ai_provider.max_tokens" 2048 --config $cfg
}
export def get-defaults-ai_provider-provider [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.ai_provider.provider" "openai" --config $cfg
}
export def get-defaults-ai_provider-temperature [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.ai_provider.temperature" 0.3 --config $cfg
}
export def get-defaults-ai_provider-timeout [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.ai_provider.timeout" 30 --config $cfg
}
export def get-defaults-kms_config-auth_method [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.kms_config.auth_method" "certificate" --config $cfg
}
export def get-defaults-kms_config-server_url [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.kms_config.server_url" "" --config $cfg
}
export def get-defaults-kms_config-timeout [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.kms_config.timeout" 30 --config $cfg
}
export def get-defaults-kms_config-verify_ssl [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.kms_config.verify_ssl" true --config $cfg
}
export def get-defaults-run_set-inventory_file [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.run_set.inventory_file" "./inventory.yaml" --config $cfg
}
export def get-defaults-run_set-output_format [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.run_set.output_format" "human" --config $cfg
}
export def get-defaults-run_set-output_path [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.run_set.output_path" "tmp/NOW-deploy" --config $cfg
}
export def get-defaults-run_set-use_time [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.run_set.use_time" true --config $cfg
}
export def get-defaults-run_set-wait [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.run_set.wait" true --config $cfg
}
export def get-defaults-secret_provider-provider [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.secret_provider.provider" "sops" --config $cfg
}
export def get-defaults-settings-cluster_admin_host [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.settings.cluster_admin_host" "" --config $cfg
}
export def get-defaults-settings-cluster_admin_port [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.settings.cluster_admin_port" 22 --config $cfg
}
export def get-defaults-settings-cluster_admin_user [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.settings.cluster_admin_user" "root" --config $cfg
}
export def get-defaults-settings-clusters_paths [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.settings.clusters_paths" null --config $cfg
}
export def get-defaults-settings-clusters_save_path [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.settings.clusters_save_path" "/${main_name}/clusters" --config $cfg
}
export def get-defaults-settings-created_clusters_dirpath [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.settings.created_clusters_dirpath" "./tmp/NOW_clusters" --config $cfg
}
export def get-defaults-settings-created_taskservs_dirpath [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.settings.created_taskservs_dirpath" "./tmp/NOW_deployment" --config $cfg
}
export def get-defaults-settings-defaults_provs_dirpath [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.settings.defaults_provs_dirpath" "./defs" --config $cfg
}
export def get-defaults-settings-defaults_provs_suffix [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.settings.defaults_provs_suffix" "_defaults.k" --config $cfg
}
export def get-defaults-settings-main_name [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.settings.main_name" "" --config $cfg
}
export def get-defaults-settings-main_title [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.settings.main_title" "" --config $cfg
}
export def get-defaults-settings-prov_clusters_path [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.settings.prov_clusters_path" "./clusters" --config $cfg
}
export def get-defaults-settings-prov_data_dirpath [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.settings.prov_data_dirpath" "./data" --config $cfg
}
export def get-defaults-settings-prov_data_suffix [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.settings.prov_data_suffix" "_settings.k" --config $cfg
}
export def get-defaults-settings-prov_local_bin_path [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.settings.prov_local_bin_path" "./bin" --config $cfg
}
export def get-defaults-settings-prov_resources_path [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.settings.prov_resources_path" "./resources" --config $cfg
}
export def get-defaults-settings-servers_paths [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.settings.servers_paths" null --config $cfg
}
export def get-defaults-settings-servers_wait_started [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.settings.servers_wait_started" 27 --config $cfg
}
export def get-defaults-settings-settings_path [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.settings.settings_path" "./settings.yaml" --config $cfg
}
export def get-defaults-sops_config-use_age [
--cfg_input: any = null
] {
let cfg = if ($cfg_input | is-not-empty) {
$cfg_input
} else {
get-config
}
config-get "defaults.sops_config.use_age" true --config $cfg
}

View file

@ -0,0 +1,203 @@
# Accessor Registry - Maps config paths to getters
# This eliminates 80+ duplicate getter function definitions
# Pattern: { name: { path: "config.path", default: default_value } }
export def build-accessor-registry [] {
{
# Core configuration accessors
paths: { path: "paths", default: {} }
debug: { path: "debug", default: {} }
sops: { path: "sops", default: {} }
validation: { path: "validation", default: {} }
output: { path: "output", default: {} }
# Provisioning core settings
provisioning-name: { path: "core.name", default: "provisioning" }
provisioning-vers: { path: "core.version", default: "2.0.0" }
provisioning-url: { path: "core.url", default: "https://provisioning.systems" }
# Debug settings
debug-enabled: { path: "debug.enabled", default: false }
no-terminal: { path: "debug.no_terminal", default: false }
debug-check-enabled: { path: "debug.check", default: false }
metadata-enabled: { path: "debug.metadata", default: false }
debug-remote-enabled: { path: "debug.remote", default: false }
ssh-debug-enabled: { path: "debug.ssh", default: false }
provisioning-log-level: { path: "debug.log_level", default: "" }
debug-match-cmd: { path: "debug.match_cmd", default: "" }
# Output configuration
work-format: { path: "output.format", default: "yaml" }
file-viewer: { path: "output.file_viewer", default: "bat" }
match-date: { path: "output.match_date", default: "%Y_%m_%d" }
# Paths configuration
workspace-path: { path: "paths.workspace", default: "" }
providers-path: { path: "paths.providers", default: "" }
taskservs-path: { path: "paths.taskservs", default: "" }
clusters-path: { path: "paths.clusters", default: "" }
templates-path: { path: "paths.templates", default: "" }
tools-path: { path: "paths.tools", default: "" }
extensions-path: { path: "paths.extensions", default: "" }
infra-path: { path: "paths.infra", default: "" }
generate-dirpath: { path: "paths.generate", default: "generate" }
custom-providers-path: { path: "paths.custom_providers", default: "" }
custom-taskservs-path: { path: "paths.custom_taskservs", default: "" }
run-taskservs-path: { path: "paths.run_taskservs", default: "taskservs" }
run-clusters-path: { path: "paths.run_clusters", default: "clusters" }
# Path files
defs-file: { path: "paths.files.defs", default: "defs.nu" }
req-versions: { path: "paths.files.req_versions", default: "" }
vars-file: { path: "paths.files.vars", default: "" }
notify-icon: { path: "paths.files.notify_icon", default: "" }
settings-file: { path: "paths.files.settings", default: "settings.ncl" }
keys-file: { path: "paths.files.keys", default: ".keys.ncl" }
# SOPS configuration
sops-key-paths: { path: "sops.key_search_paths", default: [] }
sops-use-sops: { path: "sops.use_sops", default: "age" }
sops-use-kms: { path: "sops.use_kms", default: "" }
secret-provider: { path: "sops.secret_provider", default: "sops" }
# SSH configuration
ssh-options: { path: "ssh.options", default: [] }
ssh-user: { path: "ssh.user", default: "" }
# Tools configuration
use-nickel: { path: "tools.use_nickel", default: false }
use-nickel-plugin: { path: "tools.use_nickel_plugin", default: false }
# Extensions configuration
extension-mode: { path: "extensions.mode", default: "full" }
provisioning-profile: { path: "extensions.profile", default: "" }
allowed-extensions: { path: "extensions.allowed", default: "" }
blocked-extensions: { path: "extensions.blocked", default: "" }
# AI configuration
ai-enabled: { path: "ai.enabled", default: false }
ai-provider: { path: "ai.provider", default: "openai" }
# KMS Core Settings
kms-enabled: { path: "kms.enabled", default: false }
kms-mode: { path: "kms.mode", default: "local" }
kms-version: { path: "kms.version", default: "1.0.0" }
kms-server: { path: "kms.server", default: "" }
kms-auth-method: { path: "kms.auth_method", default: "certificate" }
kms-client-cert: { path: "kms.client_cert", default: "" }
kms-client-key: { path: "kms.client_key", default: "" }
kms-ca-cert: { path: "kms.ca_cert", default: "" }
kms-api-token: { path: "kms.api_token", default: "" }
kms-username: { path: "kms.username", default: "" }
kms-password: { path: "kms.password", default: "" }
kms-timeout: { path: "kms.timeout", default: "30" }
kms-verify-ssl: { path: "kms.verify_ssl", default: "true" }
# KMS Paths
kms-base-path: { path: "kms.paths.base", default: "{{workspace.path}}/.kms" }
kms-keys-dir: { path: "kms.paths.keys_dir", default: "{{kms.paths.base}}/keys" }
kms-cache-dir: { path: "kms.paths.cache_dir", default: "{{kms.paths.base}}/cache" }
kms-config-dir: { path: "kms.paths.config_dir", default: "{{kms.paths.base}}/config" }
# KMS Local Settings
kms-local-enabled: { path: "kms.local.enabled", default: true }
kms-local-provider: { path: "kms.local.provider", default: "age" }
kms-local-key-path: { path: "kms.local.key_path", default: "{{kms.paths.keys_dir}}/age.txt" }
kms-local-sops-config: { path: "kms.local.sops_config", default: "{{workspace.path}}/.sops.yaml" }
# KMS Age Settings
kms-age-generate-on-init: { path: "kms.local.age.generate_key_on_init", default: false }
kms-age-key-format: { path: "kms.local.age.key_format", default: "age" }
kms-age-key-permissions: { path: "kms.local.age.key_permissions", default: "0600" }
# KMS SOPS Settings
kms-sops-config-path: { path: "kms.local.sops.config_path", default: "{{workspace.path}}/.sops.yaml" }
kms-sops-age-recipients: { path: "kms.local.sops.age_recipients", default: [] }
# KMS Vault Settings
kms-vault-address: { path: "kms.local.vault.address", default: "http://127.0.0.1:8200" }
kms-vault-token-path: { path: "kms.local.vault.token_path", default: "{{kms.paths.config_dir}}/vault-token" }
kms-vault-transit-path: { path: "kms.local.vault.transit_path", default: "transit" }
kms-vault-key-name: { path: "kms.local.vault.key_name", default: "provisioning" }
# KMS Remote Settings
kms-remote-enabled: { path: "kms.remote.enabled", default: false }
kms-remote-endpoint: { path: "kms.remote.endpoint", default: "" }
kms-remote-api-version: { path: "kms.remote.api_version", default: "v1" }
kms-remote-timeout: { path: "kms.remote.timeout_seconds", default: 30 }
kms-remote-retry-attempts: { path: "kms.remote.retry_attempts", default: 3 }
kms-remote-retry-delay: { path: "kms.remote.retry_delay_seconds", default: 2 }
# KMS Remote Auth
kms-remote-auth-method: { path: "kms.remote.auth.method", default: "token" }
kms-remote-token-path: { path: "kms.remote.auth.token_path", default: "{{kms.paths.config_dir}}/token" }
kms-remote-refresh-token: { path: "kms.remote.auth.refresh_token", default: true }
kms-remote-token-expiry: { path: "kms.remote.auth.token_expiry_seconds", default: 3600 }
# KMS Remote TLS
kms-remote-tls-enabled: { path: "kms.remote.tls.enabled", default: true }
kms-remote-tls-verify: { path: "kms.remote.tls.verify", default: true }
kms-remote-ca-cert-path: { path: "kms.remote.tls.ca_cert_path", default: "" }
kms-remote-client-cert-path: { path: "kms.remote.tls.client_cert_path", default: "" }
kms-remote-client-key-path: { path: "kms.remote.tls.client_key_path", default: "" }
kms-remote-tls-min-version: { path: "kms.remote.tls.min_version", default: "1.3" }
# KMS Remote Cache
kms-remote-cache-enabled: { path: "kms.remote.cache.enabled", default: true }
kms-remote-cache-ttl: { path: "kms.remote.cache.ttl_seconds", default: 300 }
kms-remote-cache-max-size: { path: "kms.remote.cache.max_size_mb", default: 50 }
# KMS Hybrid Mode
kms-hybrid-enabled: { path: "kms.hybrid.enabled", default: false }
kms-hybrid-fallback-to-local: { path: "kms.hybrid.fallback_to_local", default: true }
kms-hybrid-sync-keys: { path: "kms.hybrid.sync_keys", default: false }
# KMS Policies
kms-auto-rotate: { path: "kms.policies.auto_rotate", default: false }
kms-rotation-days: { path: "kms.policies.rotation_days", default: 90 }
kms-backup-enabled: { path: "kms.policies.backup_enabled", default: true }
kms-backup-path: { path: "kms.policies.backup_path", default: "{{kms.paths.base}}/backups" }
kms-audit-log-enabled: { path: "kms.policies.audit_log_enabled", default: false }
kms-audit-log-path: { path: "kms.policies.audit_log_path", default: "{{kms.paths.base}}/audit.log" }
# KMS Encryption
kms-encryption-algorithm: { path: "kms.encryption.algorithm", default: "ChaCha20-Poly1305" }
kms-key-derivation: { path: "kms.encryption.key_derivation", default: "scrypt" }
# KMS Security
kms-enforce-key-permissions: { path: "kms.security.enforce_key_permissions", default: true }
kms-disallow-plaintext-secrets: { path: "kms.security.disallow_plaintext_secrets", default: true }
kms-secret-scanning-enabled: { path: "kms.security.secret_scanning_enabled", default: false }
kms-min-key-size-bits: { path: "kms.security.min_key_size_bits", default: 256 }
# KMS Operations
kms-verbose: { path: "kms.operations.verbose", default: false }
kms-debug: { path: "kms.operations.debug", default: false }
kms-dry-run: { path: "kms.operations.dry_run", default: false }
kms-max-file-size-mb: { path: "kms.operations.max_file_size_mb", default: 100 }
# Provider settings
default-provider: { path: "providers.default", default: "local" }
}
}
# Get value using registry lookup
export def get-by-registry [name: string, config: record] {
let registry = (build-accessor-registry)
if not ($name in ($registry | columns)) {
error make { msg: $"Unknown accessor: ($name)" }
}
let accessor_def = ($registry | get $name)
let config_data = if ($config | is-empty) {
{}
} else {
$config
}
# Import and use get-config-value from loader module
use loader.nu get-config-value
get-config-value $config_data $accessor_def.path $accessor_def.default
}

View file

@ -1,128 +0,0 @@
#!/usr/bin/env nu
# Benchmark script comparing minimal vs full config loaders
# Shows performance improvements from modular architecture
use std log
# Run a command and measure execution time using bash 'time' command
def benchmark [name: string, cmd: string] {
# Use bash to run the command with time measurement
let output = (^bash -c $"time -p ($cmd) 2>&1 | grep real | awk '{print $2}'")
# Parse the output (format: 0.023)
let duration_s = ($output | str trim | into float)
let duration_ms = (($duration_s * 1000) | math round)
{
name: $name,
duration_ms: $duration_ms,
duration_human: $"{$duration_ms}ms"
}
}
# Benchmark minimal loader
def bench-minimal [] {
print "🚀 Benchmarking Minimal Loader..."
let result = (benchmark "Minimal: get-active-workspace"
"nu -n -c 'use provisioning/core/nulib/lib_provisioning/config/loader-minimal.nu *; get-active-workspace'")
print $" ✓ ($result.name): ($result.duration_human)"
$result
}
# Benchmark full loader
def bench-full [] {
print "🚀 Benchmarking Full Loader..."
let result = (benchmark "Full: get-config"
"nu -c 'use provisioning/core/nulib/lib_provisioning/config/accessor.nu *; get-config'")
print $" ✓ ($result.name): ($result.duration_human)"
$result
}
# Benchmark help command
def bench-help [] {
print "🚀 Benchmarking Help Commands..."
let commands = [
"help",
"help infrastructure",
"help workspace",
"help orchestration"
]
mut results = []
for cmd in $commands {
let result = (benchmark $"Help: ($cmd)"
$"./provisioning/core/cli/provisioning ($cmd) >/dev/null 2>&1")
print $" ✓ Help: ($cmd): ($result.duration_human)"
$results = ($results | append $result)
}
$results
}
# Benchmark workspace operations
def bench-workspace [] {
print "🚀 Benchmarking Workspace Commands..."
let commands = [
"workspace list",
"workspace active"
]
mut results = []
for cmd in $commands {
let result = (benchmark $"Workspace: ($cmd)"
$"./provisioning/core/cli/provisioning ($cmd) >/dev/null 2>&1")
print $" ✓ Workspace: ($cmd): ($result.duration_human)"
$results = ($results | append $result)
}
$results
}
# Main benchmark runner
export def main [] {
print "═════════════════════════════════════════════════════════════"
print "Configuration Loader Performance Benchmarks"
print "═════════════════════════════════════════════════════════════"
print ""
# Run benchmarks
let minimal = (bench-minimal)
print ""
let full = (bench-full)
print ""
let help = (bench-help)
print ""
let workspace = (bench-workspace)
print ""
# Calculate improvements
let improvement = (($full.duration_ms - $minimal.duration_ms) / ($full.duration_ms) * 100 | into int)
print "═════════════════════════════════════════════════════════════"
print "Performance Summary"
print "═════════════════════════════════════════════════════════════"
print ""
print $"Minimal Loader: ($minimal.duration_ms)ms"
print $"Full Loader: ($full.duration_ms)ms"
print $"Speed Improvement: ($improvement)% faster"
print ""
print "Fast Path Operations (using minimal loader):"
print $" • Help commands: ~($help | map {|r| $r.duration_ms} | math avg)ms average"
print $" • Workspace ops: ~($workspace | map {|r| $r.duration_ms} | math avg)ms average"
print ""
print "✅ Modular architecture provides significant performance gains!"
print " Help/Status commands: 4x+ faster"
print " No performance penalty for infrastructure operations"
print ""
}
main

View file

@ -1,285 +0,0 @@
# Cache Performance Benchmarking Suite
# Measures cache performance and demonstrates improvements
# Compares cold vs warm loads
use ./core.nu *
use ./metadata.nu *
use ./config_manager.nu *
use ./kcl.nu *
use ./sops.nu *
use ./final.nu *
# Helper: Measure execution time of a block
def measure_time [
label: string
block: closure
] {
let start = (date now | into int)
do { ^$block } | complete | ignore
let end = (date now | into int)
let elapsed_ms = (($end - $start) / 1000000)
return {
label: $label
elapsed_ms: $elapsed_ms
}
}
print "═══════════════════════════════════════════════════════════════"
print "Cache Performance Benchmarks"
print "═══════════════════════════════════════════════════════════════"
print ""
# ====== BENCHMARK 1: CACHE WRITE PERFORMANCE ======
print "Benchmark 1: Cache Write Performance"
print "─────────────────────────────────────────────────────────────────"
print ""
mut write_times = []
for i in 1..5 {
let time_result = (measure_time $"Cache write (run ($i))" {
let test_data = {
name: $"test_($i)"
value: $i
nested: {
field1: "value1"
field2: "value2"
field3: { deep: "nested" }
}
}
cache-write "benchmark" $"key_($i)" $test_data ["/tmp/test_($i).yaml"]
})
$write_times = ($write_times | append $time_result.elapsed_ms)
print $" Run ($i): ($time_result.elapsed_ms)ms"
}
let avg_write = ($write_times | math avg | math round)
print $" Average: ($avg_write)ms"
print ""
# ====== BENCHMARK 2: CACHE LOOKUP (COLD MISS) ======
print "Benchmark 2: Cache Lookup (Cold Miss)"
print "─────────────────────────────────────────────────────────────────"
print ""
mut miss_times = []
for i in 1..5 {
let time_result = (measure_time $"Cache miss lookup (run ($i))" {
cache-lookup "benchmark" $"nonexistent_($i)"
})
$miss_times = ($miss_times | append $time_result.elapsed_ms)
print $" Run ($i): ($time_result.elapsed_ms)ms"
}
let avg_miss = ($miss_times | math avg | math round)
print $" Average: ($avg_miss)ms (should be fast - just file check)"
print ""
# ====== BENCHMARK 3: CACHE LOOKUP (WARM HIT) ======
print "Benchmark 3: Cache Lookup (Warm Hit)"
print "─────────────────────────────────────────────────────────────────"
print ""
# Pre-warm the cache
cache-write "benchmark" "warmkey" { test: "data" } ["/tmp/warmkey.yaml"]
mut hit_times = []
for i in 1..10 {
let time_result = (measure_time $"Cache hit lookup (run ($i))" {
cache-lookup "benchmark" "warmkey"
})
$hit_times = ($hit_times | append $time_result.elapsed_ms)
print $" Run ($i): ($time_result.elapsed_ms)ms"
}
let avg_hit = ($hit_times | math avg | math round)
let min_hit = ($hit_times | math min)
let max_hit = ($hit_times | math max)
print ""
print $" Average: ($avg_hit)ms"
print $" Min: ($min_hit)ms (best case)"
print $" Max: ($max_hit)ms (worst case)"
print ""
# ====== BENCHMARK 4: CONFIGURATION MANAGER OPERATIONS ======
print "Benchmark 4: Configuration Manager Operations"
print "─────────────────────────────────────────────────────────────────"
print ""
# Test get config
let get_time = (measure_time "Config get" {
get-cache-config
})
print $" Get cache config: ($get_time.elapsed_ms)ms"
# Test cache-config-get
let get_setting_times = []
for i in 1..3 {
let time_result = (measure_time $"Get setting (run ($i))" {
cache-config-get "enabled"
})
$get_setting_times = ($get_setting_times | append $time_result.elapsed_ms)
}
let avg_get_setting = ($get_setting_times | math avg | math round)
print $" Get specific setting (avg of 3): ($avg_get_setting)ms"
# Test cache-config-set
let set_time = (measure_time "Config set" {
cache-config-set "test_key" true
})
print $" Set cache config: ($set_time.elapsed_ms)ms"
print ""
# ====== BENCHMARK 5: CACHE STATS OPERATIONS ======
print "Benchmark 5: Cache Statistics Operations"
print "─────────────────────────────────────────────────────────────────"
print ""
# KCL cache stats
let kcl_stats_time = (measure_time "KCL cache stats" {
get-kcl-cache-stats
})
print $" KCL cache stats: ($kcl_stats_time.elapsed_ms)ms"
# SOPS cache stats
let sops_stats_time = (measure_time "SOPS cache stats" {
get-sops-cache-stats
})
print $" SOPS cache stats: ($sops_stats_time.elapsed_ms)ms"
# Final config cache stats
let final_stats_time = (measure_time "Final config cache stats" {
get-final-config-stats
})
print $" Final config cache stats: ($final_stats_time.elapsed_ms)ms"
print ""
# ====== PERFORMANCE ANALYSIS ======
print "═══════════════════════════════════════════════════════════════"
print "Performance Analysis"
print "═══════════════════════════════════════════════════════════════"
print ""
# Calculate improvement ratio
let write_to_hit_ratio = if $avg_hit > 0 {
(($avg_write / $avg_hit) | math round)
} else {
0
}
let miss_to_hit_ratio = if $avg_hit > 0 {
(($avg_miss / $avg_hit) | math round)
} else {
0
}
print "Cache Efficiency Metrics:"
print "─────────────────────────────────────────────────────────────────"
print $" Cache Write Time: ($avg_write)ms"
print $" Cache Hit Time: ($avg_hit)ms (5-10ms target)"
print $" Cache Miss Time: ($avg_miss)ms (fast rejection)"
print ""
print "Performance Ratios:"
print "─────────────────────────────────────────────────────────────────"
print $" Write vs Hit: ($write_to_hit_ratio)x slower to populate cache"
print $" Miss vs Hit: ($miss_to_hit_ratio)x time for rejection"
print ""
# Theoretical improvement
print "Theoretical Improvements (based on config loading benchmarks):"
print "─────────────────────────────────────────────────────────────────"
# Assume typical config load breakdown:
# - KCL compilation: 50ms
# - SOPS decryption: 30ms
# - File I/O + parsing: 40ms
# - Other: 30ms
# Total cold: ~150ms
let cold_load = 150 # milliseconds
let warm_load = $avg_hit
let improvement = if $warm_load > 0 {
((($cold_load - $warm_load) / $cold_load) * 100 | math round)
} else {
0
}
print $" Estimated cold load: ($cold_load)ms (typical)"
print $" Estimated warm load: ($warm_load)ms (with cache hit)"
print $" Improvement: ($improvement)% faster"
print ""
# Multi-command scenario
let commands_per_session = 5
let cold_total = $cold_load * $commands_per_session
let warm_total = $avg_hit * $commands_per_session
let multi_improvement = if $warm_total > 0 {
((($cold_total - $warm_total) / $cold_total) * 100 | math round)
} else {
0
}
print "Multi-Command Session (5 commands):"
print "─────────────────────────────────────────────────────────────────"
print $" Without cache: ($cold_total)ms"
print $" With cache: ($warm_total)ms"
print $" Session speedup: ($multi_improvement)% faster"
print ""
# ====== RECOMMENDATIONS ======
print "═══════════════════════════════════════════════════════════════"
print "Recommendations"
print "═══════════════════════════════════════════════════════════════"
print ""
if $avg_hit < 10 {
print "✅ Cache hit performance EXCELLENT (< 10ms)"
} else if $avg_hit < 15 {
print "⚠️ Cache hit performance GOOD (< 15ms)"
} else {
print "⚠️ Cache hit performance could be improved"
}
if $avg_write < 50 {
print "✅ Cache write performance EXCELLENT (< 50ms)"
} else if $avg_write < 100 {
print "⚠️ Cache write performance ACCEPTABLE (< 100ms)"
} else {
print "⚠️ Cache write performance could be improved"
}
if $improvement > 80 {
print $"✅ Overall improvement EXCELLENT ($improvement%)"
} else if $improvement > 50 {
print $"✅ Overall improvement GOOD ($improvement%)"
} else {
print $"⚠️ Overall improvement could be optimized"
}
print ""
print "End of Benchmark Suite"
print "═══════════════════════════════════════════════════════════════"

View file

@ -1,495 +0,0 @@
# Cache Management Commands Module
# Provides CLI interface for cache operations and configuration management
# Follows Nushell 0.109.0+ guidelines strictly
use ./core.nu *
use ./metadata.nu *
use ./config_manager.nu *
use ./kcl.nu *
use ./sops.nu *
use ./final.nu *
# Clear cache (data operations)
export def cache-clear [
--type: string = "all" # Cache type to clear (all, kcl, sops, final, provider, platform)
---force = false # Force without confirmation
] {
let cache_types = match $type {
"all" => ["kcl", "sops", "final", "provider", "platform"]
_ => [$type]
}
mut cleared_count = 0
mut errors = []
for cache_type in $cache_types {
let result = (do {
match $cache_type {
"kcl" => {
clear-kcl-cache --all
}
"sops" => {
clear-sops-cache --pattern "*"
}
"final" => {
clear-final-config-cache --workspace "*"
}
_ => {
print $"⚠️ Unsupported cache type: ($cache_type)"
}
}
} | complete)
if $result.exit_code == 0 {
$cleared_count = ($cleared_count + 1)
} else {
$errors = ($errors | append $"Failed to clear ($cache_type): ($result.stderr)")
}
}
if $cleared_count > 0 {
print $"✅ Cleared ($cleared_count) cache types"
}
if not ($errors | is-empty) {
for error in $errors {
print $"❌ ($error)"
}
}
}
# List cache entries
export def cache-list [
--type: string = "*" # Cache type filter (kcl, sops, final, etc.)
--format: string = "table" # Output format (table, json, yaml)
] {
mut all_entries = []
# List KCL cache
if $type in ["*", "kcl"] {
let kcl_entries = (do {
let cache_base = (get-cache-base-path)
let kcl_dir = $"($cache_base)/kcl"
if ($kcl_dir | path exists) {
let cache_files = (glob $"($kcl_dir)/*.json" | where { |f| not ($f | str ends-with ".meta") })
for cache_file in $cache_files {
let meta_file = $"($cache_file).meta"
if ($meta_file | path exists) {
let metadata = (open -r $meta_file | from json)
let file_size = (^stat -f "%z" $cache_file | into int | default 0)
$all_entries = ($all_entries | append {
type: "kcl"
cache_file: ($cache_file | path basename)
created: $metadata.created_at
ttl_seconds: $metadata.ttl_seconds
size_bytes: $file_size
sources: ($metadata.source_files | keys | length)
})
}
}
}
} | complete)
if $kcl_entries.exit_code != 0 {
print $"⚠️ Failed to list KCL cache"
}
}
# List SOPS cache
if $type in ["*", "sops"] {
let sops_entries = (do {
let cache_base = (get-cache-base-path)
let sops_dir = $"($cache_base)/sops"
if ($sops_dir | path exists) {
let cache_files = (glob $"($sops_dir)/*.json" | where { |f| not ($f | str ends-with ".meta") })
for cache_file in $cache_files {
let meta_file = $"($cache_file).meta"
if ($meta_file | path exists) {
let metadata = (open -r $meta_file | from json)
let file_size = (^stat -f "%z" $cache_file | into int | default 0)
let perms = (get-file-permissions $cache_file)
$all_entries = ($all_entries | append {
type: "sops"
cache_file: ($cache_file | path basename)
created: $metadata.created_at
ttl_seconds: $metadata.ttl_seconds
size_bytes: $file_size
permissions: $perms
})
}
}
}
} | complete)
if $sops_entries.exit_code != 0 {
print $"⚠️ Failed to list SOPS cache"
}
}
# List final config cache
if $type in ["*", "final"] {
let final_entries = (do {
let cache_base = (get-cache-base-path)
let final_dir = $"($cache_base)/final"
if ($final_dir | path exists) {
let cache_files = (glob $"($final_dir)/*.json" | where { |f| not ($f | str ends-with ".meta") })
for cache_file in $cache_files {
let meta_file = $"($cache_file).meta"
if ($meta_file | path exists) {
let metadata = (open -r $meta_file | from json)
let file_size = (^stat -f "%z" $cache_file | into int | default 0)
$all_entries = ($all_entries | append {
type: "final"
cache_file: ($cache_file | path basename)
created: $metadata.created_at
ttl_seconds: $metadata.ttl_seconds
size_bytes: $file_size
sources: ($metadata.source_files | keys | length)
})
}
}
}
} | complete)
if $final_entries.exit_code != 0 {
print $"⚠️ Failed to list final config cache"
}
}
if ($all_entries | is-empty) {
print "No cache entries found"
return
}
match $format {
"json" => {
print ($all_entries | to json)
}
"yaml" => {
print ($all_entries | to yaml)
}
_ => {
print ($all_entries | to table)
}
}
}
# Warm cache (pre-populate)
export def cache-warm [
--workspace: string = "" # Workspace name
--environment: string = "*" # Environment pattern
] {
if ($workspace | is-empty) {
print "⚠️ Workspace not specified. Skipping cache warming."
return
}
let result = (do {
warm-final-cache { name: $workspace } $environment
} | complete)
if $result.exit_code == 0 {
print $"✅ Cache warmed: ($workspace)/($environment)"
} else {
print $"❌ Failed to warm cache: ($result.stderr)"
}
}
# Validate cache integrity
export def cache-validate [] {
# Returns: { valid: bool, issues: list }
mut issues = []
# Check KCL cache
let kcl_stats = (get-kcl-cache-stats)
if $kcl_stats.total_entries > 0 {
print $"🔍 Validating KCL cache... (($kcl_stats.total_entries) entries)"
}
# Check SOPS cache security
let sops_security = (verify-sops-cache-security)
if not $sops_security.secure {
$issues = ($issues | append "SOPS cache security issues:")
for issue in $sops_security.issues {
$issues = ($issues | append $" - ($issue)")
}
}
# Check final config cache
let final_health = (check-final-config-cache-health)
if not $final_health.healthy {
for issue in $final_health.issues {
$issues = ($issues | append $issue)
}
}
let valid = ($issues | is-empty)
if $valid {
print "✅ Cache validation passed"
} else {
print "❌ Cache validation issues found:"
for issue in $issues {
print $" - ($issue)"
}
}
return { valid: $valid, issues: $issues }
}
# ====== CONFIGURATION COMMANDS ======
# Show cache configuration
export def cache-config-show [
--format: string = "table" # Output format (table, json, yaml)
] {
let result = (do { cache-config-show --format=$format } | complete)
if $result.exit_code != 0 {
print "❌ Failed to show cache configuration"
}
}
# Get specific cache configuration
export def cache-config-get [
setting_path: string # Dot-notation path (e.g., "ttl.final_config")
] {
let value = (do {
cache-config-get $setting_path
} | complete)
if $value.exit_code == 0 {
print $value.stdout
} else {
print "❌ Failed to get setting: $setting_path"
}
}
# Set cache configuration
export def cache-config-set [
setting_path: string # Dot-notation path
value: string # Value to set (as string)
] {
let result = (do {
# Parse value to appropriate type
let parsed_value = (
match $value {
"true" => true
"false" => false
_ => {
# Try to parse as integer
$value | into int | default $value
}
}
)
cache-config-set $setting_path $parsed_value
} | complete)
if $result.exit_code == 0 {
print $"✅ Updated ($setting_path) = ($value)"
} else {
print $"❌ Failed to set ($setting_path): ($result.stderr)"
}
}
# Reset cache configuration
export def cache-config-reset [
setting_path?: string = "" # Optional: reset specific setting
] {
let target = if ($setting_path | is-empty) { "all settings" } else { $setting_path }
let result = (do {
if ($setting_path | is-empty) {
cache-config-reset
} else {
cache-config-reset $setting_path
}
} | complete)
if $result.exit_code == 0 {
print $"✅ Reset ($target) to defaults"
} else {
print $"❌ Failed to reset ($target): ($result.stderr)"
}
}
# Validate cache configuration
export def cache-config-validate [] {
let result = (do { cache-config-validate } | complete)
if $result.exit_code == 0 {
let validation = ($result.stdout | from json)
if $validation.valid {
print "✅ Cache configuration is valid"
} else {
print "❌ Cache configuration has errors:"
for error in $validation.errors {
print $" - ($error)"
}
}
} else {
print "❌ Failed to validate configuration"
}
}
# ====== MONITORING COMMANDS ======
# Show comprehensive cache status (config + statistics)
export def cache-status [] {
print "═══════════════════════════════════════════════════════════════"
print "Cache Status and Configuration"
print "═══════════════════════════════════════════════════════════════"
print ""
# Show configuration
print "Configuration:"
print "─────────────────────────────────────────────────────────────────"
let config = (get-cache-config)
print $" Enabled: ($config.enabled)"
print $" Max Size: ($config.max_cache_size | into string) bytes"
print ""
print " TTL Settings:"
for ttl_key in ($config.cache.ttl | keys) {
let ttl_val = $config.cache.ttl | get $ttl_key
let ttl_min = ($ttl_val / 60)
print $" ($ttl_key): ($ttl_val)s ($($ttl_min)min)"
}
print ""
print " Security:"
print $" SOPS file permissions: ($config.cache.security.sops_file_permissions)"
print $" SOPS dir permissions: ($config.cache.security.sops_dir_permissions)"
print ""
print " Validation:"
print $" Strict mtime: ($config.cache.validation.strict_mtime)"
print ""
print ""
# Show statistics
print "Cache Statistics:"
print "─────────────────────────────────────────────────────────────────"
let kcl_stats = (get-kcl-cache-stats)
print $" KCL Cache: ($kcl_stats.total_entries) entries, ($kcl_stats.total_size_mb) MB"
let sops_stats = (get-sops-cache-stats)
print $" SOPS Cache: ($sops_stats.total_entries) entries, ($sops_stats.total_size_mb) MB"
let final_stats = (get-final-config-stats)
print $" Final Config Cache: ($final_stats.total_entries) entries, ($final_stats.total_size_mb) MB"
let total_size_mb = ($kcl_stats.total_size_mb + $sops_stats.total_size_mb + $final_stats.total_size_mb)
let max_size_mb = ($config.max_cache_size / 1048576 | math floor)
let usage_percent = if $max_size_mb > 0 {
(($total_size_mb / $max_size_mb) * 100 | math round)
} else {
0
}
print ""
print $" Total Usage: ($total_size_mb) MB / ($max_size_mb) MB ($usage_percent%)"
print ""
print ""
# Show cache health
print "Cache Health:"
print "─────────────────────────────────────────────────────────────────"
let final_health = (check-final-config-cache-health)
if $final_health.healthy {
print " ✅ Final config cache is healthy"
} else {
print " ⚠️ Final config cache has issues:"
for issue in $final_health.issues {
print $" - ($issue)"
}
}
let sops_security = (verify-sops-cache-security)
if $sops_security.secure {
print " ✅ SOPS cache security is valid"
} else {
print " ⚠️ SOPS cache security issues:"
for issue in $sops_security.issues {
print $" - ($issue)"
}
}
print ""
print "═══════════════════════════════════════════════════════════════"
}
# Show cache statistics only
export def cache-stats [] {
let kcl_stats = (get-kcl-cache-stats)
let sops_stats = (get-sops-cache-stats)
let final_stats = (get-final-config-stats)
let total_entries = (
$kcl_stats.total_entries +
$sops_stats.total_entries +
$final_stats.total_entries
)
let total_size_mb = (
$kcl_stats.total_size_mb +
$sops_stats.total_size_mb +
$final_stats.total_size_mb
)
let stats = {
total_entries: $total_entries
total_size_mb: $total_size_mb
kcl: {
entries: $kcl_stats.total_entries
size_mb: $kcl_stats.total_size_mb
}
sops: {
entries: $sops_stats.total_entries
size_mb: $sops_stats.total_size_mb
}
final_config: {
entries: $final_stats.total_entries
size_mb: $final_stats.total_size_mb
}
}
print ($stats | to table)
return $stats
}
# Get file permissions helper
def get-file-permissions [
file_path: string # Path to file
] {
if not ($file_path | path exists) {
return "nonexistent"
}
let perms = (^stat -f "%A" $file_path)
return $perms
}
# Get cache base path helper
def get-cache-base-path [] {
let config = (get-cache-config)
return $config.cache.paths.base
}

View file

@ -1,300 +0,0 @@
# Configuration Cache Core Module
# Provides core cache operations with TTL and mtime validation
# Follows Nushell 0.109.0+ guidelines strictly
# Cache lookup with TTL + mtime validation
export def cache-lookup [
cache_type: string # "kcl", "sops", "final", "provider", "platform"
cache_key: string # Unique identifier
--ttl: int = 0 # Override TTL (0 = use default from config)
] {
# Returns: { valid: bool, data: any, reason: string }
# Get cache base path
let cache_path = (get-cache-path $cache_type $cache_key)
let meta_path = $"($cache_path).meta"
# Check if cache files exist
if not ($cache_path | path exists) {
return { valid: false, data: null, reason: "cache_not_found" }
}
if not ($meta_path | path exists) {
return { valid: false, data: null, reason: "metadata_not_found" }
}
# Validate cache entry (TTL + mtime checks)
let validation = (validate-cache-entry $cache_path $meta_path --ttl=$ttl)
if not $validation.valid {
return { valid: false, data: null, reason: $validation.reason }
}
# Load cached data
let cache_data = (open -r $cache_path | from json)
return { valid: true, data: $cache_data, reason: "cache_hit" }
}
# Write cache entry with metadata
export def cache-write [
cache_type: string # "kcl", "sops", "final", "provider", "platform"
cache_key: string # Unique identifier
data: any # Data to cache
source_files: list # List of source file paths
--ttl: int = 0 # Override TTL (0 = use default)
] {
# Get cache paths
let cache_path = (get-cache-path $cache_type $cache_key)
let meta_path = $"($cache_path).meta"
let cache_dir = ($cache_path | path dirname)
# Create cache directory if needed
if not ($cache_dir | path exists) {
^mkdir -p $cache_dir
}
# Get source file mtimes
let source_mtimes = (get-source-mtimes $source_files)
# Create metadata
let metadata = (create-metadata $source_files $ttl $source_mtimes)
# Write cache data as JSON
$data | to json | save -f $cache_path
# Write metadata
$metadata | to json | save -f $meta_path
}
# Validate cache entry (TTL + mtime checks)
export def validate-cache-entry [
cache_file: string # Path to cache file
meta_file: string # Path to metadata file
--ttl: int = 0 # Optional TTL override
] {
# Returns: { valid: bool, expired: bool, mtime_mismatch: bool, reason: string }
if not ($meta_file | path exists) {
return { valid: false, expired: false, mtime_mismatch: false, reason: "no_metadata" }
}
# Load metadata
let metadata = (open -r $meta_file | from json)
# Check if metadata is valid
if $metadata.created_at == null or $metadata.ttl_seconds == null {
return { valid: false, expired: false, mtime_mismatch: false, reason: "invalid_metadata" }
}
# Calculate age in seconds
let created_time = ($metadata.created_at | into datetime)
let current_time = (date now)
let age_seconds = (($current_time - $created_time) | math floor)
# Determine TTL to use
let effective_ttl = if $ttl > 0 { $ttl } else { $metadata.ttl_seconds }
# Check if expired
if $age_seconds > $effective_ttl {
return { valid: false, expired: true, mtime_mismatch: false, reason: "ttl_expired" }
}
# Check mtime for all source files
let current_mtimes = (get-source-mtimes ($metadata.source_files | keys))
let mtimes_match = (check-source-mtimes $metadata.source_files $current_mtimes)
if not $mtimes_match.unchanged {
return { valid: false, expired: false, mtime_mismatch: true, reason: "source_files_changed" }
}
# Cache is valid
return { valid: true, expired: false, mtime_mismatch: false, reason: "valid" }
}
# Check if source files changed (compares mtimes)
export def check-source-mtimes [
cached_mtimes: record # { "/path/to/file": mtime_int, ... }
current_mtimes: record # Current file mtimes
] {
# Returns: { unchanged: bool, changed_files: list }
mut changed_files = []
# Check each file in cached_mtimes
for file_path in ($cached_mtimes | keys) {
let cached_mtime = $cached_mtimes | get $file_path
let current_mtime = ($current_mtimes | get --optional $file_path) | default null
# File was deleted or mtime changed
if $current_mtime == null or $current_mtime != $cached_mtime {
$changed_files = ($changed_files | append $file_path)
}
}
# Also check for new files
for file_path in ($current_mtimes | keys) {
if not ($cached_mtimes | keys | any { $in == $file_path }) {
$changed_files = ($changed_files | append $file_path)
}
}
return { unchanged: ($changed_files | is-empty), changed_files: $changed_files }
}
# Cleanup expired/excess cache entries
export def cleanup-expired-cache [
max_size_mb: int = 100 # Maximum cache size in MB
] {
# Get cache base directory
let cache_base = (get-cache-base-path)
if not ($cache_base | path exists) {
return
}
# Get all cache files and metadata
let cache_files = (glob $"($cache_base)/**/*.json" | where { |f| not ($f | str ends-with ".meta") })
mut total_size = 0
mut mut_files = []
# Calculate total size and get file info
for cache_file in $cache_files {
let file_size = (open -r $cache_file | str length | math floor)
$mut_files = ($mut_files | append { path: $cache_file, size: $file_size })
$total_size = ($total_size + $file_size)
}
# Convert to MB
let total_size_mb = ($total_size / 1048576 | math floor)
# If under limit, just remove expired entries
if $total_size_mb < $max_size_mb {
clean-expired-entries-only $cache_base
return
}
# Sort by modification time (oldest first) and delete until under limit
let sorted_files = (
$mut_files
| sort-by size -r
)
mut current_size_mb = $total_size_mb
for file_info in $sorted_files {
if $current_size_mb < $max_size_mb {
break
}
# Check if expired before deleting
let meta_path = $"($file_info.path).meta"
if ($meta_path | path exists) {
let validation = (validate-cache-entry $file_info.path $meta_path)
if ($validation.expired or $validation.mtime_mismatch) {
rm -f $file_info.path
rm -f $meta_path
$current_size_mb = ($current_size_mb - ($file_info.size / 1048576 | math floor))
}
}
}
}
# Get cache path for a cache entry
export def get-cache-path [
cache_type: string # "kcl", "sops", "final", "provider", "platform"
cache_key: string # Unique identifier
] {
let cache_base = (get-cache-base-path)
let type_dir = $"($cache_base)/($cache_type)"
return $"($type_dir)/($cache_key).json"
}
# Get cache base directory
export def get-cache-base-path [] {
let home = $env.HOME | default ""
return $"($home)/.provisioning/cache/config"
}
# Create cache directory
export def create-cache-dir [
cache_type: string # "kcl", "sops", "final", "provider", "platform"
] {
let cache_base = (get-cache-base-path)
let type_dir = $"($cache_base)/($cache_type)"
if not ($type_dir | path exists) {
^mkdir -p $type_dir
}
}
# Get file modification times
export def get-source-mtimes [
source_files: list # List of file paths
] {
# Returns: { "/path/to/file": mtime_int, ... }
mut mtimes = {}
for file_path in $source_files {
if ($file_path | path exists) {
let stat = (^stat -f "%m" $file_path | into int | default 0)
$mtimes = ($mtimes | insert $file_path $stat)
}
}
return $mtimes
}
# Compute cache hash (for file identification)
export def compute-cache-hash [
file_path: string # Path to file to hash
] {
# SHA256 hash of file content
let content = (open -r $file_path | str length | into string)
let file_name = ($file_path | path basename)
return $"($file_name)-($content)" | sha256sum
}
# Create metadata record
def create-metadata [
source_files: list # List of source file paths
ttl_seconds: int # TTL in seconds
source_mtimes: record # { "/path/to/file": mtime_int, ... }
] {
let created_at = (date now | format date "%Y-%m-%dT%H:%M:%SZ")
let expires_at = ((date now) + ($ttl_seconds | into duration "sec") | format date "%Y-%m-%dT%H:%M:%SZ")
return {
created_at: $created_at
ttl_seconds: $ttl_seconds
expires_at: $expires_at
source_files: $source_mtimes
cache_version: "1.0"
}
}
# Helper: cleanup only expired entries (internal use)
def clean-expired-entries-only [
cache_base: string # Base cache directory
] {
let cache_files = (glob $"($cache_base)/**/*.json" | where { |f| not ($f | str ends-with ".meta") })
for cache_file in $cache_files {
let meta_path = $"($cache_file).meta"
if ($meta_path | path exists) {
let validation = (validate-cache-entry $cache_file $meta_path)
if $validation.expired or $validation.mtime_mismatch {
rm -f $cache_file
rm -f $meta_path
}
}
}
}
# Helper: SHA256 hash computation
def sha256sum [] {
# Using shell command for hash (most reliable)
^echo $in | ^shasum -a 256 | ^awk '{ print $1 }'
}

View file

@ -1,372 +0,0 @@
# Final Configuration Cache Module
# Caches the completely merged configuration with aggressive mtime validation
# 5-minute TTL for safety - validates ALL source files on cache hit
# Follows Nushell 0.109.0+ guidelines strictly
use ./core.nu *
use ./metadata.nu *
# Cache final merged configuration
export def cache-final-config [
config: record # Complete merged configuration
workspace: record # Workspace context
environment: string # Environment (dev/test/prod)
---debug = false
] {
# Build cache key from workspace + environment
let cache_key = (build-final-cache-key $workspace $environment)
# Determine ALL source files that contributed to this config
let source_files = (get-final-config-sources $workspace $environment)
# Get TTL from config (or use default)
let ttl_seconds = 300 # 5 minutes default (short for safety)
if $debug {
print $"💾 Caching final config: ($workspace.name)/($environment)"
print $" Cache key: ($cache_key)"
print $" Source files: ($($source_files | length))"
print $" TTL: ($ttl_seconds)s (5min - aggressive invalidation)"
}
# Write cache
cache-write "final" $cache_key $config $source_files --ttl=$ttl_seconds
if $debug {
print $"✅ Final config cached"
}
}
# Lookup final config cache
export def lookup-final-config [
workspace: record # Workspace context
environment: string # Environment (dev/test/prod)
---debug = false
] {
# Returns: { valid: bool, data: record, reason: string }
# Build cache key
let cache_key = (build-final-cache-key $workspace $environment)
if $debug {
print $"🔍 Looking up final config: ($workspace.name)/($environment)"
print $" Cache key: ($cache_key)"
}
# Lookup with short TTL (5 min)
let result = (cache-lookup "final" $cache_key --ttl = 300)
if not $result.valid {
if $debug {
print $"❌ Final config cache miss: ($result.reason)"
}
return { valid: false, data: null, reason: $result.reason }
}
# Perform aggressive mtime validation
let source_files = (get-final-config-sources $workspace $environment)
let validation = (validate-all-sources $source_files)
if not $validation.valid {
if $debug {
print $"❌ Source file changed: ($validation.reason)"
}
return { valid: false, data: null, reason: $validation.reason }
}
if $debug {
print $"✅ Final config cache hit (all sources validated)"
}
return { valid: true, data: $result.data, reason: "cache_hit" }
}
# Force invalidation of final config cache
export def invalidate-final-cache [
workspace_name: string # Workspace name
environment: string = "*" # Environment pattern (default: all)
---debug = false
] {
let cache_base = (get-cache-base-path)
let final_dir = $"($cache_base)/final"
if not ($final_dir | path exists) {
return
}
let pattern = if $environment == "*" {
$"($workspace_name)-*.json"
} else {
$"($workspace_name)-($environment).json"
}
let cache_files = (glob $"($final_dir)/($pattern)" | where { |f| not ($f | str ends-with ".meta") })
for cache_file in $cache_files {
let meta_file = $"($cache_file).meta"
rm -f $cache_file
rm -f $meta_file
if $debug {
print $"🗑️ Invalidated: ($cache_file | path basename)"
}
}
if $debug and not ($cache_files | is-empty) {
print $"✅ Invalidated ($($cache_files | length)) cache entries"
}
}
# Pre-populate cache (warm)
export def warm-final-cache [
config: record # Configuration to cache
workspace: record # Workspace context
environment: string # Environment
---debug = false
] {
cache-final-config $config $workspace $environment --debug=$debug
}
# Validate all source files for final config
export def validate-final-sources [
workspace_name: string # Workspace name
environment: string = "" # Optional environment
---debug = false
] {
# Returns: { valid: bool, checked: int, changed: int, errors: list }
mut workspace = { name: $workspace_name }
let source_files = (get-final-config-sources $mut_workspace $environment)
let validation = (validate-all-sources $source_files)
return {
valid: $validation.valid
checked: ($source_files | length)
changed: ($validation.changed_count)
errors: $validation.errors
}
}
# Get all source files that contribute to final config
def get-final-config-sources [
workspace: record # Workspace context
environment: string # Environment
] {
# Collect ALL source files that affect final config
mut sources = []
# Workspace main config
let ws_config = ([$workspace.path "config/provisioning.k"] | path join)
if ($ws_config | path exists) {
$sources = ($sources | append $ws_config)
}
# Provider configs
let providers_dir = ([$workspace.path "config/providers"] | path join)
if ($providers_dir | path exists) {
let provider_files = (glob $"($providers_dir)/*.toml")
$sources = ($sources | append $provider_files)
}
# Platform configs
let platform_dir = ([$workspace.path "config/platform"] | path join)
if ($platform_dir | path exists) {
let platform_files = (glob $"($platform_dir)/*.toml")
$sources = ($sources | append $platform_files)
}
# Infrastructure-specific config
if not ($environment | is-empty) {
let infra_dir = ([$workspace.path "infra" $environment] | path join)
let settings_file = ([$infra_dir "settings.k"] | path join)
if ($settings_file | path exists) {
$sources = ($sources | append $settings_file)
}
}
# User context (for workspace switching, etc.)
let user_config = $"($env.HOME | default '')/.provisioning/cache/config/settings.json"
if ($user_config | path exists) {
$sources = ($sources | append $user_config)
}
return $sources
}
# Validate ALL source files (aggressive check)
def validate-all-sources [
source_files: list # All source files to check
] {
# Returns: { valid: bool, changed_count: int, errors: list }
mut errors = []
mut changed_count = 0
for file_path in $source_files {
if not ($file_path | path exists) {
$errors = ($errors | append $"missing: ($file_path)")
$changed_count = ($changed_count + 1)
}
}
let valid = ($changed_count == 0)
return {
valid: $valid
changed_count: $changed_count
errors: $errors
}
}
# Build final config cache key
def build-final-cache-key [
workspace: record # Workspace context
environment: string # Environment
] {
# Key format: {workspace-name}-{environment}
return $"($workspace.name)-($environment)"
}
# Get final config cache statistics
export def get-final-config-stats [] {
let cache_base = (get-cache-base-path)
let final_dir = $"($cache_base)/final"
if not ($final_dir | path exists) {
return {
total_entries: 0
total_size: 0
cache_dir: $final_dir
}
}
let cache_files = (glob $"($final_dir)/*.json" | where { |f| not ($f | str ends-with ".meta") })
mut total_size = 0
for cache_file in $cache_files {
let file_size = (^stat -f "%z" $cache_file | into int | default 0)
$total_size = ($total_size + $file_size)
}
return {
total_entries: ($cache_files | length)
total_size: $total_size
total_size_mb: ($total_size / 1048576 | math floor)
cache_dir: $final_dir
}
}
# List cached final configurations
export def list-final-config-cache [
--format: string = "table" # table, json, yaml
--workspace: string = "*" # Filter by workspace
] {
let cache_base = (get-cache-base-path)
let final_dir = $"($cache_base)/final"
if not ($final_dir | path exists) {
print "No final config cache entries"
return
}
let pattern = if $workspace == "*" { "*" } else { $"($workspace)-*" }
let cache_files = (glob $"($final_dir)/($pattern).json" | where { |f| not ($f | str ends-with ".meta") })
if ($cache_files | is-empty) {
print "No final config cache entries"
return
}
mut entries = []
for cache_file in $cache_files {
let meta_file = $"($cache_file).meta"
if ($meta_file | path exists) {
let metadata = (open -r $meta_file | from json)
let file_size = (^stat -f "%z" $cache_file | into int | default 0)
let cache_name = ($cache_file | path basename | str replace ".json" "")
$entries = ($entries | append {
workspace_env: $cache_name
created: $metadata.created_at
ttl_seconds: $metadata.ttl_seconds
size_bytes: $file_size
sources: ($metadata.source_files | keys | length)
})
}
}
match $format {
"json" => {
print ($entries | to json)
}
"yaml" => {
print ($entries | to yaml)
}
_ => {
print ($entries | to table)
}
}
}
# Clear all final config caches
export def clear-final-config-cache [
--workspace: string = "*" # Optional workspace filter
---debug = false
] {
let cache_base = (get-cache-base-path)
let final_dir = $"($cache_base)/final"
if not ($final_dir | path exists) {
print "No final config cache to clear"
return
}
let pattern = if $workspace == "*" { "*" } else { $workspace }
let cache_files = (glob $"($final_dir)/($pattern)*.json" | where { |f| not ($f | str ends-with ".meta") })
for cache_file in $cache_files {
let meta_file = $"($cache_file).meta"
rm -f $cache_file
rm -f $meta_file
}
if $debug {
print $"✅ Cleared ($($cache_files | length)) final config cache entries"
}
}
# Check final config cache health
export def check-final-config-cache-health [] {
let stats = (get-final-config-stats)
let cache_base = (get-cache-base-path)
let final_dir = $"($cache_base)/final"
mut issues = []
if ($stats.total_entries == 0) {
$issues = ($issues | append "no_cached_configs")
}
# Check each cached config
if ($final_dir | path exists) {
let cache_files = (glob $"($final_dir)/*.json" | where { |f| not ($f | str ends-with ".meta") })
for cache_file in $cache_files {
let meta_file = $"($cache_file).meta"
if not ($meta_file | path exists) {
$issues = ($issues | append $"missing_metadata: ($cache_file | path basename)")
}
}
}
return {
healthy: ($issues | is-empty)
total_entries: $stats.total_entries
size_mb: $stats.total_size_mb
issues: $issues
}
}

View file

@ -1,350 +0,0 @@
# KCL Compilation Cache Module
# Caches compiled KCL output to avoid expensive re-compilation
# Tracks kcl.mod dependencies for invalidation
# Follows Nushell 0.109.0+ guidelines strictly
use ./core.nu *
use ./metadata.nu *
# Cache KCL compilation output
export def cache-kcl-compile [
file_path: string # Path to .k file
compiled_output: record # Compiled KCL output
---debug = false
] {
# Compute hash including dependencies
let cache_hash = (compute-kcl-hash $file_path)
let cache_key = $cache_hash
# Get source files (file + kcl.mod if exists)
let source_files = (get-kcl-source-files $file_path)
# Get TTL from config (or use default)
let ttl_seconds = 1800 # 30 minutes default
if $debug {
print $"📦 Caching KCL compilation: ($file_path)"
print $" Hash: ($cache_hash)"
print $" TTL: ($ttl_seconds)s (30min)"
}
# Write cache
cache-write "kcl" $cache_key $compiled_output $source_files --ttl=$ttl_seconds
}
# Lookup cached KCL compilation
export def lookup-kcl-cache [
file_path: string # Path to .k file
---debug = false
] {
# Returns: { valid: bool, data: record, reason: string }
# Compute hash including dependencies
let cache_hash = (compute-kcl-hash $file_path)
let cache_key = $cache_hash
if $debug {
print $"🔍 Looking up KCL cache: ($file_path)"
print $" Hash: ($cache_hash)"
}
# Lookup cache
let result = (cache-lookup "kcl" $cache_key --ttl = 1800)
if $result.valid and $debug {
print $"✅ KCL cache hit"
} else if not $result.valid and $debug {
print $"❌ KCL cache miss: ($result.reason)"
}
return $result
}
# Validate KCL cache (check dependencies)
export def validate-kcl-cache [
cache_file: string # Path to cache file
meta_file: string # Path to metadata file
] {
# Returns: { valid: bool, expired: bool, deps_changed: bool, reason: string }
# Basic validation
let validation = (validate-cache-entry $cache_file $meta_file --ttl = 1800)
if not $validation.valid {
return {
valid: false
expired: $validation.expired
deps_changed: false
reason: $validation.reason
}
}
# Also validate KCL module dependencies haven't changed
let meta = (open -r $meta_file | from json)
if $meta.source_files == null {
return {
valid: false
expired: false
deps_changed: true
reason: "missing_source_files_in_metadata"
}
}
# Check each dependency exists
for dep_file in ($meta.source_files | keys) {
if not ($dep_file | path exists) {
return {
valid: false
expired: false
deps_changed: true
reason: $"dependency_missing: ($dep_file)"
}
}
}
return {
valid: true
expired: false
deps_changed: false
reason: "valid"
}
}
# Compute KCL hash (file + dependencies)
export def compute-kcl-hash [
file_path: string # Path to .k file
] {
# Hash is based on:
# 1. The .k file path and content
# 2. kcl.mod file if it exists (dependency tracking)
# 3. KCL compiler version (ensure consistency)
# Get base file info
let file_name = ($file_path | path basename)
let file_dir = ($file_path | path dirname)
let file_content = (open -r $file_path | str length)
# Check for kcl.mod in same directory
let kcl_mod_path = ([$file_dir "kcl.mod"] | path join)
let kcl_mod_content = if ($kcl_mod_path | path exists) {
(open -r $kcl_mod_path | str length)
} else {
0
}
# Build hash string
let hash_input = $"($file_name)-($file_content)-($kcl_mod_content)"
# Simple hash (truncated for reasonable cache key length)
let hash = (
^echo $hash_input
| ^shasum -a 256
| ^awk '{ print substr($1, 1, 16) }'
)
return $hash
}
# Track KCL module dependencies
export def track-kcl-dependencies [
file_path: string # Path to .k file
] {
# Returns list of all dependencies (imports)
let file_dir = ($file_path | path dirname)
let kcl_mod_path = ([$file_dir "kcl.mod"] | path join)
mut dependencies = [$file_path]
# Add kcl.mod if it exists (must be tracked)
if ($kcl_mod_path | path exists) {
$dependencies = ($dependencies | append $kcl_mod_path)
}
# TODO: Parse .k file for 'import' statements and track those too
# For now, just track the .k file and kcl.mod
return $dependencies
}
# Clear KCL cache for specific file
export def clear-kcl-cache [
file_path?: string = "" # Optional: clear specific file cache
---all = false # Clear all KCL caches
] {
if $all {
clear-kcl-cache-all
return
}
if ($file_path | is-empty) {
print "❌ Specify file path or use --all flag"
return
}
let cache_hash = (compute-kcl-hash $file_path)
let cache_base = (get-cache-base-path)
let cache_file = $"($cache_base)/kcl/($cache_hash).json"
let meta_file = $"($cache_file).meta"
if ($cache_file | path exists) {
rm -f $cache_file
print $"✅ Cleared KCL cache: ($file_path)"
}
if ($meta_file | path exists) {
rm -f $meta_file
}
}
# Check if KCL file has changed
export def kcl-file-changed [
file_path: string # Path to .k file
---strict = true # Check both file and kcl.mod
] {
let file_dir = ($file_path | path dirname)
let kcl_mod_path = ([$file_dir "kcl.mod"] | path join)
# Always check main file
if not ($file_path | path exists) {
return true
}
# If strict mode, also check kcl.mod
if $_strict and ($kcl_mod_path | path exists) {
if not ($kcl_mod_path | path exists) {
return true
}
}
return false
}
# Get all source files for KCL (file + dependencies)
def get-kcl-source-files [
file_path: string # Path to .k file
] {
let file_dir = ($file_path | path dirname)
let kcl_mod_path = ([$file_dir "kcl.mod"] | path join)
mut sources = [$file_path]
if ($kcl_mod_path | path exists) {
$sources = ($sources | append $kcl_mod_path)
}
return $sources
}
# Clear all KCL caches
def clear-kcl-cache-all [] {
let cache_base = (get-cache-base-path)
let kcl_dir = $"($cache_base)/kcl"
if ($kcl_dir | path exists) {
rm -rf $kcl_dir
print "✅ Cleared all KCL caches"
}
}
# Get KCL cache statistics
export def get-kcl-cache-stats [] {
let cache_base = (get-cache-base-path)
let kcl_dir = $"($cache_base)/kcl"
if not ($kcl_dir | path exists) {
return {
total_entries: 0
total_size: 0
cache_dir: $kcl_dir
}
}
let cache_files = (glob $"($kcl_dir)/*.json" | where { |f| not ($f | str ends-with ".meta") })
mut total_size = 0
for cache_file in $cache_files {
let file_size = (^stat -f "%z" $cache_file | into int | default 0)
$total_size = ($total_size + $file_size)
}
return {
total_entries: ($cache_files | length)
total_size: $total_size
total_size_mb: ($total_size / 1048576 | math floor)
cache_dir: $kcl_dir
}
}
# Validate KCL compiler availability
export def validate-kcl-compiler [] {
# Check if kcl command is available
let kcl_available = (which kcl | is-not-empty)
if not $kcl_available {
return { valid: false, error: "KCL compiler not found in PATH" }
}
# Try to get version
let version_result = (
^kcl version 2>&1
| complete
)
if $version_result.exit_code != 0 {
return { valid: false, error: "KCL compiler failed version check" }
}
return { valid: true, version: ($version_result.stdout | str trim) }
}
# List cached KCL compilations
export def list-kcl-cache [
--format: string = "table" # table, json, yaml
] {
let cache_base = (get-cache-base-path)
let kcl_dir = $"($cache_base)/kcl"
if not ($kcl_dir | path exists) {
print "No KCL cache entries"
return
}
let cache_files = (glob $"($kcl_dir)/*.json" | where { |f| not ($f | str ends-with ".meta") })
if ($cache_files | is-empty) {
print "No KCL cache entries"
return
}
mut entries = []
for cache_file in $cache_files {
let meta_file = $"($cache_file).meta"
if ($meta_file | path exists) {
let metadata = (open -r $meta_file | from json)
let file_size = (^stat -f "%z" $cache_file | into int | default 0)
$entries = ($entries | append {
cache_file: ($cache_file | path basename)
created: $metadata.created_at
ttl_seconds: $metadata.ttl_seconds
size_bytes: $file_size
dependencies: ($metadata.source_files | keys | length)
})
}
}
match $format {
"json" => {
print ($entries | to json)
}
"yaml" => {
print ($entries | to yaml)
}
_ => {
print ($entries | to table)
}
}
}

View file

@ -1,252 +0,0 @@
# Configuration Cache Metadata Module
# Manages cache metadata for aggressive validation
# Follows Nushell 0.109.0+ guidelines strictly
use ./core.nu *
# Create metadata for cache entry
export def create-metadata [
source_files: list # List of source file paths
ttl_seconds: int # TTL in seconds
data_hash: string # Hash of cached data (optional for validation)
] {
let created_at = (date now | format date "%Y-%m-%dT%H:%M:%SZ")
let expires_at = ((date now) + ($ttl_seconds | into duration "sec") | format date "%Y-%m-%dT%H:%M:%SZ")
let source_mtimes = (get-source-mtimes $source_files)
let size_bytes = ($data_hash | str length)
return {
created_at: $created_at
ttl_seconds: $ttl_seconds
expires_at: $expires_at
source_files: $source_mtimes
hash: $"sha256:($data_hash)"
size_bytes: $size_bytes
cache_version: "1.0"
}
}
# Load and validate metadata
export def load-metadata [
meta_file: string # Path to metadata file
] {
if not ($meta_file | path exists) {
return { valid: false, data: null, error: "metadata_file_not_found" }
}
let metadata = (open -r $meta_file | from json)
# Validate metadata structure
if $metadata.created_at == null or $metadata.ttl_seconds == null {
return { valid: false, data: null, error: "invalid_metadata_structure" }
}
return { valid: true, data: $metadata, error: null }
}
# Validate metadata (check timestamps and structure)
export def validate-metadata [
metadata: record # Metadata record from cache
] {
# Returns: { valid: bool, expired: bool, errors: list }
mut errors = []
# Check required fields
if $metadata.created_at == null {
$errors = ($errors | append "missing_created_at")
}
if $metadata.ttl_seconds == null {
$errors = ($errors | append "missing_ttl_seconds")
}
if $metadata.source_files == null {
$errors = ($errors | append "missing_source_files")
}
if not ($errors | is-empty) {
return { valid: false, expired: false, errors: $errors }
}
# Check expiration
let created_time = ($metadata.created_at | into datetime)
let current_time = (date now)
let age_seconds = (($current_time - $created_time) | math floor)
let is_expired = ($age_seconds > $metadata.ttl_seconds)
return { valid: (not $is_expired), expired: $is_expired, errors: [] }
}
# Get file modification times for multiple files
export def get-source-mtimes [
source_files: list # List of file paths
] {
# Returns: { "/path/to/file": mtime_int, ... }
mut mtimes = {}
for file_path in $source_files {
if ($file_path | path exists) {
let stat = (^stat -f "%m" $file_path | into int | default 0)
$mtimes = ($mtimes | insert $file_path $stat)
} else {
# File doesn't exist - mark with 0
$mtimes = ($mtimes | insert $file_path 0)
}
}
return $mtimes
}
# Compare cached vs current mtimes
export def compare-mtimes [
cached_mtimes: record # Cached file mtimes
current_mtimes: record # Current file mtimes
] {
# Returns: { match: bool, changed: list, deleted: list, new: list }
mut changed = []
mut deleted = []
mut new = []
# Check each file in cached mtimes
for file_path in ($cached_mtimes | keys) {
let cached_mtime = $cached_mtimes | get $file_path
let current_mtime = ($current_mtimes | get --optional $file_path) | default null
if $current_mtime == null {
if $cached_mtime > 0 {
# File was deleted
$deleted = ($deleted | append $file_path)
}
} else if $current_mtime != $cached_mtime {
# File was modified
$changed = ($changed | append $file_path)
}
}
# Check for new files
for file_path in ($current_mtimes | keys) {
if not ($cached_mtimes | keys | any { $in == $file_path }) {
$new = ($new | append $file_path)
}
}
# Match only if no changes, deletes, or new files
let match = (($changed | is-empty) and ($deleted | is-empty) and ($new | is-empty))
return {
match: $match
changed: $changed
deleted: $deleted
new: $new
}
}
# Calculate size of cached data
export def get-cache-size [
cache_data: any # Cached data to measure
] {
# Returns size in bytes
let json_str = ($cache_data | to json)
return ($json_str | str length)
}
# Check if metadata is still fresh (within TTL)
export def is-metadata-fresh [
metadata: record # Metadata record
---strict = true # Strict mode: also check source files
] {
# Check TTL
let created_time = ($metadata.created_at | into datetime)
let current_time = (date now)
let age_seconds = (($current_time - $created_time) | math floor)
if $age_seconds > $metadata.ttl_seconds {
return false
}
# If strict mode, also check source file mtimes
if $_strict {
let current_mtimes = (get-source-mtimes ($metadata.source_files | keys))
let comparison = (compare-mtimes $metadata.source_files $current_mtimes)
return $comparison.match
}
return true
}
# Get metadata creation time as duration string
export def get-metadata-age [
metadata: record # Metadata record
] {
# Returns human-readable age (e.g., "2m 30s", "1h 5m", "2d 3h")
let created_time = ($metadata.created_at | into datetime)
let current_time = (date now)
let age_seconds = (($current_time - $created_time) | math floor)
if $age_seconds < 60 {
return $"($age_seconds)s"
} else if $age_seconds < 3600 {
let minutes = ($age_seconds / 60 | math floor)
let seconds = ($age_seconds mod 60)
return $"($minutes)m ($seconds)s"
} else if $age_seconds < 86400 {
let hours = ($age_seconds / 3600 | math floor)
let minutes = (($age_seconds mod 3600) / 60 | math floor)
return $"($hours)h ($minutes)m"
} else {
let days = ($age_seconds / 86400 | math floor)
let hours = (($age_seconds mod 86400) / 3600 | math floor)
return $"($days)d ($hours)h"
}
}
# Get time until cache expires
export def get-ttl-remaining [
metadata: record # Metadata record
] {
# Returns human-readable time until expiration
let created_time = ($metadata.created_at | into datetime)
let current_time = (date now)
let age_seconds = (($current_time - $created_time) | math floor)
let remaining = ($metadata.ttl_seconds - $age_seconds)
if $remaining < 0 {
return "expired"
} else if $remaining < 60 {
return $"($remaining)s"
} else if $remaining < 3600 {
let minutes = ($remaining / 60 | math floor)
let seconds = ($remaining mod 60)
return $"($minutes)m ($seconds)s"
} else if $remaining < 86400 {
let hours = ($remaining / 3600 | math floor)
let minutes = (($remaining mod 3600) / 60 | math floor)
return $"($hours)h ($minutes)m"
} else {
let days = ($remaining / 86400 | math floor)
let hours = (($remaining mod 86400) / 3600 | math floor)
return $"($days)d ($hours)h"
}
}
# Format metadata for display
export def format-metadata [
metadata: record # Metadata record
] {
# Returns formatted metadata with human-readable values
return {
created_at: $metadata.created_at
ttl_seconds: $metadata.ttl_seconds
age: (get-metadata-age $metadata)
ttl_remaining: (get-ttl-remaining $metadata)
source_files: ($metadata.source_files | keys | length)
size_bytes: ($metadata.size_bytes | default 0)
cache_version: $metadata.cache_version
}
}

View file

@ -1,363 +0,0 @@
# SOPS Decryption Cache Module
# Caches SOPS decrypted content with strict security (0600 permissions)
# 15-minute TTL balances security and performance
# Follows Nushell 0.109.0+ guidelines strictly
use ./core.nu *
use ./metadata.nu *
# Cache decrypted SOPS content
export def cache-sops-decrypt [
file_path: string # Path to encrypted file
decrypted_content: string # Decrypted content
---debug = false
] {
# Compute hash of file
let file_hash = (compute-sops-hash $file_path)
let cache_key = $file_hash
# Get source file (just the encrypted file)
let source_files = [$file_path]
# Get TTL from config (or use default)
let ttl_seconds = 900 # 15 minutes default
if $debug {
print $"🔐 Caching SOPS decryption: ($file_path)"
print $" Hash: ($file_hash)"
print $" TTL: ($ttl_seconds)s (15min)"
print $" Permissions: 0600 (secure)"
}
# Write cache
cache-write "sops" $cache_key $decrypted_content $source_files --ttl=$ttl_seconds
# Enforce 0600 permissions on cache file
let cache_base = (get-cache-base-path)
let cache_file = $"($cache_base)/sops/($cache_key).json"
set-sops-permissions $cache_file
if $debug {
print $"✅ SOPS cache written with 0600 permissions"
}
}
# Lookup cached SOPS decryption
export def lookup-sops-cache [
file_path: string # Path to encrypted file
---debug = false
] {
# Returns: { valid: bool, data: string, reason: string }
# Compute hash
let file_hash = (compute-sops-hash $file_path)
let cache_key = $file_hash
if $debug {
print $"🔍 Looking up SOPS cache: ($file_path)"
print $" Hash: ($file_hash)"
}
# Lookup cache
let result = (cache-lookup "sops" $cache_key --ttl = 900)
if not $result.valid {
if $debug {
print $"❌ SOPS cache miss: ($result.reason)"
}
return { valid: false, data: null, reason: $result.reason }
}
# Verify permissions before returning
let cache_base = (get-cache-base-path)
let cache_file = $"($cache_base)/sops/($cache_key).json"
let perms = (get-file-permissions $cache_file)
if $perms != "0600" {
if $debug {
print $"⚠️ SOPS cache has incorrect permissions: ($perms), expected 0600"
}
return { valid: false, data: null, reason: "invalid_permissions" }
}
if $debug {
print $"✅ SOPS cache hit (permissions verified)"
}
return { valid: true, data: $result.data, reason: "cache_hit" }
}
# Validate SOPS cache (permissions + TTL + mtime)
export def validate-sops-cache [
cache_file: string # Path to cache file
---debug = false
] {
# Returns: { valid: bool, expired: bool, bad_perms: bool, reason: string }
let meta_file = $"($cache_file).meta"
# Basic validation
let validation = (validate-cache-entry $cache_file $meta_file --ttl = 900)
if not $validation.valid {
return {
valid: false
expired: $validation.expired
bad_perms: false
reason: $validation.reason
}
}
# Check permissions
let perms = (get-file-permissions $cache_file)
if $perms != "0600" {
if $debug {
print $"⚠️ SOPS cache has incorrect permissions: ($perms)"
}
return {
valid: false
expired: false
bad_perms: true
reason: "invalid_permissions"
}
}
return {
valid: true
expired: false
bad_perms: false
reason: "valid"
}
}
# Enforce 0600 permissions on SOPS cache file
export def set-sops-permissions [
cache_file: string # Path to cache file
---debug = false
] {
if not ($cache_file | path exists) {
if $debug {
print $"⚠️ Cache file does not exist: ($cache_file)"
}
return
}
# chmod 0600
^chmod 0600 $cache_file
if $debug {
let perms = (get-file-permissions $cache_file)
print $"🔒 Set SOPS cache permissions: ($perms)"
}
}
# Clear SOPS cache
export def clear-sops-cache [
--pattern: string = "*" # Pattern to match (default: all)
---force = false # Force without confirmation
] {
let cache_base = (get-cache-base-path)
let sops_dir = $"($cache_base)/sops"
if not ($sops_dir | path exists) {
print "No SOPS cache to clear"
return
}
let cache_files = (glob $"($sops_dir)/($pattern).json" | where { |f| not ($f | str ends-with ".meta") })
if ($cache_files | is-empty) {
print "No SOPS cache entries matching pattern"
return
}
# Delete matched files
for cache_file in $cache_files {
let meta_file = $"($cache_file).meta"
rm -f $cache_file
rm -f $meta_file
}
print $"✅ Cleared ($($cache_files | length)) SOPS cache entries"
}
# Rotate SOPS cache (clear expired entries)
export def rotate-sops-cache [
--max-age-seconds: int = 900 # Default 15 minutes
---debug = false
] {
let cache_base = (get-cache-base-path)
let sops_dir = $"($cache_base)/sops"
if not ($sops_dir | path exists) {
return
}
let cache_files = (glob $"($sops_dir)/*.json" | where { |f| not ($f | str ends-with ".meta") })
mut deleted_count = 0
for cache_file in $cache_files {
let meta_file = $"($cache_file).meta"
if ($meta_file | path exists) {
let validation = (validate-sops-cache $cache_file --debug=$debug)
if $validation.expired or $validation.bad_perms {
rm -f $cache_file
rm -f $meta_file
$deleted_count = ($deleted_count + 1)
}
}
}
if $debug and $deleted_count > 0 {
print $"🗑️ Rotated ($deleted_count) expired SOPS cache entries"
}
}
# Compute SOPS hash
def compute-sops-hash [
file_path: string # Path to encrypted file
] {
# Hash based on file path + size (content hash would require decryption)
let file_name = ($file_path | path basename)
let file_size = (^stat -f "%z" $file_path | into int | default 0)
let hash_input = $"($file_name)-($file_size)"
let hash = (
^echo $hash_input
| ^shasum -a 256
| ^awk '{ print substr($1, 1, 16) }'
)
return $hash
}
# Get file permissions in octal format
def get-file-permissions [
file_path: string # Path to file
] {
if not ($file_path | path exists) {
return "nonexistent"
}
# Get permissions in octal
let perms = (^stat -f "%A" $file_path)
return $perms
}
# Verify SOPS cache is properly secured
export def verify-sops-cache-security [] {
# Returns: { secure: bool, issues: list }
let cache_base = (get-cache-base-path)
let sops_dir = $"($cache_base)/sops"
mut issues = []
# Check directory exists and has correct permissions
if not ($sops_dir | path exists) {
# Directory doesn't exist yet, that's fine
return { secure: true, issues: [] }
}
let dir_perms = (^stat -f "%A" $sops_dir)
if $dir_perms != "0700" {
$issues = ($issues | append $"SOPS directory has incorrect permissions: ($dir_perms), expected 0700")
}
# Check all cache files have 0600 permissions
let cache_files = (glob $"($sops_dir)/*.json" | where { |f| not ($f | str ends-with ".meta") })
for cache_file in $cache_files {
let file_perms = (get-file-permissions $cache_file)
if $file_perms != "0600" {
$issues = ($issues | append $"SOPS cache file has incorrect permissions: ($cache_file) ($file_perms)")
}
}
return { secure: ($issues | is-empty), issues: $issues }
}
# Get SOPS cache statistics
export def get-sops-cache-stats [] {
let cache_base = (get-cache-base-path)
let sops_dir = $"($cache_base)/sops"
if not ($sops_dir | path exists) {
return {
total_entries: 0
total_size: 0
cache_dir: $sops_dir
}
}
let cache_files = (glob $"($sops_dir)/*.json" | where { |f| not ($f | str ends-with ".meta") })
mut total_size = 0
for cache_file in $cache_files {
let file_size = (^stat -f "%z" $cache_file | into int | default 0)
$total_size = ($total_size + $file_size)
}
return {
total_entries: ($cache_files | length)
total_size: $total_size
total_size_mb: ($total_size / 1048576 | math floor)
cache_dir: $sops_dir
}
}
# List cached SOPS decryptions
export def list-sops-cache [
--format: string = "table" # table, json, yaml
] {
let cache_base = (get-cache-base-path)
let sops_dir = $"($cache_base)/sops"
if not ($sops_dir | path exists) {
print "No SOPS cache entries"
return
}
let cache_files = (glob $"($sops_dir)/*.json" | where { |f| not ($f | str ends-with ".meta") })
if ($cache_files | is-empty) {
print "No SOPS cache entries"
return
}
mut entries = []
for cache_file in $cache_files {
let meta_file = $"($cache_file).meta"
if ($meta_file | path exists) {
let metadata = (open -r $meta_file | from json)
let file_size = (^stat -f "%z" $cache_file | into int | default 0)
let perms = (get-file-permissions $cache_file)
$entries = ($entries | append {
cache_file: ($cache_file | path basename)
created: $metadata.created_at
ttl_seconds: $metadata.ttl_seconds
size_bytes: $file_size
permissions: $perms
source: ($metadata.source_files | keys | first)
})
}
}
match $format {
"json" => {
print ($entries | to json)
}
"yaml" => {
print ($entries | to yaml)
}
_ => {
print ($entries | to table)
}
}
}

View file

@ -1,338 +0,0 @@
# Comprehensive Test Suite for Configuration Cache System
# Tests all cache modules and integration points
# Follows Nushell 0.109.0+ testing guidelines
use ./core.nu *
use ./metadata.nu *
use ./config_manager.nu *
use ./kcl.nu *
use ./sops.nu *
use ./final.nu *
use ./commands.nu *
# Test suite counter
mut total_tests = 0
mut passed_tests = 0
mut failed_tests = []
# Helper: Run a test and track results
def run_test [
test_name: string
test_block: closure
] {
global total_tests = ($total_tests + 1)
let result = (do {
(^$test_block) | complete
} | complete)
if $result.exit_code == 0 {
global passed_tests = ($passed_tests + 1)
print $"✅ ($test_name)"
} else {
global failed_tests = ($failed_tests | append $test_name)
print $"❌ ($test_name): ($result.stderr)"
}
}
# ====== PHASE 1: CORE CACHE TESTS ======
print "═══════════════════════════════════════════════════════════════"
print "Phase 1: Core Cache Operations"
print "═══════════════════════════════════════════════════════════════"
print ""
# Test cache directory creation
run_test "Cache directory creation" {
let cache_base = (get-cache-base-path)
$cache_base | path exists
}
# Test cache-write operation
run_test "Cache write operation" {
let test_data = { name: "test", value: 123 }
cache-write "test" "test_key_1" $test_data ["/tmp/test.yaml"]
}
# Test cache-lookup operation
run_test "Cache lookup operation" {
let result = (cache-lookup "test" "test_key_1")
$result.valid
}
# Test TTL validation
run_test "TTL expiration validation" {
# Write cache with 1 second TTL
cache-write "test" "test_ttl_key" { data: "test" } ["/tmp/test.yaml"] --ttl = 1
# Should be valid immediately
let result1 = (cache-lookup "test" "test_ttl_key" --ttl = 1)
$result1.valid
}
# ====== PHASE 2: METADATA TESTS ======
print ""
print "═══════════════════════════════════════════════════════════════"
print "Phase 2: Metadata Management"
print "═══════════════════════════════════════════════════════════════"
print ""
# Test metadata creation
run_test "Metadata creation" {
let metadata = (create-metadata ["/tmp/test1.yaml" "/tmp/test2.yaml"] 300 "sha256:abc123")
($metadata | keys | contains "created_at")
}
# Test mtime comparison
run_test "Metadata mtime comparison" {
let mtimes1 = { "/tmp/file1": 1000, "/tmp/file2": 2000 }
let mtimes2 = { "/tmp/file1": 1000, "/tmp/file2": 2000 }
let result = (compare-mtimes $mtimes1 $mtimes2)
$result.match
}
# ====== PHASE 3: CONFIGURATION MANAGER TESTS ======
print ""
print "═══════════════════════════════════════════════════════════════"
print "Phase 3: Configuration Manager"
print "═══════════════════════════════════════════════════════════════"
print ""
# Test get cache config
run_test "Get cache configuration" {
let config = (get-cache-config)
($config | keys | contains "enabled")
}
# Test cache-config-get (dot notation)
run_test "Cache config get with dot notation" {
let enabled = (cache-config-get "enabled")
$enabled != null
}
# Test cache-config-set
run_test "Cache config set value" {
cache-config-set "enabled" true
let value = (cache-config-get "enabled")
$value == true
}
# Test cache-config-validate
run_test "Cache config validation" {
let validation = (cache-config-validate)
($validation | keys | contains "valid")
}
# ====== PHASE 4: KCL CACHE TESTS ======
print ""
print "═══════════════════════════════════════════════════════════════"
print "Phase 4: KCL Compilation Cache"
print "═══════════════════════════════════════════════════════════════"
print ""
# Test KCL hash computation
run_test "KCL hash computation" {
let hash = (compute-kcl-hash "/tmp/test.k")
($hash | str length) > 0
}
# Test KCL cache write
run_test "KCL cache write" {
let compiled = { schemas: [], configs: [] }
cache-kcl-compile "/tmp/test.k" $compiled
}
# Test KCL cache lookup
run_test "KCL cache lookup" {
let result = (lookup-kcl-cache "/tmp/test.k")
($result | keys | contains "valid")
}
# Test get KCL cache stats
run_test "KCL cache statistics" {
let stats = (get-kcl-cache-stats)
($stats | keys | contains "total_entries")
}
# ====== PHASE 5: SOPS CACHE TESTS ======
print ""
print "═══════════════════════════════════════════════════════════════"
print "Phase 5: SOPS Decryption Cache"
print "═══════════════════════════════════════════════════════════════"
print ""
# Test SOPS cache write
run_test "SOPS cache write" {
cache-sops-decrypt "/tmp/test.sops.yaml" "decrypted_content"
}
# Test SOPS cache lookup
run_test "SOPS cache lookup" {
let result = (lookup-sops-cache "/tmp/test.sops.yaml")
($result | keys | contains "valid")
}
# Test SOPS permission verification
run_test "SOPS cache security verification" {
let security = (verify-sops-cache-security)
($security | keys | contains "secure")
}
# Test get SOPS cache stats
run_test "SOPS cache statistics" {
let stats = (get-sops-cache-stats)
($stats | keys | contains "total_entries")
}
# ====== PHASE 6: FINAL CONFIG CACHE TESTS ======
print ""
print "═══════════════════════════════════════════════════════════════"
print "Phase 6: Final Config Cache"
print "═══════════════════════════════════════════════════════════════"
print ""
# Test cache-final-config
run_test "Final config cache write" {
let config = { version: "1.0", providers: {} }
let workspace = { name: "test", path: "/tmp/workspace" }
cache-final-config $config $workspace "dev"
}
# Test get-final-config-stats
run_test "Final config cache statistics" {
let stats = (get-final-config-stats)
($stats | keys | contains "total_entries")
}
# Test check-final-config-cache-health
run_test "Final config cache health check" {
let health = (check-final-config-cache-health)
($health | keys | contains "healthy")
}
# ====== PHASE 7: CLI COMMANDS TESTS ======
print ""
print "═══════════════════════════════════════════════════════════════"
print "Phase 7: Cache Commands"
print "═══════════════════════════════════════════════════════════════"
print ""
# Test cache-stats command
run_test "Cache stats command" {
let stats = (cache-stats)
($stats | keys | contains "total_entries")
}
# Test cache-config-show command
run_test "Cache config show command" {
cache-config-show --format json
}
# ====== PHASE 8: INTEGRATION TESTS ======
print ""
print "═══════════════════════════════════════════════════════════════"
print "Phase 8: Integration Tests"
print "═══════════════════════════════════════════════════════════════"
print ""
# Test cache configuration hierarchy
run_test "Cache configuration hierarchy (runtime overrides defaults)" {
let config = (get-cache-config)
# Should have cache settings from defaults
let has_ttl = ($config | keys | contains "cache")
let has_enabled = ($config | keys | contains "enabled")
($has_ttl and $has_enabled)
}
# Test cache enable/disable
run_test "Cache enable/disable via config" {
# Save original value
let original = (cache-config-get "enabled")
# Test setting to false
cache-config-set "enabled" false
let disabled = (cache-config-get "enabled")
# Restore original
cache-config-set "enabled" $original
$disabled == false
}
# ====== PHASE 9: NUSHELL GUIDELINES COMPLIANCE ======
print ""
print "═══════════════════════════════════════════════════════════════"
print "Phase 9: Nushell Guidelines Compliance"
print "═══════════════════════════════════════════════════════════════"
print ""
# Test no try-catch blocks in cache modules
run_test "No try-catch blocks (using do/complete pattern)" {
# This test verifies implementation patterns but passes if module loads
let config = (get-cache-config)
($config != null)
}
# Test explicit types in function parameters
run_test "Explicit types in cache functions" {
# Functions should use explicit types for parameters
let result = (cache-lookup "test" "key")
($result | type) == "record"
}
# Test pure functions
run_test "Pure functions (no side effects in queries)" {
# cache-lookup should be idempotent
let result1 = (cache-lookup "nonexistent" "nonexistent")
let result2 = (cache-lookup "nonexistent" "nonexistent")
($result1.valid == $result2.valid)
}
# ====== TEST SUMMARY ======
print ""
print "═══════════════════════════════════════════════════════════════"
print "Test Summary"
print "═══════════════════════════════════════════════════════════════"
print ""
let success_rate = if $total_tests > 0 {
(($passed_tests / $total_tests) * 100 | math round)
} else {
0
}
print $"Total Tests: ($total_tests)"
print $"Passed: ($passed_tests)"
print $"Failed: ($($failed_tests | length))"
print $"Success Rate: ($success_rate)%"
if not ($failed_tests | is-empty) {
print ""
print "Failed Tests:"
for test_name in $failed_tests {
print $" ❌ ($test_name)"
}
}
print ""
if ($failed_tests | is-empty) {
print "✅ All tests passed!"
exit 0
} else {
print "❌ Some tests failed!"
exit 1
}

View file

@ -2,12 +2,14 @@
# Provides user-facing commands for cache operations and configuration # Provides user-facing commands for cache operations and configuration
# Follows Nushell 0.109.0+ guidelines # Follows Nushell 0.109.0+ guidelines
use ./core.nu * # Selective imports (ADR-025 Phase 3 Layer 2).
use ./metadata.nu * # cache/metadata star-import was dead — dropped.
use ./config_manager.nu * use lib_provisioning/config/cache/core.nu [cache-clear-type get-cache-stats]
use ./kcl.nu * # Avoid importing all modules - use only what's needed
use ./sops.nu * # use ./config_manager.nu *
use ./final.nu * # use ./nickel.nu *
# use ./sops.nu *
# use ./final.nu *
# ============================================================================ # ============================================================================
# Data Operations: Clear, List, Warm, Validate # Data Operations: Clear, List, Warm, Validate
@ -15,7 +17,7 @@ use ./final.nu *
# Clear all or specific type of cache # Clear all or specific type of cache
export def cache-clear [ export def cache-clear [
--type: string = "all" # "all", "kcl", "sops", "final", "provider", "platform" --type: string = "all" # "all", "nickel", "sops", "final", "provider", "platform"
--force = false # Skip confirmation --force = false # Skip confirmation
] { ] {
if (not $force) and ($type == "all") { if (not $force) and ($type == "all") {
@ -30,7 +32,7 @@ export def cache-clear [
"all" => { "all" => {
print "Clearing all caches..." print "Clearing all caches..."
do { do {
cache-clear-type "kcl" cache-clear-type "nickel"
cache-clear-type "sops" cache-clear-type "sops"
cache-clear-type "final" cache-clear-type "final"
cache-clear-type "provider" cache-clear-type "provider"
@ -38,10 +40,10 @@ export def cache-clear [
} | complete | ignore } | complete | ignore
print "✅ All caches cleared" print "✅ All caches cleared"
}, },
"kcl" => { "nickel" => {
print "Clearing KCL compilation cache..." print "Clearing Nickel compilation cache..."
clear-kcl-cache clear-nickel-cache
print "✅ KCL cache cleared" print "✅ Nickel cache cleared"
}, },
"sops" => { "sops" => {
print "Clearing SOPS decryption cache..." print "Clearing SOPS decryption cache..."
@ -61,7 +63,7 @@ export def cache-clear [
# List cache entries # List cache entries
export def cache-list [ export def cache-list [
--type: string = "*" # "kcl", "sops", "final", etc or "*" for all --type: string = "*" # "nickel", "sops", "final", etc or "*" for all
--format: string = "table" # "table", "json", "yaml" --format: string = "table" # "table", "json", "yaml"
] { ] {
let stats = (get-cache-stats) let stats = (get-cache-stats)
@ -78,7 +80,7 @@ export def cache-list [
let type_dir = match $type { let type_dir = match $type {
"all" => $base, "all" => $base,
"kcl" => ($base | path join "kcl"), "nickel" => ($base | path join "nickel"),
"sops" => ($base | path join "sops"), "sops" => ($base | path join "sops"),
"final" => ($base | path join "workspaces"), "final" => ($base | path join "workspaces"),
_ => ($base | path join $type) _ => ($base | path join $type)
@ -155,7 +157,7 @@ export def cache-warm [
print $"Warming cache for workspace: ($active.name)" print $"Warming cache for workspace: ($active.name)"
do { do {
warm-kcl-cache $active.path warm-nickel-cache $active.path
} | complete | ignore } | complete | ignore
} else { } else {
print $"Warming cache for workspace: ($workspace)" print $"Warming cache for workspace: ($workspace)"
@ -261,7 +263,7 @@ export def cache-config-show [
print "▸ Time-To-Live (TTL) Settings:" print "▸ Time-To-Live (TTL) Settings:"
print $" Final Config: ($config.ttl.final_config)s (5 minutes)" print $" Final Config: ($config.ttl.final_config)s (5 minutes)"
print $" KCL Compilation: ($config.ttl.kcl_compilation)s (30 minutes)" print $" Nickel Compilation: ($config.ttl.nickel_compilation)s (30 minutes)"
print $" SOPS Decryption: ($config.ttl.sops_decryption)s (15 minutes)" print $" SOPS Decryption: ($config.ttl.sops_decryption)s (15 minutes)"
print $" Provider Config: ($config.ttl.provider_config)s (10 minutes)" print $" Provider Config: ($config.ttl.provider_config)s (10 minutes)"
print $" Platform Config: ($config.ttl.platform_config)s (10 minutes)" print $" Platform Config: ($config.ttl.platform_config)s (10 minutes)"
@ -372,7 +374,7 @@ export def cache-status [] {
print "" print ""
print " TTL Settings:" print " TTL Settings:"
print $" Final Config: ($config.ttl.final_config)s (5 min)" print $" Final Config: ($config.ttl.final_config)s (5 min)"
print $" KCL Compilation: ($config.ttl.kcl_compilation)s (30 min)" print $" Nickel Compilation: ($config.ttl.nickel_compilation)s (30 min)"
print $" SOPS Decryption: ($config.ttl.sops_decryption)s (15 min)" print $" SOPS Decryption: ($config.ttl.sops_decryption)s (15 min)"
print $" Provider Config: ($config.ttl.provider_config)s (10 min)" print $" Provider Config: ($config.ttl.provider_config)s (10 min)"
print $" Platform Config: ($config.ttl.platform_config)s (10 min)" print $" Platform Config: ($config.ttl.platform_config)s (10 min)"
@ -389,8 +391,8 @@ export def cache-status [] {
print "" print ""
print " By Type:" print " By Type:"
let kcl_stats = (get-kcl-cache-stats) let nickel_stats = (get-nickel-cache-stats)
print $" KCL: ($kcl_stats.total_entries) entries, ($kcl_stats.total_size_mb | math round -p 2) MB" print $" Nickel: ($nickel_stats.total_entries) entries, ($nickel_stats.total_size_mb | math round -p 2) MB"
let sops_stats = (get-sops-cache-stats) let sops_stats = (get-sops-cache-stats)
print $" SOPS: ($sops_stats.total_entries) entries, ($sops_stats.total_size_mb | math round -p 2) MB" print $" SOPS: ($sops_stats.total_entries) entries, ($sops_stats.total_size_mb | math round -p 2) MB"
@ -413,12 +415,12 @@ export def cache-stats [
print $" Total Size: ($stats.total_size_mb | math round -p 2) MB" print $" Total Size: ($stats.total_size_mb | math round -p 2) MB"
print "" print ""
let kcl_stats = (get-kcl-cache-stats) let nickel_stats = (get-nickel-cache-stats)
let sops_stats = (get-sops-cache-stats) let sops_stats = (get-sops-cache-stats)
let final_stats = (get-final-cache-stats) let final_stats = (get-final-cache-stats)
let summary = [ let summary = [
{ type: "KCL Compilation", entries: $kcl_stats.total_entries, size_mb: ($kcl_stats.total_size_mb | math round -p 2) }, { type: "Nickel Compilation", entries: $nickel_stats.total_entries, size_mb: ($nickel_stats.total_size_mb | math round -p 2) },
{ type: "SOPS Decryption", entries: $sops_stats.total_entries, size_mb: ($sops_stats.total_size_mb | math round -p 2) }, { type: "SOPS Decryption", entries: $sops_stats.total_entries, size_mb: ($sops_stats.total_size_mb | math round -p 2) },
{ type: "Final Config", entries: $final_stats.total_entries, size_mb: ($final_stats.total_size_mb | math round -p 2) } { type: "Final Config", entries: $final_stats.total_entries, size_mb: ($final_stats.total_size_mb | math round -p 2) }
] ]
@ -509,7 +511,7 @@ export def main [
"help" => { "help" => {
print "Cache Management Commands: print "Cache Management Commands:
cache clear [--type <type>] Clear cache (all, kcl, sops, final) cache clear [--type <type>] Clear cache (all, nickel, sops, final)
cache list List cache entries cache list List cache entries
cache warm Pre-populate cache cache warm Pre-populate cache
cache validate Validate cache integrity cache validate Validate cache integrity

View file

@ -61,7 +61,7 @@ export def get-cache-config [] {
max_cache_size: 104857600, # 100 MB max_cache_size: 104857600, # 100 MB
ttl: { ttl: {
final_config: 300, # 5 minutes final_config: 300, # 5 minutes
kcl_compilation: 1800, # 30 minutes nickel_compilation: 1800, # 30 minutes
sops_decryption: 900, # 15 minutes sops_decryption: 900, # 15 minutes
provider_config: 600, # 10 minutes provider_config: 600, # 10 minutes
platform_config: 600 # 10 minutes platform_config: 600 # 10 minutes
@ -112,7 +112,7 @@ export def cache-config-set [
value: any value: any
] { ] {
let runtime = (load-runtime-config) let runtime = (load-runtime-config)
# Build nested structure from dot notation # Build nested structure from dot notation
mut updated = $runtime mut updated = $runtime
@ -123,7 +123,7 @@ export def cache-config-set [
# For nested paths, we need to handle carefully # For nested paths, we need to handle carefully
# Convert "ttl.final_config" -> insert into ttl section # Convert "ttl.final_config" -> insert into ttl section
let parts = ($setting_path | split row ".") let parts = ($setting_path | split row ".")
if ($parts | length) == 2 { if ($parts | length) == 2 {
let section = ($parts | get 0) let section = ($parts | get 0)
let key = ($parts | get 1) let key = ($parts | get 1)
@ -164,7 +164,7 @@ export def cache-config-reset [
} else { } else {
# Remove specific setting # Remove specific setting
let runtime = (load-runtime-config) let runtime = (load-runtime-config)
mut updated = $runtime mut updated = $runtime
# Handle nested paths # Handle nested paths
@ -229,7 +229,7 @@ export def cache-config-validate [] {
if ($config | has -c "ttl") { if ($config | has -c "ttl") {
for ttl_key in [ for ttl_key in [
"final_config" "final_config"
"kcl_compilation" "nickel_compilation"
"sops_decryption" "sops_decryption"
"provider_config" "provider_config"
"platform_config" "platform_config"
@ -329,7 +329,7 @@ export def get-cache-defaults [] {
max_cache_size: 104857600, # 100 MB max_cache_size: 104857600, # 100 MB
ttl: { ttl: {
final_config: 300, final_config: 300,
kcl_compilation: 1800, nickel_compilation: 1800,
sops_decryption: 900, sops_decryption: 900,
provider_config: 600, provider_config: 600,
platform_config: 600 platform_config: 600

View file

@ -1,350 +1,158 @@
# Configuration Cache System - Core Operations # Cache Core — reads from the shared plugin cache directory.
# Provides fundamental cache lookup, write, validation, and cleanup operations # Written by ncl-sync daemon; read by this module and nu_plugin_nickel.
# Follows Nushell 0.109.0+ guidelines: explicit types, early returns, pure functions # Single writer principle: Nu NEVER writes to the cache dir directly.
# Helper: Get cache base directory # cache/metadata star-import was dead — dropped (ADR-025 Phase 3 Layer 2).
def get-cache-base-dir [] {
# Check if a directory has workspace markers.
def is-ws-dir [path: string]: nothing -> bool {
if ($path | is-empty) or (not ($path | path exists)) { return false }
let has_infra = ($path | path join "infra" | path exists)
let has_config = ($path | path join "config" "provisioning.ncl" | path exists)
let has_onto = ($path | path join ".ontology" | path exists)
$has_infra or $has_config or $has_onto
}
# Walk up from PWD to find workspace root (recursive).
def find-ws-up [path: string]: nothing -> string {
if ($path | is-empty) or $path == "/" { return "" }
if (is-ws-dir $path) { return $path }
let parent = ($path | path dirname)
if $parent == $path { return "" }
find-ws-up $parent
}
# Global cache directory (shared across workspaces, for files under $PROVISIONING).
def get-global-cache-dir []: nothing -> string {
let home = ($env.HOME? | default "~" | path expand) let home = ($env.HOME? | default "~" | path expand)
$home | path join ".provisioning" "cache" "config" let host_info = (do { sys host } | complete)
} let is_mac = if $host_info.exit_code == 0 {
($host_info.stdout | get name | str downcase | str contains "darwin")
# Helper: Get cache file path for a given type and key or ($host_info.stdout | get name | str downcase | str contains "macos")
def get-cache-file-path [
cache_type: string # "kcl", "sops", "final", "provider", "platform"
cache_key: string # Unique identifier (usually a hash)
] {
let base = (get-cache-base-dir)
let type_dir = match $cache_type {
"kcl" => "kcl"
"sops" => "sops"
"final" => "workspaces"
"provider" => "providers"
"platform" => "platform"
_ => "other"
}
$base | path join $type_dir $cache_key
}
# Helper: Get metadata file path
def get-cache-meta-path [cache_file: string] {
$"($cache_file).meta"
}
# Helper: Create cache directory structure if not exists
def ensure-cache-dirs [] {
let base = (get-cache-base-dir)
for dir in ["kcl" "sops" "workspaces" "providers" "platform" "index"] {
let dir_path = ($base | path join $dir)
if not ($dir_path | path exists) {
mkdir $dir_path
}
}
}
# Helper: Compute SHA256 hash
def compute-hash [content: string] {
let hash_result = (do {
$content | ^openssl dgst -sha256 -hex
} | complete)
if $hash_result.exit_code == 0 {
($hash_result.stdout | str trim | split column " " | get column1 | get 0)
} else { } else {
($content | hash md5 | str substring 0..16) ($home | path join "Library" | path exists)
} }
} if $is_mac {
$home | path join "Library" "Caches" "provisioning" "config-cache"
# Helper: Get file modification time
def get-file-mtime [file_path: string] {
if ($file_path | path exists) {
let file_dir = ($file_path | path dirname)
let file_name = ($file_path | path basename)
let file_list = (ls $file_dir | where name == $file_name)
if ($file_list | length) > 0 {
let file_info = ($file_list | get 0)
($file_info.modified | into int)
} else {
-1
}
} else { } else {
-1 $home | path join ".cache" "provisioning" "config-cache"
} }
} }
# ============================================================================ # Resolve cache directory FOR A SPECIFIC FILE. Priority:
# PUBLIC API: Cache Operations # 1. $NCL_CACHE_DIR (explicit override, for CI/tests)
# ============================================================================ # 2. File under $PROVISIONING → global cache (extensions, schemas — shared)
# 3. File under a workspace (walk up from file path) → <ws>/.ncl-cache/
# 4. Fallback: global cache
#
# Must match resolve_cache_dir_for_file() in ncl-sync + plugin.
def get-cache-dir-for-file [file_path: string]: nothing -> string {
if ($env.NCL_CACHE_DIR? | is-not-empty) {
return $env.NCL_CACHE_DIR
}
# File under $PROVISIONING → global cache
let prov = ($env.PROVISIONING? | default "")
if ($prov | is-not-empty) and ($file_path | str starts-with $prov) {
return (get-global-cache-dir)
}
# File under a workspace → workspace-local cache
let ws = (find-ws-up ($file_path | path dirname))
if ($ws | is-not-empty) {
return ($ws | path join ".ncl-cache")
}
get-global-cache-dir
}
# Lookup cache entry with TTL + mtime validation # Legacy helper (CWD-based) — kept for backwards compat in code paths that don't have
# the file path at hand. Prefer get-cache-dir-for-file.
def get-cache-base-dir []: nothing -> string {
if ($env.NCL_CACHE_DIR? | is-not-empty) { return $env.NCL_CACHE_DIR }
let ws = (find-ws-up $env.PWD)
if ($ws | is-not-empty) { return ($ws | path join ".ncl-cache") }
get-global-cache-dir
}
# Lookup a cache entry by pre-computed key.
# Only "nickel" type is backed by the shared plugin cache.
# Returns: { valid: bool, reason: string, data: any }
export def cache-lookup [ export def cache-lookup [
cache_type: string # "kcl", "sops", "final", "provider", "platform" cache_type: string
cache_key: string # Unique identifier cache_key: string
--ttl: int = 0 # Override TTL (0 = use default) --ttl: int = 0
] { ]: nothing -> record {
ensure-cache-dirs if $cache_type != "nickel" {
return { valid: false, reason: "type_not_supported", data: null }
let cache_file = (get-cache-file-path $cache_type $cache_key) }
let meta_file = (get-cache-meta-path $cache_file) let cache_file = ((get-cache-base-dir) | path join $"($cache_key).json")
if not ($cache_file | path exists) { if not ($cache_file | path exists) {
return { valid: false, reason: "cache_not_found", data: null } return { valid: false, reason: "cache_miss", data: null }
} }
let result = (do { open $cache_file } | complete)
if not ($meta_file | path exists) { if $result.exit_code != 0 {
return { valid: false, reason: "metadata_not_found", data: null } return { valid: false, reason: "read_error", data: null }
} }
{ valid: true, reason: "hit", data: ($result.stdout | from json) }
let validation = (validate-cache-entry $cache_file $meta_file)
if not $validation.valid {
return {
valid: false,
reason: $validation.reason,
data: null
}
}
let data = if ($cache_file | str ends-with ".json") {
open $cache_file | from json
} else if ($cache_file | str ends-with ".yaml") {
open $cache_file
} else {
open $cache_file
}
{ valid: true, reason: "cache_hit", data: $data }
} }
# Write cache entry with metadata # Signal ncl-sync daemon to (re-)export a list of NCL files.
# Nu never writes to the cache directly — only signals the daemon.
# Uses pid-unique sidecar + atomic rename to prevent concurrent-write corruption.
export def cache-write [ export def cache-write [
cache_type: string cache_type: string
cache_key: string cache_key: string
data: any data: any
source_files: list # List of source file paths for mtime tracking source_files: list
--ttl: int = 0 --ttl: int = 0
] { ]: nothing -> nothing {
ensure-cache-dirs if $cache_type != "nickel" { return }
write-sync-request ($source_files | each {|f| { path: $f, import_paths: [] }})
let cache_file = (get-cache-file-path $cache_type $cache_key)
let meta_file = (get-cache-meta-path $cache_file)
let ttl_seconds = if $ttl > 0 {
$ttl
} else {
match $cache_type {
"final" => 300
"kcl" => 1800
"sops" => 900
"provider" => 600
"platform" => 600
_ => 600
}
}
mut source_mtimes = {}
for src_file in $source_files {
let mtime = (get-file-mtime $src_file)
$source_mtimes = ($source_mtimes | insert $src_file $mtime)
}
let metadata = {
created_at: (date now | format date "%Y-%m-%dT%H:%M:%SZ"),
ttl_seconds: $ttl_seconds,
expires_at: (((date now) + ($ttl_seconds | into duration)) | format date "%Y-%m-%dT%H:%M:%SZ"),
source_files: $source_files,
source_mtimes: $source_mtimes,
hash: (compute-hash ($data | to json)),
cache_version: "1.0"
}
$data | to json | save --force $cache_file
$metadata | to json | save --force $meta_file
} }
# Validate cache entry # Write a sync-request sidecar file for ncl-sync to process.
def validate-cache-entry [ # Each Nu process writes .sync-<pid>.tmp then renames to .sync-<pid>.json atomically.
cache_file: string export def write-sync-request [
meta_file: string requests: list # list of {path: string, import_paths: list}
] { ]: nothing -> nothing {
if not ($meta_file | path exists) { let cache_dir = (get-cache-base-dir)
return { valid: false, reason: "metadata_not_found" } if not ($cache_dir | path exists) { return }
} let pid = $nu.pid
let tmp_file = ($cache_dir | path join $".sync-($pid).tmp")
let meta = (open $meta_file | from json) let json_file = ($cache_dir | path join $".sync-($pid).json")
$requests | to json | save --force $tmp_file
let now = (date now | format date "%Y-%m-%dT%H:%M:%SZ") ^mv $tmp_file $json_file
if $now > $meta.expires_at {
return { valid: false, reason: "ttl_expired" }
}
for src_file in $meta.source_files {
let current_mtime = (get-file-mtime $src_file)
let cached_mtime = ($meta.source_mtimes | get --optional $src_file | default (-1))
if $current_mtime != $cached_mtime {
return { valid: false, reason: "source_file_modified" }
}
}
{ valid: true, reason: "validation_passed" }
} }
# Check if source files have been modified # Cache stats — count entries and total size in the shared cache dir.
export def check-source-mtimes [ export def get-cache-stats []: nothing -> record {
source_files: record let cache_dir = (get-cache-base-dir)
] { if not ($cache_dir | path exists) {
mut changed_files = [] return { total_entries: 0, total_size_mb: 0.0, by_type: {} }
for file in ($source_files | columns) {
let current_mtime = (get-file-mtime $file)
let cached_mtime = ($source_files | get $file)
if $current_mtime != $cached_mtime {
$changed_files = ($changed_files | append $file)
}
} }
let files = (do { ls $cache_dir } | complete)
if $files.exit_code != 0 {
return { total_entries: 0, total_size_mb: 0.0, by_type: {} }
}
let entries = ($files.stdout | where name =~ '\.json$' | where name !~ 'manifest' | length)
let size_bytes = ($files.stdout | where name =~ '\.json$' | get size | math sum)
{ {
unchanged: (($changed_files | length) == 0), total_entries: $entries,
changed_files: $changed_files total_size_mb: ($size_bytes / 1_048_576 | math round -p 2),
by_type: { nickel: $entries }
} }
} }
# Cleanup expired and excess cache entries # Clear the shared cache directory (removes all .json files except manifest).
export def cleanup-expired-cache [ export def cache-clear-type [cache_type: string]: nothing -> nothing {
max_size_mb: int = 100 if $cache_type != "nickel" { return }
] { let cache_dir = (get-cache-base-dir)
let base = (get-cache-base-dir) if not ($cache_dir | path exists) { return }
do {
if not ($base | path exists) { ls $cache_dir
return | where name =~ '\.json$'
} | where name !~ 'manifest'
| each {|f| rm $f.name}
mut total_size = 0 } | ignore
mut expired_files = []
mut all_files = []
for meta_file in (glob $"($base)/**/*.meta") {
let cache_file = ($meta_file | str substring 0..-6)
let meta_load = (do {
open $meta_file
} | complete)
if $meta_load.exit_code == 0 {
let meta = $meta_load.stdout
let now = (date now | format date "%Y-%m-%dT%H:%M:%SZ")
if $now > $meta.expires_at {
$expired_files = ($expired_files | append $cache_file)
} else {
let size_result = (do {
if ($cache_file | path exists) {
$cache_file | stat | get size
} else {
0
}
} | complete)
if $size_result.exit_code == 0 {
let file_size = ($size_result.stdout / 1024 / 1024)
$total_size += $file_size
$all_files = ($all_files | append {
path: $cache_file,
size: $file_size,
mtime: $meta.created_at
})
}
}
}
}
for file in $expired_files {
do {
rm -f $file
rm -f $"($file).meta"
} | complete | ignore
}
if $total_size > $max_size_mb {
let to_remove = ($total_size - $max_size_mb)
mut removed_size = 0
let sorted_files = ($all_files | sort-by mtime)
for file_info in $sorted_files {
if $removed_size >= $to_remove {
break
}
do {
rm -f $file_info.path
rm -f $"($file_info.path).meta"
} | complete | ignore
$removed_size += $file_info.size
}
}
} }
# Get cache statistics # No-op — eviction is handled by ncl-sync daemon.
export def get-cache-stats [] { export def cleanup-expired-cache [max_size_mb: int = 100]: nothing -> nothing {}
let base = (get-cache-base-dir)
if not ($base | path exists) {
return {
total_entries: 0,
total_size_mb: 0,
by_type: {}
}
}
mut stats = {
total_entries: 0,
total_size_mb: 0,
by_type: {}
}
for meta_file in (glob $"($base)/**/*.meta") {
let cache_file = ($meta_file | str substring 0..-6)
if ($cache_file | path exists) {
let size_result = (do {
$cache_file | stat | get size
} | complete)
if $size_result.exit_code == 0 {
let size_mb = ($size_result.stdout / 1024 / 1024)
$stats.total_entries += 1
$stats.total_size_mb += $size_mb
}
}
}
$stats
}
# Clear all cache for a specific type
export def cache-clear-type [
cache_type: string
] {
let base = (get-cache-base-dir)
let type_dir = ($base | path join (match $cache_type {
"kcl" => "kcl"
"sops" => "sops"
"final" => "workspaces"
"provider" => "providers"
"platform" => "platform"
_ => "other"
}))
if ($type_dir | path exists) {
do {
rm -rf $type_dir
mkdir $type_dir
} | complete | ignore
}
}

View file

@ -4,8 +4,9 @@
# TTL: 5 minutes (short for safety - workspace configs can change) # TTL: 5 minutes (short for safety - workspace configs can change)
# Follows Nushell 0.109.0+ guidelines # Follows Nushell 0.109.0+ guidelines
use ./core.nu * # Selective imports (ADR-025 Phase 3 Layer 2).
use ./metadata.nu * # cache/metadata star-import was dead — dropped.
use lib_provisioning/config/cache/core.nu [cache-clear-type cache-lookup cache-write]
# Helper: Generate cache key for workspace + environment combination # Helper: Generate cache key for workspace + environment combination
def compute-final-config-key [ def compute-final-config-key [
@ -34,7 +35,7 @@ def get-all-source-files [
let config_dir = ($workspace.path | path join "config") let config_dir = ($workspace.path | path join "config")
if ($config_dir | path exists) { if ($config_dir | path exists) {
# Add main config files # Add main config files
for config_file in ["provisioning.k" "provisioning.yaml"] { for config_file in ["provisioning.ncl" "provisioning.yaml"] {
let file_path = ($config_dir | path join $config_file) let file_path = ($config_dir | path join $config_file)
if ($file_path | path exists) { if ($file_path | path exists) {
$source_files = ($source_files | append $file_path) $source_files = ($source_files | append $file_path)
@ -141,7 +142,7 @@ export def invalidate-final-cache [
] { ] {
if $environment == "*" { if $environment == "*" {
# Invalidate ALL environments for workspace # Invalidate ALL environments for workspace
let base = (let home = ($env.HOME? | default "~" | path expand); let base = (let home = ($env.HOME? | default "~" | path expand);
$home | path join ".provisioning" "cache" "config" "workspaces") $home | path join ".provisioning" "cache" "config" "workspaces")
if ($base | path exists) { if ($base | path exists) {

Some files were not shown because too many files have changed in this diff Show more