chore: add current provisioning state before migration

This commit is contained in:
Jesús Pérez 2025-09-22 23:11:41 +01:00
parent a9703b4748
commit 50745b0f22
660 changed files with 88126 additions and 0 deletions

View file

@ -0,0 +1,12 @@
export def cleanup [
wk_path: string
]: nothing -> nothing {
if $env.PROVISIONING_DEBUG == false and ($wk_path | path exists) {
rm --force --recursive $wk_path
} else {
#use utils/interface.nu _ansi
_print $"(_ansi default_dimmed)______________________(_ansi reset)"
_print $"(_ansi default_dimmed)Work files not removed"
_print $"(_ansi default_dimmed)wk_path:(_ansi reset) ($wk_path)"
}
}

View file

@ -0,0 +1,107 @@
# Enhanced configuration management for provisioning tool
export def load-config [
config_path: string
--validate = true
]: record {
if not ($config_path | path exists) {
print $"🛑 Configuration file not found: ($config_path)"
return {}
}
try {
let config = (open $config_path)
if $validate {
validate-config $config
}
$config
} catch {|err|
print $"🛑 Error loading configuration from ($config_path): ($err.msg)"
{}
}
}
export def validate-config [
config: record
]: bool {
let required_fields = ["version", "providers", "servers"]
let missing_fields = ($required_fields | where {|field|
($config | get -o $field | is-empty)
})
if ($missing_fields | length) > 0 {
print "🛑 Missing required configuration fields:"
$missing_fields | each {|field| print $" - ($field)"}
return false
}
true
}
export def merge-configs [
base_config: record
override_config: record
]: record {
$base_config | merge $override_config
}
export def get-config-value [
config: record
path: string
default_value?: any
]: any {
let path_parts = ($path | split row ".")
let mut current = $config
for part in $path_parts {
if ($current | get -o $part | is-empty) {
return $default_value
}
$current = ($current | get $part)
}
$current
}
export def set-config-value [
config: record
path: string
value: any
]: record {
let path_parts = ($path | split row ".")
let mut result = $config
if ($path_parts | length) == 1 {
$result | upsert $path_parts.0 $value
} else {
let key = ($path_parts | last)
let parent_path = ($path_parts | range 0..-1 | str join ".")
let parent = (get-config-value $result $parent_path {})
let updated_parent = ($parent | upsert $key $value)
set-config-value $result $parent_path $updated_parent
}
}
export def save-config [
config: record
config_path: string
--backup = true
]: bool {
if $backup and ($config_path | path exists) {
let backup_path = $"($config_path).backup.(date now | format date '%Y%m%d_%H%M%S')"
try {
cp $config_path $backup_path
print $"💾 Backup created: ($backup_path)"
} catch {|err|
print $"⚠️ Warning: Could not create backup: ($err.msg)"
}
}
try {
$config | to yaml | save $config_path
print $"✅ Configuration saved to: ($config_path)"
true
} catch {|err|
print $"🛑 Error saving configuration: ($err.msg)"
false
}
}

View file

@ -0,0 +1,88 @@
# Enhanced logging system for provisioning tool
export def log-info [
message: string
context?: string
] {
let timestamp = (date now | format date '%Y-%m-%d %H:%M:%S')
let context_str = if ($context | is-not-empty) { $" [($context)]" } else { "" }
print $" ($timestamp)($context_str) ($message)"
}
export def log-success [
message: string
context?: string
] {
let timestamp = (date now | format date '%Y-%m-%d %H:%M:%S')
let context_str = if ($context | is-not-empty) { $" [($context)]" } else { "" }
print $"✅ ($timestamp)($context_str) ($message)"
}
export def log-warning [
message: string
context?: string
] {
let timestamp = (date now | format date '%Y-%m-%d %H:%M:%S')
let context_str = if ($context | is-not-empty) { $" [($context)]" } else { "" }
print $"⚠️ ($timestamp)($context_str) ($message)"
}
export def log-error [
message: string
context?: string
details?: string
] {
let timestamp = (date now | format date '%Y-%m-%d %H:%M:%S')
let context_str = if ($context | is-not-empty) { $" [($context)]" } else { "" }
let details_str = if ($details | is-not-empty) { $"\n Details: ($details)" } else { "" }
print $"🛑 ($timestamp)($context_str) ($message)($details_str)"
}
export def log-debug [
message: string
context?: string
] {
if $env.PROVISIONING_DEBUG {
let timestamp = (date now | format date '%Y-%m-%d %H:%M:%S')
let context_str = if ($context | is-not-empty) { $" [($context)]" } else { "" }
print $"🐛 ($timestamp)($context_str) ($message)"
}
}
export def log-step [
step: string
total_steps: int
current_step: int
context?: string
] {
let progress = $"($current_step)/($total_steps)"
let context_str = if ($context | is-not-empty) { $" [($context)]" } else { "" }
print $"🔄 ($progress)($context_str) ($step)"
}
export def log-progress [
message: string
percent: int
context?: string
] {
let context_str = if ($context | is-not-empty) { $" [($context)]" } else { "" }
print $"📊 ($context_str) ($message) ($percent)%"
}
export def log-section [
title: string
context?: string
] {
let context_str = if ($context | is-not-empty) { $" [($context)]" } else { "" }
print $""
print $"📋 ($context_str) ($title)"
print $"─────────────────────────────────────────────────────────────"
}
export def log-subsection [
title: string
context?: string
] {
let context_str = if ($context | is-not-empty) { $" [($context)]" } else { "" }
print $" 📌 ($context_str) ($title)"
}

View file

@ -0,0 +1,78 @@
export def throw-error [
error: string
text?: string
context?: string
--span: record
--code: int = 1
--suggestion: string
]: nothing -> nothing {
#use utils/interface.nu _ansi
let error = $"\n(_ansi red_bold)($error)(_ansi reset)"
let msg = ($text | default "this caused an internal error")
let suggestion = if ($suggestion | is-not-empty) { $"\n💡 Suggestion: (_ansi yellow)($suggestion)(_ansi reset)" } else { "" }
# Log error for debugging
if $env.PROVISIONING_DEBUG {
print $"DEBUG: Error occurred at: (date now | format date '%Y-%m-%d %H:%M:%S')"
print $"DEBUG: Context: ($context | default 'no context')"
print $"DEBUG: Error code: ($code)"
}
if ($env.PROVISIONING_OUT | is-empty) {
if $span == null and $context == null {
error make --unspanned { msg: ( $error + "\n" + $msg + $suggestion) }
} else if $span != null and $env.PROVISIONING_METADATA {
error make {
msg: $error
label: {
text: $"($msg) (_ansi blue)($context)(_ansi reset)($suggestion)"
span: $span
}
}
} else {
error make --unspanned { msg: ( $error + "\n" + $msg + "\n" + $"(_ansi blue)($context | default "" )(_ansi reset)($suggestion)") }
}
} else {
_print ( $error + "\n" + $msg + "\n" + $"(_ansi blue)($context | default "" )(_ansi reset)($suggestion)")
}
}
export def safe-execute [
command: closure
context: string
--fallback: closure
] {
let result = (do $command | complete)
if $result.exit_code != 0 {
print $"⚠️ Warning: Error in ($context): ($result.stderr)"
if ($fallback | is-not-empty) {
print "🔄 Executing fallback..."
do $fallback
} else {
print $"🛑 Execution failed in ($context)"
print $" Error: ($result.stderr)"
}
} else {
$result.stdout
}
}
export def try [
settings_data: record
defaults_data: record
]: nothing -> nothing {
$settings_data.servers | each { |server|
_print ( $defaults_data.defaults | merge $server )
}
_print ($settings_data.servers | get hostname)
_print ($settings_data.servers | get 0).tasks
let zli_cfg = (open "resources/oci-reg/zli-cfg" | from json)
if $zli_cfg.sops? != null {
_print "Found"
} else {
_print "NOT Found"
}
let pos = 0
_print ($settings_data.servers | get $pos )
}

View file

@ -0,0 +1,81 @@
export def throw-error [
error: string
text?: string
context?: string
--span: record
--code: int = 1
--suggestion: string
]: nothing -> nothing {
let error = $"\n(_ansi red_bold)($error)(_ansi reset)"
let msg = ($text | default "this caused an internal error")
let suggestion = if ($suggestion | is-not-empty) {
$"\n💡 Suggestion: (_ansi yellow)($suggestion)(_ansi reset)"
} else {
""
}
# Log error for debugging
if $env.PROVISIONING_DEBUG {
print $"DEBUG: Error occurred at: (date now | format date '%Y-%m-%d %H:%M:%S')"
print $"DEBUG: Context: ($context | default 'no context')"
print $"DEBUG: Error code: ($code)"
}
if ($env.PROVISIONING_OUT | is-empty) {
if $span == null and $context == null {
error make --unspanned { msg: ( $error + "\n" + $msg + $suggestion) }
} else if $span != null and $env.PROVISIONING_METADATA {
error make {
msg: $error
label: {
text: $"($msg) (_ansi blue)($context)(_ansi reset)($suggestion)"
span: $span
}
}
} else {
error make --unspanned {
msg: ( $error + "\n" + $msg + "\n" + $"(_ansi blue)($context | default "" )(_ansi reset)($suggestion)")
}
}
} else {
_print ( $error + "\n" + $msg + "\n" + $"(_ansi blue)($context | default "" )(_ansi reset)($suggestion)")
}
}
export def safe-execute [
command: closure
context: string
--fallback: closure
]: any {
try {
do $command
} catch {|err|
print $"⚠️ Warning: Error in ($context): ($err.msg)"
if ($fallback | is-not-empty) {
print "🔄 Executing fallback..."
do $fallback
} else {
print $"🛑 Execution failed in ($context)"
print $" Error: ($err.msg)"
}
}
}
export def try [
settings_data: record
defaults_data: record
]: nothing -> nothing {
$settings_data.servers | each { |server|
_print ( $defaults_data.defaults | merge $server )
}
_print ($settings_data.servers | get hostname)
_print ($settings_data.servers | get 0).tasks
let zli_cfg = (open "resources/oci-reg/zli-cfg" | from json)
if $zli_cfg.sops? != null {
_print "Found"
} else {
_print "NOT Found"
}
let pos = 0
_print ($settings_data.servers | get $pos )
}

View file

@ -0,0 +1,80 @@
export def throw-error [
error: string
text?: string
context?: string
--span: record
--code: int = 1
--suggestion: string
]: nothing -> nothing {
let error = $"\n(_ansi red_bold)($error)(_ansi reset)"
let msg = ($text | default "this caused an internal error")
let suggestion = if ($suggestion | is-not-empty) {
$"\n💡 Suggestion: (_ansi yellow)($suggestion)(_ansi reset)"
} else {
""
}
if $env.PROVISIONING_DEBUG {
print $"DEBUG: Error occurred at: (date now | format date '%Y-%m-%d %H:%M:%S')"
print $"DEBUG: Context: ($context | default 'no context')"
print $"DEBUG: Error code: ($code)"
}
if ($env.PROVISIONING_OUT | is-empty) {
if $span == null and $context == null {
error make --unspanned { msg: ( $error + "\n" + $msg + $suggestion) }
} else if $span != null and $env.PROVISIONING_METADATA {
error make {
msg: $error
label: {
text: $"($msg) (_ansi blue)($context)(_ansi reset)($suggestion)"
span: $span
}
}
} else {
error make --unspanned {
msg: ( $error + "\n" + $msg + "\n" + $"(_ansi blue)($context | default "" )(_ansi reset)($suggestion)")
}
}
} else {
_print ( $error + "\n" + $msg + "\n" + $"(_ansi blue)($context | default "" )(_ansi reset)($suggestion)")
}
}
export def safe-execute [
command: closure
context: string
--fallback: closure
] {
try {
do $command
} catch {|err|
print $"⚠️ Warning: Error in ($context): ($err.msg)"
if ($fallback | is-not-empty) {
print "🔄 Executing fallback..."
do $fallback
} else {
print $"🛑 Execution failed in ($context)"
print $" Error: ($err.msg)"
}
}
}
export def try [
settings_data: record
defaults_data: record
]: nothing -> nothing {
$settings_data.servers | each { |server|
_print ( $defaults_data.defaults | merge $server )
}
_print ($settings_data.servers | get hostname)
_print ($settings_data.servers | get 0).tasks
let zli_cfg = (open "resources/oci-reg/zli-cfg" | from json)
if $zli_cfg.sops? != null {
_print "Found"
} else {
_print "NOT Found"
}
let pos = 0
_print ($settings_data.servers | get $pos )
}

View file

@ -0,0 +1,81 @@
export def throw-error [
error: string
text?: string
context?: string
--span: record
--code: int = 1
--suggestion: string
]: nothing -> nothing {
let error = $"\n(_ansi red_bold)($error)(_ansi reset)"
let msg = ($text | default "this caused an internal error")
let suggestion = if ($suggestion | is-not-empty) {
$"\n💡 Suggestion: (_ansi yellow)($suggestion)(_ansi reset)"
} else {
""
}
# Log error for debugging
if $env.PROVISIONING_DEBUG {
print $"DEBUG: Error occurred at: (date now | format date '%Y-%m-%d %H:%M:%S')"
print $"DEBUG: Context: ($context | default 'no context')"
print $"DEBUG: Error code: ($code)"
}
if ($env.PROVISIONING_OUT | is-empty) {
if $span == null and $context == null {
error make --unspanned { msg: ( $error + "\n" + $msg + $suggestion) }
} else if $span != null and $env.PROVISIONING_METADATA {
error make {
msg: $error
label: {
text: $"($msg) (_ansi blue)($context)(_ansi reset)($suggestion)"
span: $span
}
}
} else {
error make --unspanned {
msg: ( $error + "\n" + $msg + "\n" + $"(_ansi blue)($context | default "" )(_ansi reset)($suggestion)")
}
}
} else {
_print ( $error + "\n" + $msg + "\n" + $"(_ansi blue)($context | default "" )(_ansi reset)($suggestion)")
}
}
export def safe-execute [
command: closure
context: string
--fallback: closure
]: any {
try {
do $command
} catch {|err|
print $"⚠️ Warning: Error in ($context): ($err.msg)"
if ($fallback | is-not-empty) {
print "🔄 Executing fallback..."
do $fallback
} else {
print $"🛑 Execution failed in ($context)"
print $" Error: ($err.msg)"
}
}
}
export def try [
settings_data: record
defaults_data: record
]: nothing -> nothing {
$settings_data.servers | each { |server|
_print ( $defaults_data.defaults | merge $server )
}
_print ($settings_data.servers | get hostname)
_print ($settings_data.servers | get 0).tasks
let zli_cfg = (open "resources/oci-reg/zli-cfg" | from json)
if $zli_cfg.sops? != null {
_print "Found"
} else {
_print "NOT Found"
}
let pos = 0
_print ($settings_data.servers | get $pos )
}

View file

@ -0,0 +1,113 @@
use std
use ../secrets/lib.nu decode_secret_file
use ../secrets/lib.nu get_secret_provider
export def find_file [
start_path: string
match_path: string
only_first: bool
] {
mut found_path = ""
mut search_path = $start_path
let home_root = ($env.HOME | path dirname)
while $found_path == "" and $search_path != "/" and $search_path != $home_root {
if $search_path == "" { break }
let res = if $only_first {
(^find $search_path -type f -name $match_path -print -quit | complete)
} else {
(^find $search_path -type f -name $match_path err> (if $nu.os-info.name == "windows" { "NUL" } else { "/dev/null" }) | complete)
}
if $res.exit_code == 0 { $found_path = ($res.stdout | str trim ) }
$search_path = ($search_path | path dirname)
}
$found_path
}
export def copy_file [
source: string
target: string
quiet: bool
] {
let provider = (get_secret_provider)
if $provider == "" or ($env.PROVISIONING_USE_SOPS == "" and $env.PROVISIONING_USE_KMS == "") {
let ops = if $quiet { "" } else { "-v" }
cp $ops $source $target
return
}
(decode_secret_file $source $target $quiet)
}
export def copy_prov_files [
src_root: string
src_path: string
target: string
no_replace: bool
quiet: bool
] {
mut path_name = ""
let start_path = if $src_path == "" or $src_path == "." { $src_root } else { ($src_root | path join $src_path) } | str replace "." $env.PWD
let p = ($start_path | path type)
if not ($start_path | path exists) { return }
if ($start_path | path type) != "dir" {
# if ($"($target)/($path_name)" | path exists ) and $no_replace { return }
copy_file $start_path $target $quiet
return
}
for item in (glob ($start_path | path join "*")) {
$path_name = ($item | path basename)
if ($item | path type) == "dir" {
if not ($target | path join $path_name | path exists) { ^mkdir -p ($target | path join $path_name) }
copy_prov_files ($item | path dirname) $path_name ($target | path join $path_name) $no_replace $quiet
} else if ($item | path exists) {
if ($target | path join $path_name| path exists ) and $no_replace { continue }
if not ($target | path exists) { ^mkdir -p $target }
copy_file $item ($target | path join $path_name) $quiet
}
}
}
export def select_file_list [
root_path: string
title: string
is_for_task: bool
recursive_cnt: int
]: nothing -> string {
if ($env | get -o PROVISIONING_OUT | default "" | is-not-empty) or $env.PROVISIONING_NO_TERMINAL { return ""}
if not ($root_path | path dirname | path exists) { return {} }
_print $"(_ansi purple_bold)($title)(_ansi reset) ($root_path) "
if (glob $root_path | length) == 0 { return {} }
let pick_list = (ls ($root_path | into glob) | default [])
let msg_sel = if $is_for_task {
"Select one file"
} else {
"To use a file select one"
}
if ($pick_list | length) == 0 { return "" }
let selection = if ($pick_list | length) > 1 {
let prompt = $"(_ansi default_dimmed)($msg_sel) \(use arrows and press [enter] or [esc] to cancel\):(_ansi reset)"
let pos_select = ($pick_list | each {|it| $"($it.modified) -> ($it.name | path basename)"} |input list --index $prompt)
if $pos_select == null { return null }
let selection = ($pick_list | get -o $pos_select)
if not $is_for_task {
_print $"\nFor (_ansi green_bold)($selection.name)(_ansi reset) file use:"
}
$selection
} else {
let selection = ($pick_list | get -o 0)
if not $is_for_task {
_print $"\n(_ansi default_dimmed)For a file (_ansi reset)(_ansi green_bold)($selection.name)(_ansi reset) use:"
}
$selection
}
let file_selection = if $selection.type == "dir" {
let cnt = if $recursive_cnt > 0 {
# print $recursive_cnt
if ($recursive_cnt - 1) == 0 { return $selection }
$recursive_cnt - 1
} else { $recursive_cnt }
return (select_file_list $selection.name $title $is_for_task $cnt)
} else {
$selection
}
if not $is_for_task {
show_clip_to $"($file_selection.name)" true
}
$file_selection
}

View file

@ -0,0 +1,47 @@
use std
export def datalist_to_format [
out: string
data: list
] {
# Not supported "toml" => ($data | flatten | to toml )
match $out {
"json" => ( $data | to json )
"yaml" => ( $data | to yaml )
"text" => ( $data | to text )
"md" => ( $data | to md )
"nuon" => ( $data | to nuon )
"csv" => ( $data | to csv )
_ => {
$data |table -e
# if $cols != null {
# let str_cols = ($cols | str replace "ips" "")
# $ips = if ($cols | str contains "ips") {
# # _print (mw_servers_ips $curr_settings $args --prov $prov --serverpos $serverpos)
# ($data | each {|srv| | ($srv.ip_addresses |
# each {|it| { hostname: $srv.hostname, ip: $it.address, access: $it.access, family: $it.family }})} |
# flatten
# )
# }
# #if $str_cols != "" {
# # ($data | select -o ($str_cols | split row ","))
# #}
# } else {
# $data
# }
}
}
}
export def money_conversion [
src: string
target: string
amount: float
] {
let host = 'api.frankfurter.app';
let url = $"https://($host)/latest?amount=($amount)&from=($src)&to=($target)"
#let data = (http get $url --raw --allow-errors)
let res = (^curl -sSL $url err> (if $nu.os-info.name == "windows" { "NUL" } else { "/dev/null" }) | complete)
if $res.exit_code == 0 and ($res.stdout | is-not-empty) {
($res.stdout| from json | get -o rates | get -o $target | default 0)
} else { 0 }
}

View file

@ -0,0 +1,178 @@
#!/usr/bin/env -S nu
# Author: JesusPerezLorenzo
# Release: 1.0.4
# Date: 6-2-2024
#use ../lib_provisioning/utils/templates.nu on_template_path
export def github_latest_tag [
url: string = ""
use_dev_release: bool = false
id_target: string = "releases/tag"
]: nothing -> string {
#let res = (http get $url -r )
if ($url | is-empty) { return "" }
let res = (^curl -s $url | complete)
let html_content = if ($res.exit_code != 0) {
print $"🛑 Error (_ansi red)($url)(_ansi reset):\n ($res.exit_code) ($res.stderr)"
return ""
} else { $res.stdout }
# curl -s https://github.com/project-zot/zot/tags | grep "<h2 " | grep "releases/tag"
let versions = ($html_content | parse --regex '<h2 (?<a>.*?)</a>' | get -o a | each {|it|
($it | parse --regex ($"($id_target)" + '/(?<version>.*?)"') | get version | get -o 0 | default "")
})
let list = if $use_dev_release {
$versions
} else {
($versions | where {|it|
not ($it | str contains "-rc") and not ($it | str contains "-alpha")
})
}
$list | sort -r | get -o 0 | default ""
}
export def value_input_list [
input_type: string
options_list: list
msg: string
default_value: string
]: nothing -> string {
let selection_pos = ( $options_list
| input list --index (
$"(_ansi default_dimmed)Select(_ansi reset) (_ansi yellow_bold)($msg)(_ansi reset) " +
$"\n(_ansi default_dimmed)\(use arrow keys and press [enter] or [escape] for default '(_ansi reset)" +
$"($default_value)(_ansi default_dimmed)'\)(_ansi reset)"
))
if $selection_pos != null {
($options_list | get -o $selection_pos | default $default_value)
} else { $default_value }
}
export def value_input [
input_type: string
numchar: int
msg: string
default_value: string
not_empty: bool
]: nothing -> string {
while true {
let value_input = if $numchar > 0 {
print ($"(_ansi yellow_bold)($msg)(_ansi reset) " +
$"(_ansi default_dimmed) type value (_ansi green_bold)($numchar) chars(_ansi reset) " +
$"(_ansi default_dimmed) default '(_ansi reset)" +
$"($default_value)(_ansi default_dimmed)'(_ansi reset)"
)
(input --numchar $numchar)
} else {
print ($"(_ansi yellow_bold)($msg)(_ansi reset) " +
$"(_ansi default_dimmed)\(type value and press [enter] default '(_ansi reset)" +
$"($default_value)(_ansi default_dimmed)'\)(_ansi reset)"
)
(input)
}
if $not_empty and ($value_input | is-empty) {
if ($default_value | is-not-empty) { return $default_value }
continue
} else if ($value_input | is-empty) {
return $default_value
}
let result = match $input_type {
"number" => {
if ($value_input | parse --regex '^[0-9]' | length) > 0 { $value_input } else { "" }
},
"ipv4-address" => {
if ($value_input | parse --regex '^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$' | length) > 0 { $value_input } else { "" }
},
_ => $value_input,
}
if $value_input != $result { continue }
return $value_input
}
return $default_value
}
export def "generate_title" [
title: string
]: nothing -> nothing {
_print $"\n(_ansi purple)($env.PROVISIONING_NAME)(_ansi reset) (_ansi default_dimmed)generate:(_ansi reset) (_ansi cyan)($title)(_ansi reset)"
_print $"(_ansi default_dimmed)-------------------------------------------------------------(_ansi reset)\n"
}
export def "generate_data_items" [
defs_gen: list = []
defs_values: list = []
]: nothing -> record {
mut data = {}
for it in $defs_values {
let input_type = ($it | get -o input_type | default "")
let options_list = ($it | get -o options_list | default [])
let numchar = ($it | get -o numchar | default 0)
let msg = ($it | get -o msg | default "")
let default_value = match $input_type {
"list-record" | "list" => ($it | get -o default_value | default []),
"record" => ($it | get -o default_value | default {}),
_ => ($it | get -o default_value | default ""),
}
let var = ($it | get -o var | default "")
let not_empty = ($it | get -o not_empty | default false)
print $input_type
let value = match $input_type {
"record" => (generate_data_items $it),
"list-record" => {
let record_key = ($it | get -o record | default "")
let record_value = ($defs_gen | get -o $record_key | default [])
print ($record_value | table -e)
# where {|it| ($it | get -o $record_key | is-not-empty)} | get -o 0 | get -o $record_key | default [])
if ($record_value | is-empty) { continue }
mut val = []
while true {
let selection_pos = ( [ $"Add ($msg)", $"No more ($var)" ]
| input list --index (
$"(_ansi default_dimmed)Select(_ansi reset) (_ansi yellow_bold)($msg)(_ansi reset) " +
$"\n(_ansi default_dimmed)\(use arrow keys and press [enter] or [escape] to finish '(_ansi reset)"
))
if $selection_pos == null or $selection_pos == 1 { break }
$val = ($val | append (generate_data_items $defs_gen $record_value))
}
$val
},
"list" => (value_input_list $input_type $options_list $msg $default_value),
_ => (value_input $input_type $numchar $msg $default_value $not_empty),
}
$data = ($data | merge { $var: $value })
}
$data
}
export def "generate_data_def" [
root_path: string
infra_name: string
infra_path: string
created: bool
inputfile: string = ""
]: nothing -> nothing {
let data = (if ($inputfile | is-empty) {
let defs_path = ($root_path | path join $env.PROVISIONING_GENERATE_DIRPATH | path join $env.PROVISIONING_GENERATE_DEFSFILE)
if ( $defs_path | path exists) {
let data_gen = (open $defs_path)
let title = $"($data_gen| get -o title | default "")"
generate_title $title
let defs_values = ($data_gen | get -o defs_values | default [])
(generate_data_items $data_gen $defs_values)
} else {
if $env.PROVISIONING_DEBUG { _print $"🛑 ($env.PROVISIONING_NAME) generate: Invalid path (_ansi red)($defs_path)(_ansi reset)" }
}
} else {
(open $inputfile)
} | merge {
infra_name: $infra_name,
infra_path: $infra_path,
})
let vars_filepath = $"/tmp/data_($infra_name)_($env.NOW).yaml"
($data | to yaml | str replace "$name" $infra_name| save -f $vars_filepath)
let remove_files = if $env.PROVISIONING_DEBUG { false } else { true }
on_template_path $infra_path $vars_filepath $remove_files true
if not $env.PROVISIONING_DEBUG {
rm -f $vars_filepath
}
}

View file

@ -0,0 +1,23 @@
export def parse_help_command [
source: string
name?: string
--task: closure
--ismod
--end
] {
#use utils/interface.nu end_run
let args = $env.PROVISIONING_ARGS? | default ""
let has_help = if ($args | str contains "help") or ($args |str ends-with " h") {
true
} else if $name != null and $name == "help" or $name == "h" {
true
} else { false }
if not $has_help { return }
let mod_str = if $ismod { "-mod" } else { "" }
^$env.PROVISIONING_NAME $mod_str ...($source | split row " ") --help
if $task != null { do $task }
if $end {
if not $env.PROVISIONING_DEBUG { end_run "" }
exit
}
}

View file

@ -0,0 +1,71 @@
# Import Helper Functions
# Provides clean, environment-based imports to avoid relative paths
# Provider middleware imports
export def prov-middleware []: nothing -> string {
$env.PROVISIONING_PROV_LIB | path join "middleware.nu"
}
export def prov-env-middleware []: nothing -> string {
$env.PROVISIONING_PROV_LIB | path join "env_middleware.nu"
}
# Provider-specific imports
export def aws-env []: nothing -> string {
$env.PROVISIONING_PROVIDERS_PATH | path join "aws" "nulib" "aws" "env.nu"
}
export def aws-servers []: nothing -> string {
$env.PROVISIONING_PROVIDERS_PATH | path join "aws" "nulib" "aws" "servers.nu"
}
export def upcloud-env []: nothing -> string {
$env.PROVISIONING_PROVIDERS_PATH | path join "upcloud" "nulib" "upcloud" "env.nu"
}
export def upcloud-servers []: nothing -> string {
$env.PROVISIONING_PROVIDERS_PATH | path join "upcloud" "nulib" "upcloud" "servers.nu"
}
export def local-env []: nothing -> string {
$env.PROVISIONING_PROVIDERS_PATH | path join "local" "nulib" "local" "env.nu"
}
export def local-servers []: nothing -> string {
$env.PROVISIONING_PROVIDERS_PATH | path join "local" "nulib" "local" "servers.nu"
}
# Core module imports
export def core-servers []: nothing -> string {
$env.PROVISIONING_CORE_NULIB | path join "servers"
}
export def core-taskservs []: nothing -> string {
$env.PROVISIONING_CORE_NULIB | path join "taskservs"
}
export def core-clusters []: nothing -> string {
$env.PROVISIONING_CORE_NULIB | path join "clusters"
}
# Lib provisioning imports (for internal cross-references)
export def lib-utils []: nothing -> string {
$env.PROVISIONING_CORE_NULIB | path join "lib_provisioning" "utils"
}
export def lib-secrets []: nothing -> string {
$env.PROVISIONING_CORE_NULIB | path join "lib_provisioning" "secrets"
}
export def lib-sops []: nothing -> string {
$env.PROVISIONING_CORE_NULIB | path join "lib_provisioning" "sops"
}
export def lib-ai []: nothing -> string {
$env.PROVISIONING_CORE_NULIB | path join "lib_provisioning" "ai"
}
# Helper for dynamic imports with specific files
export def import-path [base: string, file: string]: nothing -> string {
$base | path join $file
}

View file

@ -0,0 +1,50 @@
export def show_titles []: nothing -> nothing {
if (detect_claude_code) { return false }
if ($env.PROVISIONING_NO_TITLES? | default false) { return }
if ($env.PROVISIONING_OUT | is-not-empty) { return }
_print $"(_ansi blue_bold)(open -r ($env.PROVISIONING_RESOURCES | path join "ascii.txt"))(_ansi reset)"
}
export def use_titles [ ]: nothing -> bool {
if ($env.PROVISIONING_NO_TITLES? | default false) { return }
if ($env.PROVISIONING_NO_TERMINAL? | default false) { return false }
if ($env.PROVISIONING_ARGS? | str contains "-h" ) { return false }
if ($env.PROVISIONING_ARGS? | str contains "--notitles" ) { return false }
if ($env.PROVISIONING_ARGS? | str contains "query") and ($env.PROVISIONING_ARGS? | str contains "-o" ) { return false }
true
}
export def provisioning_init [
helpinfo: bool
module: string
args: list<string> # Other options, use help to get info
]: nothing -> nothing {
if (use_titles) { show_titles }
if $helpinfo != null and $helpinfo {
let cmd_line: list<string> = if ($args| length) == 0 {
$args | str join " "
} else {
$env.PROVISIONING_ARGS? | default ""
}
let cmd_args: list<string> = ($cmd_line | str replace "--helpinfo" "" |
str replace "-h" "" | str replace $module "" | str trim | split row " "
)
if ($cmd_args | length) > 0 {
# _print $"---($module)-- ($env.PROVISIONING_NAME) -mod '($module)' ($cmd_args) help"
^$"($env.PROVISIONING_NAME)" "-mod" $"($module | str replace ' ' '|')" ...$cmd_args help
# let str_mod_0 = ($cmd_args | get -o 0 | default "")
# let str_mod_1 = ($cmd_args | get -o 1 | default "")
# if $str_mod_1 != "" {
# let final_args = ($cmd_args | drop nth 0 1)
# _print $"---($module)-- ($env.PROVISIONING_NAME) -mod '($str_mod_0) ($str_mod_1)' ($cmd_args | drop nth 0) help"
# ^$"($env.PROVISIONING_NAME)" "-mod" $"'($str_mod_0) ($str_mod_1)'" ...$final_args help
# } else {
# let final_args = ($cmd_args | drop nth 0)
# _print $"---($module)-- ($env.PROVISIONING_NAME) -mod ($str_mod_0) ($cmd_args | drop nth 0) help"
# ^$"($env.PROVISIONING_NAME)" "-mod" ($str_mod_0) ...$final_args help
# }
} else {
^$"($env.PROVISIONING_NAME)" help
}
exit 0
}
}

View file

@ -0,0 +1,193 @@
export def _ansi [
arg?: string
--escape: record
]: nothing -> string {
if ($env | get -o PROVISIONING_NO_TERMINAL | default false) {
""
} else if (is-terminal --stdout) {
if $escape != null {
(ansi --escape $escape)
} else {
(ansi $arg)
}
} else {
""
}
}
export def format_out [
data: string
src?: string
mode?: string
]: nothing -> string {
let msg = match $src {
"json" => ($data | from json),
_ => $data,
}
match $mode {
"table" => {
($msg | table -i false)
},
_ => { $msg }
}
}
export def _print [
data: string
src?: string
context?: string
mode?: string
-n # no newline
]: nothing -> nothing {
let output = ($env | get -o PROVISIONING_OUT| default "")
if $n {
if ($output | is-empty) {
print -n $data
}
return
}
if ($output | is-empty) {
print (format_out $data $src $mode)
} else {
match $output {
"json" => {
if $context != "result" { return }
if $src == "json" {
print ($data)
} else {
print ($data | to json)
}
},
"yaml" | "yml" => {
if $context != "result" { return }
if $src == "json" {
print ($data | from json | to yaml)
} else {
print ($data | to yaml)
}
},
"toml" | "tml" => {
if $context != "result" { return }
if $src == "json" {
print ($data | from json | to toml)
} else {
print ($data)
}
},
"text" | "txt" => {
if $context != "result" { return }
print (format_out $data $src $mode)
},
_ => {
if ($output | str ends-with ".json" ) {
if $context != "result" { return }
(if $src == "json" {
($data)
} else {
($data | to json)
} | save --force $output)
} else if ($output | str ends-with ".yaml" ) {
if $context != "result" { return }
(if $src == "json" {
($data | from json | to yaml)
} else {
($data | to yaml)
} | save --force $output)
} else if ($output | str ends-with ".toml" ) {
if $context != "result" { return }
(if $src == "json" {
($data | from json | to toml)
} else {
($data)
} | save --force $output)
} else if ($output | str ends-with ".text" ) or ($output | str ends-with ".txt" ) {
if $context != "result" { return }
format_out $data $src $mode | save --force $output
} else {
format_out $data $src $mode | save --append $output
}
}
}
}
}
export def end_run [
context: string
]: nothing -> nothing {
if ($env.PROVISIONING_OUT | is-not-empty) { return }
if ($env.PROVISIONING_NO_TITLES? | default false) { return false }
if (detect_claude_code) { return false }
if $env.PROVISIONING_DEBUG {
_print $"\n(_ansi blue)----🌥 ----🌥 ----🌥 ---- oOo ----🌥 ----🌥 ----🌥 ---- (_ansi reset)"
} else {
let the_context = if $context != "" { $" to ($context)" } else { "" }
if (is-terminal --stdout) {
_print $"\n(_ansi cyan)Thanks for using (_ansi blue_bold)($env.PROVISIONING_URL | ansi link --text 'Provisioning')(_ansi reset)"
if $the_context != "" {
_print $"(_ansi yellow_dimmed)($the_context)(_ansi reset)"
}
_print ($env.PROVISIONING_URL | ansi link --text $"(_ansi default_dimmed)Click here for more info or visit \n($env.PROVISIONING_URL)(_ansi reset)")
} else {
_print $"\n(_ansi cyan)Thanks for using (_ansi blue_bold) Provisioning [($env.PROVISIONING_URL)](_ansi reset)($the_context)"
_print $"(_ansi default_dimmed)For more info or visit ($env.PROVISIONING_URL)(_ansi reset)"
}
}
}
export def show_clip_to [
msg: string
show: bool
]: nothing -> nothing {
if $show { _print $msg }
if (is-terminal --stdout) {
clip_copy $msg $show
}
}
export def log_debug [
msg: string
]: nothing -> nothing {
use std
std log debug $msg
# std assert (1 == 1)
}
#// Examples:
#// desktop_run_notify "Port scan" "Done" { port scan 8.8.8.8 53 }
#// desktop_run_notify "Task try" "Done" --timeout 5sec
export def desktop_run_notify [
title: string
body: string
task?: closure
--timeout: duration
--icon: string
] {
let icon_path = if $icon == null {
$env.PROVISIONING_NOTIFY_ICON
} else { $icon }
let time_out = if $timeout == null {
8sec
} else { $timeout }
if $task != null {
let start = date now
let result = do $task
let end = date now
let total = $end - $start | format duration sec
let result_typ = ($result | describe)
let msg = if $result_typ == "bool" {
(if $result { "✅ done " } else { $"🛑 fail "})
} else if ($result_typ | str starts-with "record") {
(if $result.status { "✅ done " } else { $"🛑 fail ($result.error)" })
} else { "" }
let time_body = $"($body) ($msg) finished in ($total) "
( notify_msg $title $body $icon_path $time_body $timeout $task )
return $result
} else {
( notify_msg $title $body $icon_path "" $timeout $task )
true
}
}
export def detect_claude_code []: nothing -> bool {
let claudecode = ($env.CLAUDECODE? | default "" | str contains "1")
let entrypoint = ($env.CLAUDE_CODE_ENTRYPOINT? | default "" | str contains "cli")
$claudecode or $entrypoint
}

View file

@ -0,0 +1,70 @@
# Enhanced logging system for provisioning tool
export def log-info [
message: string
context?: string
] {
let timestamp = (date now | format date '%Y-%m-%d %H:%M:%S')
let context_str = if ($context | is-not-empty) { $" [($context)]" } else { "" }
print $" ($timestamp)($context_str) ($message)"
}
export def log-success [
message: string
context?: string
] {
let timestamp = (date now | format date '%Y-%m-%d %H:%M:%S')
let context_str = if ($context | is-not-empty) { $" [($context)]" } else { "" }
print $"✅ ($timestamp)($context_str) ($message)"
}
export def log-warning [
message: string
context?: string
] {
let timestamp = (date now | format date '%Y-%m-%d %H:%M:%S')
let context_str = if ($context | is-not-empty) { $" [($context)]" } else { "" }
print $"⚠️ ($timestamp)($context_str) ($message)"
}
export def log-error [
message: string
context?: string
details?: string
] {
let timestamp = (date now | format date '%Y-%m-%d %H:%M:%S')
let context_str = if ($context | is-not-empty) { $" [($context)]" } else { "" }
let details_str = if ($details | is-not-empty) { $"\n Details: ($details)" } else { "" }
print $"🛑 ($timestamp)($context_str) ($message)($details_str)"
}
export def log-debug [
message: string
context?: string
] {
if $env.PROVISIONING_DEBUG {
let timestamp = (date now | format date '%Y-%m-%d %H:%M:%S')
let context_str = if ($context | is-not-empty) { $" [($context)]" } else { "" }
print $"🐛 ($timestamp)($context_str) ($message)"
}
}
export def log-step [
step: string
total_steps: int
current_step: int
context?: string
] {
let progress = $"($current_step)/($total_steps)"
let context_str = if ($context | is-not-empty) { $" [($context)]" } else { "" }
print $"🔄 ($progress)($context_str) ($step)"
}
export def log-progress [
message: string
percent: int
context?: string
] {
let context_str = if ($context | is-not-empty) { $" [($context)]" } else { "" }
print $"📊 ($context_str) ($message) ($percent)%"
}

View file

@ -0,0 +1,23 @@
# Exclude minor or specific parts for global 'export use'
export use interface.nu *
export use clean.nu *
export use error.nu *
export use help.nu *
export use init.nu *
export use generate.nu *
export use undefined.nu *
export use qr.nu *
export use ssh.nu *
export use settings.nu *
export use templates.nu *
# export use test.nu
export use format.nu *
export use files.nu *
export use on_select.nu *
export use imports.nu *

View file

@ -0,0 +1,65 @@
export def run_on_selection [
select: string
name: string
item_path: string
main_path: string
root_path: string
]: nothing -> nothing {
if not ($item_path | path exists) { return }
match $select {
"edit" | "editor" | "ed" | "e" => {
let cmd = ($env | get -o EDITOR | default "vi")
let full_cmd = $"($cmd) ($main_path)"
^($cmd) $main_path
show_clip_to $full_cmd true
},
"view" | "vw" | "v" => {
let cmd = ($env| get -o PROVISIONING_FILEVIEWER | default (if (^bash -c "type -P bat" | is-not-empty) { "bat" } else { "cat" }))
let full_cmd = $"($cmd) ($main_path)"
^($cmd) $main_path
show_clip_to $full_cmd true
},
"list" | "ls" | "l" => {
let full_cmd = $"ls -l ($item_path)"
print (ls $item_path | each {|it| {
name: ($it.name | str replace $root_path ""),
type: $it.type, size: $it.size, modified: $it.modified
}})
show_clip_to $full_cmd true
},
"tree" | "tr" | "t" => {
let full_cmd = $"tree -L 3 ($item_path)"
^tree -L 3 $item_path
show_clip_to $full_cmd true
},
"code" | "c" => {
let full_cmd = $"code ($item_path)"
^code $item_path
show_clip_to $full_cmd true
},
"shell" | "sh" | "s" => {
let full_cmd = $"($env.SHELL) -c " + $"cd ($item_path) ; ($env.SHELL)"
print $"(_ansi default_dimmed)Use [ctrl-d] or 'exit' to end with(_ansi reset) ($env.SHELL)"
^($env.SHELL) -c $"cd ($item_path) ; ($env.SHELL)"
show_titles
_print "Command "
(show_clip_to $full_cmd false)
},
"nu"| "n" => {
let full_cmd = $"($env.NU) -i -e " + $"cd ($item_path)"
_print $"(_ansi default_dimmed)Use [ctrl-d] or 'exit' to end with(_ansi reset) nushell\n"
^($env.NU) -i -e $"cd ($item_path)"
show_titles
_print "Command "
(show_clip_to $full_cmd false)
},
"" => {
_print $"($name): ($item_path)"
show_clip_to $item_path false
},
_ => {
_print $"($select) ($name): ($item_path)"
show_clip_to $item_path false
}
}
}

View file

@ -0,0 +1,5 @@
export def "make_qr" [
url?: string
] {
show_qr ($url | default $env.PROVISIONING_URL)
}

View file

@ -0,0 +1,501 @@
use ../../../../providers/prov_lib/middleware.nu *
use ../context.nu *
use ../sops/mod.nu *
export def find_get_settings [
--infra (-i): string # Infra directory
--settings (-s): string # Settings path
include_notuse: bool = false
no_error: bool = false
]: nothing -> record {
#use utils/settings.nu [ load_settings ]
if $infra != null {
if $settings != null {
(load_settings --infra $infra --settings $settings $include_notuse $no_error)
} else {
(load_settings --infra $infra $include_notuse $no_error)
}
} else {
if $settings != null {
(load_settings --settings $settings $include_notuse $no_error)
} else {
(load_settings $include_notuse $no_error)
}
}
}
export def check_env [
]: nothing -> bool {
# TuDO
true
}
export def get_context_infra_path [
]: nothing -> string {
let context = (setup_user_context)
if $context == null or $context.infra == null { return "" }
if $context.infra_path? != null and ($context.infra_path | path join $context.infra | path exists) {
return ($context.infra_path| path join $context.infra)
}
if ($env.PROVISIONING_INFRA_PATH | path join $context.infra | path exists) {
return ($env.PROVISIONING_INFRA_PATH | path join $context.infra)
}
""
}
export def get_infra [
infra?: string
]: nothing -> string {
if ($infra | is-not-empty) {
if ($infra | path exists) {
$infra
} else if ($infra | path join $env.PROVISIONING_DFLT_SET | path exists) {
$infra
} else if ($env.PROVISIONING_INFRA_PATH | path join $infra | path join $env.PROVISIONING_DFLT_SET | path exists) {
$env.PROVISIONING_INFRA_PATH | path join $infra
} else {
let text = $"($infra) on ($env.PROVISIONING_INFRA_PATH | path join $infra)"
(throw-error "🛑 Path not found " $text "get_infra" --span (metadata $infra).span)
}
} else {
if ($env.PWD | path join $env.PROVISIONING_DFLT_SET | path exists) {
$env.PWD
} else if ($env.PROVISIONING_INFRA_PATH | path join ($env.PWD | path basename) |
path join $env.PROVISIONING_DFLT_SET | path exists) {
$env.PROVISIONING_INFRA_PATH | path join ($env.PWD | path basename)
} else {
let context_path = get_context_infra_path
if $context_path != "" { return $context_path }
$env.PROVISIONING_KLOUD_PATH
}
}
}
export def parse_kcl_file [
src: string
target: string
append: bool
msg: string
err_exit?: bool = false
]: nothing -> bool {
# Try nu_plugin_kcl first if available
let format = if $env.PROVISIONING_WK_FORMAT == "json" { "json" } else { "yaml" }
let result = (process_kcl_file $src $format)
if ($result | is-empty) {
let text = $"kcl ($src) failed code ($result.exit_code)"
(throw-error $msg $text "parse_kcl_file" --span (metadata $result).span)
if $err_exit { exit $result.exit_code }
return false
}
if $append {
$result | save --append $target
} else {
$result | save -f $target
}
true
}
export def load_from_wk_format [
src: string
]: nothing -> record {
if not ( $src | path exists) { return {} }
let data_raw = (open -r $src)
if $env.PROVISIONING_WK_FORMAT == "json" {
$data_raw | from json | default {}
} else {
$data_raw | from yaml | default {}
}
}
export def load_defaults [
src_path: string
item_path: string
target_path: string
]: nothing -> string {
if ($target_path | path exists) {
if (is_sops_file $target_path) { decode_sops_file $src_path $target_path true }
retrurn
}
let full_path = if ($item_path | path exists) {
($item_path)
} else if ($"($item_path).k" | path exists) {
$"($item_path).k"
} else if ($src_path | path dirname | path join $"($item_path).k" | path exists) {
$src_path | path dirname | path join $"($item_path).k"
} else {
""
}
if $full_path == "" { return true }
if (is_sops_file $full_path) {
decode_sops_file $full_path $target_path true
(parse_kcl_file $target_path $target_path false $"🛑 load default settings failed ($target_path) ")
} else {
(parse_kcl_file $full_path $target_path false $"🛑 load default settings failed ($full_path)")
}
}
export def get_provider_env [
settings: record
server: record
]: nothing -> record {
let prov_env_path = if ($server.prov_settings | path exists ) {
$server.prov_settings
} else {
let file_path = ($settings.src_path | path join $server.prov_settings)
if ($file_path | str ends-with '.k' ) { $file_path } else { $"($file_path).k" }
}
if not ($prov_env_path| path exists ) {
if $env.PROVISIONING_DEBUG { _print $"🛑 load (_ansi cyan_bold)provider_env(_ansi reset) from ($server.prov_settings) failed at ($prov_env_path)" }
return {}
}
let str_created_taskservs_dirpath = ($settings.data.created_taskservs_dirpath | default "/tmp" |
str replace "\~" $env.HOME | str replace "NOW" $env.NOW | str replace "./" $"($settings.src_path)/")
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 }
if not ( $created_taskservs_dirpath | path exists) { ^mkdir -p $created_taskservs_dirpath }
let source_settings_path = ($created_taskservs_dirpath | path join $"($prov_env_path | path basename)")
let target_settings_path = ($created_taskservs_dirpath| path join $"($prov_env_path | path basename | str replace '.k' '').($env.PROVISIONING_WK_FORMAT)")
let res = if (is_sops_file $prov_env_path) {
decode_sops_file $prov_env_path $source_settings_path true
(parse_kcl_file $source_settings_path $target_settings_path false $"🛑 load prov settings failed ($target_settings_path)")
} else {
cp $prov_env_path $source_settings_path
(parse_kcl_file $source_settings_path $target_settings_path false $"🛑 load prov settings failed ($prov_env_path)")
}
if not $env.PROVISIONING_DEBUG { rm -f $source_settings_path }
if $res and ($target_settings_path | path exists) {
let data = (open $target_settings_path)
if not $env.PROVISIONING_DEBUG { rm -f $target_settings_path }
$data
} else {
{}
}
}
export def get_file_format [
filename: string
]: nothing -> string {
if ($filename | str ends-with ".json") {
"json"
} else if ($filename | str ends-with ".yaml") {
"yaml"
} else {
$env.PROVISIONING_WK_FORMAT
}
}
export def save_provider_env [
data: record
settings: record
provider_path: string
]: nothing -> nothing {
if ($provider_path | is-empty) or not ($provider_path | path dirname |path exists) {
_print $"❗ Can not save provider env for (_ansi blue)($provider_path | path dirname)(_ansi reset) in (_ansi red)($provider_path)(_ansi reset )"
return
}
if (get_file_format $provider_path) == "json" {
$"data: ($data | to json | encode base64)" | save --force $provider_path
} else {
$"data: ($data | to yaml | encode base64)" | save --force $provider_path
}
let result = (on_sops "encrypt" $provider_path --quiet)
if ($result | is-not-empty) {
($result | save --force $provider_path)
}
}
export def get_provider_data_path [
settings: record
server: record
]: nothing -> string {
let data_path = if ($settings.data.prov_data_dirpath | str starts-with "." ) {
($settings.src_path | path join $settings.data.prov_data_dirpath)
} else {
$settings.data.prov_data_dirpath
}
if not ($data_path | path exists) { ^mkdir -p $data_path }
($data_path | path join $"($server.provider)_cache.($env.PROVISIONING_WK_FORMAT)")
}
export def load_provider_env [
settings: record
server: record
provider_path: string = ""
]: nothing -> record {
let data = if ($provider_path | is-not-empty) and ($provider_path |path exists) {
let file_data = if (is_sops_file $provider_path) {
on_sops "decrypt" $provider_path --quiet
let result = (on_sops "decrypt" $provider_path --quiet)
# --character-set binhex
if (get_file_format $provider_path) == "json" {
($result | from json | get -o data | default "" | decode base64 | decode | from json)
} else {
($result | from yaml | get -o data | default "" | decode base64 | decode | from yaml)
}
} else {
open $provider_path
}
if ($file_data | is-empty) or ($file_data | get -o main | get -o vpc) == "?" {
# (throw-error $"load provider ($server.provider) settings failed" $"($provider_path) no main data"
# "load_provider_env" --span (metadata $data).span)
if $env.PROVISIONING_DEBUG { _print $"load provider ($server.provider) settings failed ($provider_path) no main data in load_provider_env" }
{}
} else {
$file_data
}
} else {
{}
}
if ($data | is-empty) {
let new_data = (get_provider_env $settings $server)
if ($new_data | is-not-empty) and ($provider_path | is-not-empty) { save_provider_env $new_data $settings $provider_path }
$new_data
} else {
$data
}
}
export def load_provider_settings [
settings: record
server: record
]: nothing -> record {
let data_path = if ($settings.data.prov_data_dirpath | str starts-with "." ) {
($settings.src_path | path join $settings.data.prov_data_dirpath)
} else { $settings.data.prov_data_dirpath }
if ($data_path | is-empty) {
(throw-error $"load provider ($server.provider) settings failed" $"($settings.data.prov_data_dirpath)"
"load_provider_settings" --span (metadata $data_path).span)
}
if not ($data_path | path exists) { ^mkdir -p $data_path }
let provider_path = ($data_path | path join $"($server.provider)_cache.($env.PROVISIONING_WK_FORMAT)")
let data = (load_provider_env $settings $server $provider_path)
if ($data | is-empty) or ($data | get -o main | get -o vpc) == "?" {
mw_create_cache $settings $server false
(load_provider_env $settings $server $provider_path)
} else {
$data
}
}
export def load [
infra?: string
in_src?: string
include_notuse?: bool = false
--no_error
]: nothing -> record {
let source = if $in_src == null or ($in_src | str ends-with '.k' ) { $in_src } else { $"($in_src).k" }
let source_path = if $source != null and ($source | path type) == "dir" { $"($source)/($env.PROVISIONING_DFLT_SET)" } else { $source }
let src_path = if $source_path != null and ($source_path | path exists) {
$"./($source_path)"
} else if $source_path != null and ($source_path | str ends-with $env.PROVISIONING_DFLT_SET) == false {
if $no_error {
return {}
} else {
(throw-error "🛑 invalid settings infra / path " $"file ($source) settings in ($infra)" "settings->load" --span (metadata $source).span)
}
} else if ($infra | is-empty) and ($env.PROVISIONING_DFLT_SET| is-not-empty ) and ($env.PROVISIONING_DFLT_SET | path exists) {
$"./($env.PROVISIONING_DFLT_SET)"
} else if ($infra | path join $env.PROVISIONING_DFLT_SET | path exists) {
$infra | path join $env.PROVISIONING_DFLT_SET
} else {
if $no_error {
return {}
} else {
(throw-error "🛑 invalid settings infra / path " $"file ($source) settings in ($infra)" "settings->load" --span (metadata $source_path).span)
}
}
let src_dir = ($src_path | path dirname)
let infra_path = if $src_dir == "." {
$env.PWD
} else if ($src_dir | is-empty) {
$env.PWD | path join $infra
} else if ($src_dir | path exists ) and ( $src_dir | str starts-with "/") {
$src_dir
} else {
$env.PWD | path join $src_dir
}
let wk_settings_path = mktemp -d
if not (parse_kcl_file $"($src_path)" $"($wk_settings_path)/settings.($env.PROVISIONING_WK_FORMAT)" false "🛑 load settings failed ") { return }
if $env.PROVISIONING_DEBUG { _print $"DEBUG source path: ($src_path)" }
let settings_data = open $"($wk_settings_path)/settings.($env.PROVISIONING_WK_FORMAT)"
if $env.PROVISIONING_DEBUG { _print $"DEBUG work path: ($wk_settings_path)" }
let servers_paths = ($settings_data | get -o servers_paths | default [])
# Set full path for provider data
let data_fullpath = if ($settings_data.prov_data_dirpath | str starts-with "." ) {
($src_dir | path join $settings_data.prov_data_dirpath)
} else { $settings_data.prov_data_dirpath }
mut list_servers = []
mut providers_settings = []
for it in $servers_paths {
let file_path = if ($it | str ends-with ".k") {
$it
} else {
$"($it).k"
}
let server_path = if ($file_path | str starts-with "/") {
$file_path
} else {
($src_path | path dirname | path join $file_path)
}
if not ($server_path | path exists) {
if $no_error {
"" | save $server_path
} else {
(throw-error "🛑 server path not found " ($server_path) "load each on list_servers" --span (metadata $servers_paths).span)
}
}
let target_settings_path = $"($wk_settings_path)/($it | str replace --all "/" "_").($env.PROVISIONING_WK_FORMAT)"
if not (parse_kcl_file ($server_path | path join $server_path) $target_settings_path false "🛑 load settings failed ") { return }
#if not (parse_kcl_file $server_path $target_settings_path false "🛑 load settings failed ") { return }
if not ( $target_settings_path | path exists) { continue }
let servers_defs = (open $target_settings_path | default {})
for srvr in ($servers_defs | get -o servers | default []) {
if not $include_notuse and $srvr.not_use { continue }
let provider = $srvr.provider
if not ($"($wk_settings_path)/($provider)($settings_data.defaults_provs_suffix).($env.PROVISIONING_WK_FORMAT)" | path exists ) {
let dflt_item = ($settings_data.defaults_provs_dirpath | path join $"($provider)($settings_data.defaults_provs_suffix)")
let dflt_item_fullpath = if ($dflt_item | str starts-with "." ) {
($src_dir | path join $dflt_item)
} else { $dflt_item }
load_defaults $src_path $dflt_item_fullpath ($wk_settings_path | path join $"($provider)($settings_data.defaults_provs_suffix).($env.PROVISIONING_WK_FORMAT)")
}
# Loading defaults provider ...
let server_with_dflts = if ($"($wk_settings_path)/($provider)($settings_data.defaults_provs_suffix).($env.PROVISIONING_WK_FORMAT)" | path exists ) {
open ($"($wk_settings_path)/($provider)($settings_data.defaults_provs_suffix).($env.PROVISIONING_WK_FORMAT)") | merge $srvr
} else { $srvr }
# Loading provider data settings
let server_prov_data = if ($data_fullpath | path join $"($provider)($settings_data.prov_data_suffix)" | path exists) {
(load_defaults $src_dir ($data_fullpath | path join $"($provider)($settings_data.prov_data_suffix)")
($wk_settings_path | path join $"($provider)($settings_data.prov_data_suffix)")
)
if (($wk_settings_path | path join $"($provider)($settings_data.prov_data_suffix)") | path exists) {
$server_with_dflts | merge (load_from_wk_format ($wk_settings_path | path join $"($provider)($settings_data.prov_data_suffix)"))
} else { $server_with_dflts }
} else { $server_with_dflts }
# Loading provider data settings
let server_with_data = if ($data_fullpath | path join $"($srvr.hostname)_($provider)($settings_data.prov_data_suffix)" | path exists) {
(load_defaults $src_dir ($data_fullpath | path join $"($srvr.hostname)_($provider)($settings_data.prov_data_suffix)")
($wk_settings_path | path join $"($srvr.hostname)_($provider)($settings_data.prov_data_suffix)")
)
if ($wk_settings_path | path join $"($srvr.hostname)_($provider)($settings_data.prov_data_suffix)" | path exists) {
$server_prov_data | merge (load_from_wk_format ($wk_settings_path | path join $"($srvr.hostname)_($provider)($settings_data.prov_data_suffix)"))
} else { $server_prov_data }
} else { $server_prov_data }
$list_servers = ($list_servers | append $server_with_data)
if ($providers_settings | where {|it| $it.provider == $provider} | length) == 0 {
$providers_settings = ($providers_settings | append {
provider: $provider,
settings: (load_provider_settings {
data: $settings_data,
providers: $providers_settings,
src: ($src_path | path basename),
src_path: ($src_path | path dirname),
infra: ($infra_path | path basename),
infra_path: ($infra_path |path dirname),
wk_path: $wk_settings_path
}
$server_with_data)
}
)
}
}
}
#{ settings: $settings_data, servers: ($list_servers | flatten) }
# | to ($env.PROVISIONING_WK_FORMAT) | save --append $"($wk_settings_path)/settings.($env.PROVISIONING_WK_FORMAT)"
# let servers_settings = { servers: ($list_servers | flatten) }
let servers_settings = { servers: $list_servers }
if $env.PROVISIONING_WK_FORMAT == "json" {
#$servers_settings | to json | save --append $"($wk_settings_path)/settings.($env.PROVISIONING_WK_FORMAT)"
$servers_settings | to json | save --force $"($wk_settings_path)/servers.($env.PROVISIONING_WK_FORMAT)"
} else {
#$servers_settings | to yaml | save --append $"($wk_settings_path)/settings.($env.PROVISIONING_WK_FORMAT)"
$servers_settings | to yaml | save --force $"($wk_settings_path)/servers.($env.PROVISIONING_WK_FORMAT)"
}
#let $settings_data = (open $"($wk_settings_path)/settings.($env.PROVISIONING_WK_FORMAT)")
let $settings_data = ($settings_data | merge $servers_settings )
{
data: $settings_data,
providers: $providers_settings,
src: ($src_path | path basename),
src_path: ($src_path | path dirname),
infra: ($infra_path | path basename),
infra_path: ($infra_path |path dirname),
wk_path: $wk_settings_path
}
}
export def load_settings [
--infra (-i): string
--settings (-s): string # Settings path
include_notuse: bool = false
no_error: bool = false
]: nothing -> record {
let kld = get_infra (if $infra == null { "" } else { $infra })
if $no_error {
(load $kld $settings $include_notuse --no_error)
} else {
(load $kld $settings $include_notuse)
}
# let settings = (load $kld $settings $exclude_not_use)
# if $env.PROVISIONING_USE_SOPS? != "" {
# use sops/lib.nu check_sops
# check_sops $settings.src_path
# }
# $settings
}
export def save_settings_file [
settings: record
target_file: string
match_text: string
new_text: string
mark_changes: bool = false
]: nothing -> nothing {
let it_path = if ($target_file | path exists) {
$target_file
} else if ($settings.src_path | path join $"($target_file).k" | path exists) {
($settings.src_path | path join $"($target_file).k")
} else if ($settings.src_path | path join $"($target_file).($env.PROVISIONING_WK_FORMAT)" | path exists) {
($settings.src_path | path join $"($target_file).($env.PROVISIONING_WK_FORMAT)")
} else {
_print $"($target_file) not found in ($settings.src_path)"
return false
}
if (is_sops_file $it_path) {
let result = (on_sops "decrypt" $it_path --quiet)
if ($result | is-empty) {
(throw-error $"🛑 saving settings to ($it_path)"
$"from ($match_text) to ($new_text)"
$"in ($target_file)" --span (metadata $it_path).span)
return false
} else {
$result | str replace $match_text $new_text| save --force $it_path
let en_result = (on_sops "encrypt" $it_path --quiet)
if ($en_result | is-not-empty) {
($en_result | save --force $it_path)
}
}
} else {
open $it_path --raw | str replace $match_text $new_text | save --force $it_path
}
#if $it_path != "" and (^grep -q $match_text $it_path | complete).exit_code == 0 {
# if (^sed -i $"s/($match_text)/($match_text)\"($new_text)\"/g" $it_path | complete).exit_code == 0 {
_print $"($target_file) saved with new value "
if $mark_changes {
if ($settings.wk_path | path join "changes" | path exists) == false {
$"($it_path) has been changed" | save ($settings.wk_path | path join "changes") --append
}
} else if ($env.PROVISIONING_MODULE | is-not-empty) {
^($env.PROVISIONING_NAME) "-mod" $env.PROVISIONING_MODULE $env.PROVISIONING_ARGS
exit
}
# }
#}
}
export def save_servers_settings [
settings: record
match_text: string
new_text: string
]: nothing -> nothing {
$settings.data.servers_paths | each { | it |
save_settings_file $settings $it $match_text $new_text
}
}
export def settings_with_env [
settings: record
] {
mut $servers_with_ips = []
for srv in ($settings.data.servers) {
let pub_ip = (mw_ip_from_cache $settings $srv false)
if ($pub_ip | is-empty) {
$servers_with_ips = ($servers_with_ips | append ($srv))
} else {
$servers_with_ips = ($servers_with_ips | append ($srv | merge { network_public_ip: $pub_ip }))
}
}
($settings | merge { data: ($settings.data | merge { servers: $servers_with_ips}) })
}

View file

@ -0,0 +1,54 @@
# Simple validation functions for provisioning tool
export def check-required [
value: any
name: string
]: bool {
if ($value | is-empty) {
print $"🛑 Required parameter '($name)' is missing or empty"
return false
}
true
}
export def check-path [
path: string
]: bool {
if ($path | is-empty) {
print "🛑 Path parameter is empty"
return false
}
true
}
export def check-path-exists [
path: string
]: bool {
if not ($path | path exists) {
print $"🛑 Path '($path)' does not exist"
return false
}
true
}
export def check-command [
command: string
]: bool {
let result = (^bash -c $"type -P ($command)" | complete)
if $result.exit_code != 0 {
print $"🛑 Command '($command)' not found in PATH"
return false
}
true
}
export def safe-run [
command: closure
context: string
]: any {
try {
do $command
} catch {|err|
print $"⚠️ Warning: Error in ($context): ($err.msg)"
}
}

View file

@ -0,0 +1,141 @@
export def ssh_cmd [
settings: record
server: record
with_bash: bool
cmd: string
live_ip: string
] {
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)
}
if $ip == "" { return false }
if not (check_connection $server $ip "ssh_cmd") { return false }
let remote_cmd = if $with_bash {
let ops = if $env.PROVISIONING_DEBUG { "-x" } else { "" }
$"bash ($ops) ($cmd)"
} else { $cmd }
let ssh_loglevel = if $env.PROVISIONING_DEBUG {
_print $"Run ($remote_cmd) in ($server.installer_user)@($ip)"
"-o LogLevel=info"
} else {
"-o LogLevel=quiet"
}
let res = (^ssh "-o" ($env.SSH_OPS | get -o 0) "-o" ($env.SSH_OPS | get -o 1) "-o" IdentitiesOnly=yes $ssh_loglevel
"-i" ($server.ssh_key_path | str replace ".pub" "")
$"($server.installer_user)@($ip)" ($remote_cmd) | complete)
if $res.exit_code != 0 {
_print $"❗ run ($remote_cmd) in ($server.hostname) errors ($res.stdout ) "
return false
}
if $env.PROVISIONING_DEBUG and $remote_cmd != "ls" { _print $res.stdout }
true
}
export def scp_to [
settings: record
server: record
source: list<string>
target: string
live_ip: string
] {
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)
}
if $ip == "" { return false }
if not (check_connection $server $ip "scp_to") { return false }
let source_files = ($source | str join " ")
let ssh_loglevel = if $env.PROVISIONING_DEBUG {
_print $"Sending ($source | str join ' ') to ($server.installer_user)@($ip)/tmp/($target)"
_print $"scp -o ($env.SSH_OPS | get -o 0) -o ($env.SSH_OPS | get -o 1) -o IdentitiesOnly=yes -i ($server.ssh_key_path | str replace ".pub" "") ($source_files) ($server.installer_user)@($ip):($target)"
"-o LogLevel=info"
} else {
"-o LogLevel=quiet"
}
let res = (^scp "-o" ($env.SSH_OPS | get -o 0) "-o" ($env.SSH_OPS | get -o 1) "-o" IdentitiesOnly=yes $ssh_loglevel
"-i" ($server.ssh_key_path | str replace ".pub" "")
$source_files $"($server.installer_user)@($ip):($target)" | complete)
if $res.exit_code != 0 {
_print $"❗ copy ($target | str join ' ') to ($server.hostname) errors ($res.stdout ) "
return false
}
if $env.PROVISIONING_DEBUG { _print $res.stdout }
true
}
export def scp_from [
settings: record
server: record
source: string
target: string
live_ip: string
] {
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)
}
if $ip == "" { return false }
if not (check_connection $server $ip "scp_from") { return false }
let ssh_loglevel = if $env.PROVISIONING_DEBUG {
_print $"Getting ($target | str join ' ') from ($server.installer_user)@($ip)/tmp/($target)"
"-o LogLevel=info"
} else {
"-o LogLevel=quiet"
}
let res = (^scp "-o" ($env.SSH_OPS | get -o 0) "-o" ($env.SSH_OPS | get -o 1) "-o" IdentitiesOnly=yes $ssh_loglevel
"-i" ($server.ssh_key_path | str replace ".pub" "")
$"($server.installer_user)@($ip):($source)" $target | complete)
if $res.exit_code != 0 {
_print $"❗ copy ($source) from ($server.hostname) to ($target) errors ($res.stdout ) "
return false
}
if $env.PROVISIONING_DEBUG { _print $res.stdout }
true
}
export def ssh_cp_run [
settings: record
server: record
source: list<string>
target: string
with_bash: bool
live_ip: string
ssh_remove: bool
] {
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)
}
if $ip == "" {
_print $"❗ ssh_cp_run (_ansi red_bold)No IP(_ansi reset) to (_ansi green_bold)($server.hostname)(_ansi reset)"
return false
}
if not (scp_to $settings $server $source $target $ip) { return false }
if not (ssh_cmd $settings $server $with_bash $target $ip) { return false }
if $env.PROVISIONING_SSH_DEBUG? != null and $env.PROVISIONING_SSH_DEBUG { return true }
if $ssh_remove {
return (ssh_cmd $settings $server false $"rm -f ($target)" $ip)
}
true
}
export def check_connection [
server: record
ip: string
origin: string
] {
if not (port_scan $ip $server.liveness_port 1) {
_print (
$"\n🛑 (_ansi red)Error connection(_ansi reset) ($origin) (_ansi blue)($server.hostname)(_ansi reset) " +
$"(_ansi blue_bold)($ip)(_ansi reset) at ($server.liveness_port) (_ansi red_bold)failed(_ansi reset) "
)
return false
}
true
}

View file

@ -0,0 +1,168 @@
export def run_from_template [
template_path: string # Template path
vars_path: string # Variable file with settings for template
run_file: string # File to run
out_file?: string # Out file path
--check_mode # Use check mode to review and not create server
--only_make # not run
] {
# Check if nu_plugin_tera is available
if not $env.PROVISIONING_USE_TERA_PLUGIN {
_print $"🛑 (_ansi red)Error(_ansi reset) nu_plugin_tera not available - template rendering not supported"
return false
}
if not ( $template_path | path exists ) {
_print $"🛑 (_ansi red)Error(_ansi reset) template ($template_path) (_ansi red)not found(_ansi reset)"
return false
}
if not ( $vars_path | path exists ) {
_print $"🛑 (_ansi red)Error(_ansi reset) vars file ($vars_path) (_ansi red)not found(_ansi reset)"
return false
}
let out_file_name = ($out_file | default "")
# Debug: Show what file we're trying to open
if $env.PROVISIONING_DEBUG {
_print $"🔍 Template vars file: ($vars_path)"
if ($vars_path | path exists) {
_print "📄 File preview (first 3 lines):"
_print (open $vars_path --raw | lines | take 3 | str join "\n")
} else {
_print $"❌ File does not exist!"
}
}
# Load variables from YAML/JSON file
let vars = if ($vars_path | path exists) {
if $env.PROVISIONING_DEBUG {
_print $"🔍 Parsing YAML configuration: ($vars_path)"
}
# Check for common YAML syntax issues before attempting to parse
let content = (open $vars_path --raw)
let unquoted_vars = ($content | lines | enumerate | where {|line| $line.item =~ '\s+\w+:\s+\$\w+'})
if ($unquoted_vars | length) > 0 {
_print ""
_print $"🛑 (_ansi red_bold)INFRASTRUCTURE CONFIGURATION ERROR(_ansi reset)"
_print $"📄 Failed to parse YAML variables file: (_ansi yellow)($vars_path | path basename)(_ansi reset)"
_print ""
_print $"(_ansi blue_bold)Diagnosis:(_ansi reset)"
_print "• Found unquoted variable references (invalid YAML syntax):"
for $var in $unquoted_vars {
let line_num = ($var.index + 1)
let line_content = ($var.item | str trim)
_print $" Line ($line_num): (_ansi red)($line_content)(_ansi reset)"
}
_print ""
_print $"(_ansi blue_bold)Root Cause:(_ansi reset)"
_print $"KCL-to-YAML conversion is not properly handling string variables."
# Extract variable names from the problematic lines
let sample_vars = ($unquoted_vars | take 3 | each {|line|
($line.item | str trim | split row " " | last)
} | str join ", ")
if ($sample_vars | is-not-empty) {
_print $"Example variables: ($sample_vars) should be quoted or resolved."
} else {
_print "String variables should be quoted or resolved during conversion."
}
_print ""
_print $"(_ansi blue_bold)Fix Required:(_ansi reset)"
_print $"1. Check KCL configuration generation process"
_print $"2. Ensure variables are properly quoted or resolved during YAML generation"
_print $"3. Source KCL files appear correct, issue is in conversion step"
_print ""
_print $"(_ansi blue_bold)Infrastructure file:(_ansi reset) ($vars_path)"
exit 1
}
# If no obvious issues found, attempt to parse YAML
open $vars_path
} else {
_print $"❌ Variables file not found: ($vars_path)"
return false
}
# Use nu_plugin_tera for template rendering
let result = (render_template $template_path $vars)
# let result = if $result.exit_code == 0 {
# {exit_code: 0, stdout: $result.stdout, stderr: ""}
# } else {
# {exit_code: 1, stdout: "", stderr: $"Template rendering failed for ($template_path)"}
# }
#if $result.exit_code != 0 {
if ($result | is-empty) {
let text = $"(_ansi yellow)template(_ansi reset): ($template_path)\n(_ansi yellow)vars(_ansi reset): ($vars_path)\n(_ansi red)Failed(_ansi reset)"
print $result
print $"(_ansi red)ERROR(_ansi red) nu_plugin_tera render:\n($text)"
exit
}
if not $only_make and $env.PROVISIONING_DEBUG or ($check_mode and ($out_file_name | is-empty)) {
if $env.PROVISIONING_DEBUG and not $check_mode {
_print $"Result running: \n (_ansi default_dimmed)nu_plugin_tera render ($template_path) ($vars_path)(_ansi reset)"
# _print $"\n(_ansi yellow_bold)exit code: ($result.exit_code)(_ansi reset)"
}
let cmd = ($env| get -o PROVISIONING_FILEVIEWER | default (if (^bash -c "type -P bat" | is-not-empty) { "bat" } else { "cat" }))
if $cmd != "bat" { _print $"(_ansi magenta_bold)----------------------------------------------------------------------------------------------------------------(_ansi reset)"}
(echo $result | run-external $cmd -)
if $cmd != "bat" { _print $"(_ansi magenta_bold)----------------------------------------------------------------------------------------------------------------(_ansi reset)"}
_print $"Saved in (_ansi green_bold)($run_file)(_ansi reset)"
}
$result | str replace --all "\\ " "\\" | save --append $run_file
if $only_make {
if ($out_file_name | is-not-empty) {
(cat $run_file | tee { save -f $out_file_name } | ignore)
}
return true
}
if $check_mode and not $only_make {
if $out_file_name == "" {
_print $"✅ No errors found !\nTo save command to a file, run next time adding: (_ansi blue)--outfile \(-o\)(_ansi reset) file-path-to-save "
} else {
(cat $run_file | tee { save -f $out_file_name } | ignore)
_print $"✅ No errors found !\nSave in (_ansi green_bold)(_ansi i)($out_file_name)(_ansi reset)"
}
return true
}
if $out_file_name != "" and ($out_file_name | path type) == "file" {
(^bash $run_file | save --force $out_file_name)
} else {
let res = if $env.PROVISIONING_DEBUG {
(^bash -x $run_file | complete)
} else {
(^bash $run_file | complete)
}
if $res.exit_code != 0 {
_print $"\n🛑 (_ansi red)Error(_ansi reset) run from template ($template_path | path basename) (_ansi green_bold)($run_file)(_ansi reset) (_ansi red_bold)failed(_ansi reset) "
_print $"\n($res.stdout)"
return false
}
}
true
}
export def on_template_path [
source_path: string
vars_path: string
remove_path: bool
on_error_exit: bool
] {
for it in (^ls ...(glob $"($source_path)/*")| lines) {
let item = ($it | str trim | str replace -r ':$' '')
if ($item | is-empty) or ($item | path basename | str starts-with "tmp.") or ($item | path basename | str starts-with "_") { continue }
if ($item | path type) == "dir" {
if (ls $item | length) == 0 { continue }
(on_template_path $item $vars_path $remove_path $on_error_exit)
continue
}
if not ($item | str ends-with ".j2") or not ($item | path exists) { continue }
if not (run_from_template $item $vars_path ($item | str replace ".j2" "") --only_make) {
echo $"🛑 Error on_template_path (_ansi red_bold)($item)(_ansi reset) and vars (_ansi yellow_bold)($vars_path)(_ansi reset)"
if $on_error_exit { exit 1 }
}
if $remove_path { rm -f $item }
}
}

View file

@ -0,0 +1,9 @@
export def on_test [] {
use nupm/
cd $"($env.PROVISIONING)/core/nulib"
nupm test test_addition
cd $env.PWD
nupm test basecamp_addition
}

View file

@ -0,0 +1,11 @@
# Exclude minor or specific parts for global 'export use'
export use clean.nu *
export use error.nu *
export use help.nu *
export use interface.nu *
export use undefined.nu *

View file

@ -0,0 +1,25 @@
export def option_undefined [
root: string
src: string
info?: string
] {
_print $"🛑 invalid_option ($src) ($info)"
_print $"\nUse (_ansi blue_bold)($env.PROVISIONING_NAME) ($root) ($src) help(_ansi reset) for help on commands and options"
}
export def invalid_task [
src: string
task: string
--end
] {
let show_src = {|color|
if $src == "" { "" } else { $" (_ansi $color)($src)(_ansi reset)"}
}
if $task != "" {
_print $"🛑 invalid (_ansi blue)($env.PROVISIONING_NAME)(_ansi reset)(do $show_src "yellow") task or option: (_ansi red)($task)(_ansi reset)"
} else {
_print $"(_ansi blue)($env.PROVISIONING_NAME)(_ansi reset)(do $show_src "yellow") no task or option found !"
}
_print $"Use (_ansi blue_bold)($env.PROVISIONING_NAME)(_ansi reset)(do $show_src "blue_bold") (_ansi blue_bold)help(_ansi reset) for help on commands and options"
if $end and not $env.PROVISIONING_DEBUG { end_run "" }
}

View file

@ -0,0 +1,93 @@
# Enhanced validation utilities for provisioning tool
export def validate-required [
value: any
name: string
context?: string
]: bool {
if ($value | is-empty) {
print $"🛑 Required parameter '($name)' is missing or empty"
if ($context | is-not-empty) {
print $"Context: ($context)"
}
print $"💡 Please provide a value for '($name)'"
return false
}
true
}
export def validate-path [
path: string
context?: string
--must-exist
]: bool {
if ($path | is-empty) {
print "🛑 Path parameter is empty"
if ($context | is-not-empty) {
print $"Context: ($context)"
}
return false
}
if $must_exist and not ($path | path exists) {
print $"🛑 Path '($path)' does not exist"
if ($context | is-not-empty) {
print $"Context: ($context)"
}
print "💡 Check if the path exists and you have proper permissions"
return false
}
true
}
export def validate-command [
command: string
context?: string
]: bool {
let cmd_exists = (^bash -c $"type -P ($command)" | complete)
if $cmd_exists.exit_code != 0 {
print $"🛑 Command '($command)' not found in PATH"
if ($context | is-not-empty) {
print $"Context: ($context)"
}
print $"💡 Install '($command)' or add it to your PATH"
return false
}
true
}
export def safe-execute [
command: closure
context: string
--fallback: closure
]: any {
try {
do $command
} catch {|err|
print $"⚠️ Warning: Error in ($context): ($err.msg)"
if $fallback != null {
print "🔄 Executing fallback..."
do $fallback
} else {
print $"🛑 Execution failed in ($context)"
print $"Error: ($err.msg)"
}
}
}
export def validate-settings [
settings: record
required_fields: list
]: bool {
let missing_fields = ($required_fields | where {|field|
($settings | get -o $field | is-empty)
})
if ($missing_fields | length) > 0 {
print "🛑 Missing required settings fields:"
$missing_fields | each {|field| print $" - ($field)"}
return false
}
true
}

View file

@ -0,0 +1,121 @@
# Validation helper functions for provisioning tool
export def validate-required [
value: any
name: string
context?: string
]: bool {
if ($value | is-empty) {
print $"🛑 Required parameter '($name)' is missing or empty"
if ($context | is-not-empty) {
print $"Context: ($context)"
}
print $"💡 Please provide a value for '($name)'"
return false
}
true
}
export def validate-path [
path: string
context?: string
--must-exist
]: bool {
if ($path | is-empty) {
print "🛑 Path parameter is empty"
if ($context | is-not-empty) {
print $"Context: ($context)"
}
return false
}
if $must_exist and not ($path | path exists) {
print $"🛑 Path '($path)' does not exist"
if ($context | is-not-empty) {
print $"Context: ($context)"
}
print "💡 Check if the path exists and you have proper permissions"
return false
}
true
}
export def validate-command [
command: string
context?: string
]: bool {
let cmd_exists = (^bash -c $"type -P ($command)" | complete)
if $cmd_exists.exit_code != 0 {
print $"🛑 Command '($command)' not found in PATH"
if ($context | is-not-empty) {
print $"Context: ($context)"
}
print $"💡 Install '($command)' or add it to your PATH"
return false
}
true
}
export def validate-ip [
ip: string
context?: string
]: bool {
let ip_parts = ($ip | split row ".")
if ($ip_parts | length) != 4 {
print $"🛑 Invalid IP address format: ($ip)"
if ($context | is-not-empty) {
print $"Context: ($context)"
}
return false
}
let valid_parts = ($ip_parts | each {|part|
let num = ($part | into int)
$num >= 0 and $num <= 255
})
if not ($valid_parts | all {|valid| $valid}) {
print $"🛑 Invalid IP address values: ($ip)"
if ($context | is-not-empty) {
print $"Context: ($context)"
}
return false
}
true
}
export def validate-port [
port: int
context?: string
]: bool {
if $port < 1 or $port > 65535 {
print $"🛑 Invalid port number: ($port). Must be between 1 and 65535"
if ($context | is-not-empty) {
print $"Context: ($context)"
}
return false
}
true
}
export def validate-settings [
settings: record
required_fields: list
context?: string
]: bool {
let missing_fields = ($required_fields | where {|field|
($settings | get -o $field | is-empty)
})
if ($missing_fields | length) > 0 {
print "🛑 Missing required settings fields:"
$missing_fields | each {|field| print $" - ($field)"}
if ($context | is-not-empty) {
print $"Context: ($context)"
}
return false
}
true
}

View file

@ -0,0 +1,285 @@
#!/usr/bin/env nu
# Agnostic Version Management Core
# No hardcoded tools or specific implementations
# use ../utils/error.nu *
# use ../utils/format.nu *
# Generic version record schema
export def version-schema []: nothing -> record {
{
id: "" # Unique identifier
type: "" # Component type (tool/provider/taskserv/cluster)
version: "" # Current version
fixed: false # Version pinning
source: {} # Source configuration
detector: {} # Detection configuration
updater: {} # Update configuration
metadata: {} # Any additional data
}
}
# Generic version operations interface
export def version-operations []: nothing -> record {
{
detect: { |config| "" } # Detect installed version
fetch: { |config| "" } # Fetch available versions
compare: { |v1, v2| 0 } # Compare versions
update: { |config, version| {} } # Update to version
}
}
# Version comparison (works with semantic and non-semantic versions)
export def compare-versions [
v1: string
v2: string
--strategy: string = "semantic" # semantic, string, numeric, custom
]: nothing -> int {
if $v1 == $v2 { return 0 }
if ($v1 | is-empty) { return (-1) }
if ($v2 | is-empty) { return 1 }
match $strategy {
"semantic" => {
# Try semantic versioning
let parts1 = ($v1 | split row "." | each { |p|
($p | str trim | into int) | default 0
})
let parts2 = ($v2 | split row "." | each { |p|
($p | str trim | into int) | default 0
})
let max_len = ([$parts1 $parts2] | each { |it| $it | length } | math max)
for i in 0..<$max_len {
let p1 = ($parts1 | get -o $i | default 0)
let p2 = ($parts2 | get -o $i | default 0)
if $p1 < $p2 { return (-1) }
if $p1 > $p2 { return 1 }
}
0
}
"string" => {
# Simple string comparison
if $v1 < $v2 { (-1) } else if $v1 > $v2 { 1 } else { 0 }
}
"numeric" => {
# Numeric comparison (for build numbers)
let n1 = ($v1 | into float | default 0)
let n2 = ($v2 | into float | default 0)
if $n1 < $n2 { (-1) } else if $n1 > $n2 { 1 } else { 0 }
}
_ => 0
}
}
# Execute command and extract version
export def detect-version [
config: record # Detection configuration
]: nothing -> string {
if ($config | is-empty) { return "" }
let method = ($config | get -o method | default "command")
match $method {
"command" => {
let cmd = ($config | get -o command | default "")
if ($cmd | is-empty) { return "" }
let result = (^sh -c $cmd err> /dev/null | complete)
if $result.exit_code == 0 {
let output = $result.stdout
# Apply extraction pattern if provided
if ($config | get -o pattern | is-not-empty) {
let parsed = ($output | parse -r $config.pattern)
if ($parsed | length) > 0 {
let row = ($parsed | get 0)
let capture_name = ($config | get -o capture | default "capture0")
($row | get -o $capture_name | default "")
} else {
""
}
} else {
$output | str trim
}
} else {
""
}
}
"file" => {
let path = ($config | get -o path | default "")
if not ($path | path exists) { return "" }
let content = (open $path)
if ($config | get -o field | is-not-empty) {
$content | get -o $config.field | default ""
} else {
$content | str trim
}
}
"api" => {
let url = ($config | get -o url | default "")
if ($url | is-empty) { return "" }
let result = (http get $url --headers [User-Agent "nushell-version-checker"] | complete)
if $result.exit_code == 0 and ($result.stdout | length) > 0 {
let response = ($result.stdout | from json)
if ($config | get -o field | is-not-empty) {
$response | get -o $config.field | default ""
} else {
$response | to text | str trim
}
} else {
""
}
}
"script" => {
# Execute custom script
let script = ($config | get -o script | default "")
if ($script | is-empty) { return "" }
(nu -c $script | str trim | default "")
}
_ => ""
}
}
# Fetch available versions from source
export def fetch-versions [
config: record # Source configuration
--limit: int = 10
]: nothing -> list {
if ($config | is-empty) { return [] }
let type = ($config | get -o type | default "")
match $type {
"github" => {
let repo = ($config | get -o repo | default "")
if ($repo | is-empty) { return [] }
# Try releases first, then tags
let endpoints = [
$"https://api.github.com/repos/($repo)/releases"
$"https://api.github.com/repos/($repo)/tags"
]
for endpoint in $endpoints {
let response = (http get $endpoint --headers [User-Agent "nushell-version-checker"] | default [] | to json | from json | default [])
if ($response | length) > 0 {
return ($response
| first $limit
| each { |item|
let version = ($item | get -o tag_name | default ($item | get -o name | default ""))
$version | str replace -r '^v' ''
})
}
}
[]
}
"docker" => {
let image = ($config | get -o image | default "")
if ($image | is-empty) { return [] }
# Parse namespace/repo
let parts = ($image | split row "/")
let namespace = if ($parts | length) > 1 { $parts | get 0 } else { "library" }
let repo = ($parts | last)
let url = $"https://hub.docker.com/v2/namespaces/($namespace)/repositories/($repo)/tags"
let result = (http get $url --headers [User-Agent "nushell-version-checker"] | complete)
if $result.exit_code == 0 and ($result.stdout | length) > 0 {
let response = ($result.stdout | from json)
if ($response | get -o results | is-not-empty) {
$response
| get -o results
| first $limit
| each { |tag| $tag.name }
| where { |v| $v !~ "latest|dev|nightly|edge|alpha|beta|rc" }
} else {
[]
}
} else {
[]
}
}
"url" => {
let url = ($config | get -o url | default "")
if ($url | is-empty) { return [] }
let result = (http get $url --headers [User-Agent "nushell-version-checker"] | complete)
if $result.exit_code == 0 and ($result.stdout | length) > 0 {
let response = ($result.stdout | from json)
let field = ($config | get -o field | default "")
if ($field | is-not-empty) {
$response | get -o $field | default []
} else {
[$response | to text | str trim]
}
} else {
[]
}
}
"script" => {
let script = ($config | get -o script | default "")
if ($script | is-empty) { return [] }
(nu -c $script | lines | default [])
}
_ => []
}
}
# Generic version check
export def check-version [
component: record
--fetch-latest = false
--respect-fixed = true
]: nothing -> record {
# Detect installed version
let installed = if ($component | get -o detector | is-not-empty) {
(detect-version $component.detector)
} else { "" }
# Get configured version
let configured = ($component | get -o version | default "")
# Check if fixed
let is_fixed = ($component | get -o fixed | default false)
# Fetch latest if requested
let latest = if $fetch_latest and (not $is_fixed or not $respect_fixed) {
if ($component | get -o source | is-not-empty) {
let versions = (fetch-versions $component.source --limit=1)
if ($versions | length) > 0 { $versions | get 0 } else { $configured }
} else { $configured }
} else { $configured }
# Compare versions
let comparison_strategy = ($component | get -o comparison | default "semantic")
let status = if $is_fixed and $respect_fixed {
"fixed"
} else if ($installed | is-empty) {
"not_installed"
} else if ($installed | is-not-empty) and ($latest != $installed) and ((compare-versions $installed $latest --strategy=$comparison_strategy) < 0) {
"update_available"
} else if (compare-versions $installed $configured --strategy=$comparison_strategy) < 0 {
"behind_config"
} else if (compare-versions $installed $configured --strategy=$comparison_strategy) > 0 {
"ahead_config"
} else {
"up_to_date"
}
{
id: $component.id
type: $component.type
installed: $installed
configured: $configured
latest: $latest
fixed: $is_fixed
status: $status
}
}

View file

@ -0,0 +1,94 @@
#!/usr/bin/env nu
# Configurable formatters for version status display
# Status icon mapping (configurable)
export def status-icons []: nothing -> record {
{
fixed: "🔒"
not_installed: "❌"
update_available: "⬆️"
behind_config: "⚠️"
ahead_config: "🔄"
up_to_date: "✅"
unknown: "❓"
}
}
# Format status with configurable icons
export def format-status [
status: string
--icons: record = {}
]: nothing -> string {
let icon_map = if ($icons | is-empty) { (status-icons) } else { $icons }
let icon = ($icon_map | get -o $status | default $icon_map.unknown)
let text = match $status {
"fixed" => "Fixed"
"not_installed" => "Not installed"
"update_available" => "Update available"
"behind_config" => "Behind config"
"ahead_config" => "Ahead of config"
"up_to_date" => "Up to date"
_ => "Unknown"
}
$"($icon) ($text)"
}
# Format version results as table
export def format-results [
results: list
--group-by: string = "type"
--show-fields: list = ["id", "installed", "configured", "latest", "status"]
--icons: record = {}
]: nothing -> nothing {
if ($results | is-empty) {
print "No components found"
return
}
# Group results if requested
if ($group_by | is-not-empty) {
let grouped = ($results | group-by { |r| $r | get -o $group_by | default "unknown" })
for group in ($grouped | transpose key value) {
print $"\n### ($group.key | str capitalize)"
let formatted = ($group.value | each { |item|
mut row = {}
for field in $show_fields {
if $field == "status" {
$row = ($row | insert $field (format-status $item.status --icons=$icons))
} else {
$row = ($row | insert $field ($item | get -o $field | default ""))
}
}
$row
})
print ($formatted | table)
}
} else {
# Direct table output
let formatted = ($results | each { |item|
mut row = {}
for field in $show_fields {
if $field == "status" {
$row = ($row | insert $field (format-status $item.status --icons=$icons))
} else {
$row = ($row | insert $field ($item | get -o $field | default ""))
}
}
$row
})
print ($formatted | table)
}
# Summary
print "\n📊 Summary:"
let by_status = ($results | group-by status)
for status in ($by_status | transpose key value) {
print $" (format-status $status.key --icons=$icons): ($status.value | length)"
}
}

View file

@ -0,0 +1,264 @@
#!/usr/bin/env nu
# Dynamic configuration loader for version management
# Discovers and loads version configurations from the filesystem
use version_core.nu *
# Discover version configurations
export def discover-configurations [
--base-path: string = ""
--types: list = [] # Filter by types
]: nothing -> list {
let base = if ($base_path | is-empty) {
($env.PROVISIONING? | default $env.PWD)
} else { $base_path }
mut configurations = []
# Load from known version files directly
let version_files = [
($base | path join "versions.yaml")
($base | path join "core" | path join "versions.yaml")
]
for file in $version_files {
if ($file | path exists) {
let configs = (load-configuration-file $file)
if ($configs | is-not-empty) {
$configurations = ($configurations | append $configs)
}
}
}
# Also check providers directory
let providers_path = ($base | path join "providers")
if ($providers_path | path exists) {
for provider_dir in (ls $providers_path | get name) {
let version_file = ($provider_dir | path join "versions.yaml")
if ($version_file | path exists) {
let configs = (load-configuration-file $version_file)
if ($configs | is-not-empty) {
$configurations = ($configurations | append $configs)
}
}
}
}
# Filter by types if specified
if ($types | length) > 0 {
$configurations | where type in $types
} else {
$configurations
}
}
# Load configuration from file
export def load-configuration-file [
file_path: string
]: nothing -> list {
if not ($file_path | path exists) { return [] }
let ext = ($file_path | path parse | get extension)
let parent_dir = ($file_path | path dirname)
let context = (extract-context $parent_dir)
mut configs = []
match $ext {
"yaml" | "yml" => {
let data = (open $file_path)
if ($data | describe | str contains "record") {
# Convert record entries to configurations
for item in ($data | transpose key value) {
let config = (create-configuration $item.key $item.value $context $file_path)
$configs = ($configs | append $config)
}
} else if ($data | describe | str contains "list") {
# Already a list of configurations
$configs = $data
}
}
"k" => {
# Parse KCL files for version information
let content = (open $file_path)
let version_data = (extract-kcl-versions $content)
for item in $version_data {
let config = (create-configuration $item.name $item $context $file_path)
$configs = ($configs | append $config)
}
}
"toml" => {
let data = (open $file_path)
for section in ($data | transpose key value) {
if ($section.value | get -o version | is-not-empty) {
let config = (create-configuration $section.key $section.value $context $file_path)
$configs = ($configs | append $config)
}
}
}
"json" => {
let data = (open $file_path)
if ($data | get -o components | is-not-empty) {
$configs = $data.components
} else {
# Treat as single configuration
$configs = [$data]
}
}
_ => []
}
$configs
}
# Extract context from path
export def extract-context [
dir_path: string
]: nothing -> record {
let parts = ($dir_path | split row "/")
# Determine type based on path structure
let type = if ($parts | any { |p| $p == "providers" }) {
"provider"
} else if ($parts | any { |p| $p == "taskservs" }) {
"taskserv"
} else if ($parts | any { |p| $p == "clusters" }) {
"cluster"
} else if ($parts | any { |p| $p == "tools" }) {
"tool"
} else {
"generic"
}
# Extract category/subcategory
let category = if $type == "provider" {
$parts | skip while { |p| $p != "providers" } | skip 1 | first
} else if $type == "taskserv" {
$parts | skip while { |p| $p != "taskservs" } | skip 1 | first
} else {
""
}
{
type: $type
category: $category
path: $dir_path
}
}
# Create configuration object
export def create-configuration [
id: string
data: record
context: record
source_file: string
]: nothing -> record {
# Build detector configuration
let detector = if ($data | get -o check_cmd | is-not-empty) {
{
method: "command"
command: $data.check_cmd
pattern: ($data | get -o parse_pattern | default "")
capture: ($data | get -o capture_group | default "version")
}
} else if ($data | get -o detector | is-not-empty) {
$data.detector
} else {
{}
}
# Build source configuration
let source = if ($data | get -o source | is-not-empty) {
if ($data.source | str contains "github.com") {
{
type: "github"
repo: ($data.source | parse -r 'github\.com/(?<repo>.+)' | get -o 0 | get -o repo | str replace -r '/(releases|tags).*$' '')
}
} else if ($data.source | str starts-with "docker") {
{
type: "docker"
image: ($data.source | str replace "docker://" "")
}
} else if ($data.source | str starts-with "http") {
{
type: "url"
url: $data.source
field: ($data | get -o version_field | default "")
}
} else {
{ type: "custom", config: $data.source }
}
} else if ($data | get -o tags | is-not-empty) {
# Infer from tags URL
if ($data.tags | str contains "github") {
{
type: "github"
repo: ($data.tags | parse -r 'github\.com/(?<repo>[^/]+/[^/]+)' | get -o 0 | get -o repo)
}
} else {
{ type: "url", url: $data.tags }
}
} else {
{}
}
# Build complete configuration
{
id: $id
type: $context.type
category: ($context.category | default "")
version: ($data | get -o version | default "")
fixed: ($data | get -o fixed | default false)
source: $source
detector: $detector
comparison: ($data | get -o comparison | default "semantic")
metadata: {
source_file: $source_file
site: ($data | get -o site | default "")
description: ($data | get -o description | default "")
install_cmd: ($data | get -o install_cmd | default "")
lib: ($data | get -o lib | default "")
}
}
}
# Extract version info from KCL content
export def extract-kcl-versions [
content: string
]: nothing -> list {
mut versions = []
# Look for schema definitions with version fields
let lines = ($content | lines)
mut current_schema = ""
mut current_data = {}
for line in $lines {
if ($line | str contains "schema ") {
# New schema found
if ($current_schema | is-not-empty) and ($current_data | get -o version | is-not-empty) {
$versions = ($versions | append {
name: $current_schema
...$current_data
})
}
$current_schema = ($line | parse -r 'schema\s+(\w+)' | get -o 0 | get -o 0 | default "")
$current_data = {}
} else if ($line | str contains "version:") or ($line | str contains "version =") {
# Extract version
let version = ($line | parse -r 'version[:\s=]+"?([^"]+)"?' | get -o 0 | get -o 0 | default "")
if ($version | is-not-empty) {
$current_data.version = $version
}
}
}
# Add last schema if valid
if ($current_schema | is-not-empty) and ($current_data | get -o version | is-not-empty) {
$versions = ($versions | append {
name: $current_schema
...$current_data
})
}
$versions
}

View file

@ -0,0 +1,217 @@
#!/usr/bin/env nu
# Main version management interface
# Completely configuration-driven, no hardcoded components
use version_core.nu *
use version_loader.nu *
use version_formatter.nu *
use interface.nu *
# Check versions for discovered components
export def check-versions [
--path: string = "" # Base path to search
--types: list = [] # Filter by types
--fetch-latest = false # Fetch latest versions
--respect-fixed = true # Respect fixed flag
--config-file: string = "" # Use specific config file
]: nothing -> list {
# Load configurations
let configs = if ($config_file | is-not-empty) {
load-configuration-file $config_file
} else {
discover-configurations --base-path=$path --types=$types
}
# Check each configuration
$configs | each { |config|
check-version $config --fetch-latest=$fetch_latest --respect-fixed=$respect_fixed
}
}
# Display version status
export def show-versions [
--path: string = ""
--types: list = []
--fetch-latest = true
--group-by: string = "type"
--format: string = "table" # table, json, yaml
]: nothing -> nothing {
let results = (check-versions --path=$path --types=$types --fetch-latest=$fetch_latest)
match $format {
"table" => {
format-results $results --group-by=$group_by
}
"json" => {
print ($results | to json -i 2)
}
"yaml" => {
print ($results | to yaml)
}
_ => {
format-results $results
}
}
}
# Check for available updates (does not modify configs)
export def check-available-updates [
--path: string = ""
--types: list = []
]: nothing -> nothing {
let results = (check-versions --path=$path --types=$types --fetch-latest=true --respect-fixed=true)
let updates = ($results | where status == "update_available")
if ($updates | is-empty) {
_print "✅ All components are up to date"
return
}
_print "Updates available:"
_print ($updates | select id configured latest | rename id configured "latest available" | table)
# Show installation guidance for each update
for update in $updates {
let config = (discover-configurations --types=[$update.type]
| where id == $update.id
| get -o 0)
if ($config | is-not-empty) {
show-installation-guidance $config $update.latest
}
}
_print $"\n💡 After installing, run 'tools apply-updates' to update configuration files"
}
# Apply updates to configuration files (after manual installation)
export def apply-config-updates [
--path: string = ""
--types: list = []
--dry-run = false
--force = false # Update even if fixed
]: nothing -> nothing {
let results = (check-versions --path=$path --types=$types --fetch-latest=false --respect-fixed=(not $force))
# Find components where installed version is newer than configured
let updates = ($results | where status == "ahead_config")
if ($updates | is-empty) {
_print "✅ All configurations match installed versions"
return
}
_print "Configuration updates available (installed version newer than configured):"
_print ($updates | select id configured installed | table)
if $dry_run {
_print "\n🔍 Dry run mode - no changes will be made"
return
}
let proceed = (input "Update configurations to match installed versions? (y/n): ")
if $proceed != "y" { return }
# Update each component's configuration file to match installed version
for update in $updates {
let config = (discover-configurations --types=[$update.type]
| where id == $update.id
| get -o 0)
if ($config | is-not-empty) {
let source_file = $config.metadata.source_file
update-configuration-file $source_file $update.id $update.installed
_print $"✅ Updated config ($update.id): ($update.configured) -> ($update.installed)"
}
}
}
# Show agnostic installation guidance
export def show-installation-guidance [
config: record
version: string
]: nothing -> nothing {
_print $"\n📦 To install ($config.id) ($version):"
# Show documentation/site links from configuration
if ($config.metadata.site | is-not-empty) {
_print $" • Documentation: ($config.metadata.site)"
}
# Show source repository if available
if ($config.source.type? | default "" | str contains "github") {
let repo = ($config.source.repo? | default "")
if ($repo | is-not-empty) {
_print $" • Releases: https://github.com/($repo)/releases"
}
}
# Show generic installation command if available in metadata
if ($config.metadata.install_cmd? | default "" | is-not-empty) {
_print $" • Install: ($config.metadata.install_cmd)"
}
_print $"\n🔍 Configuration updated, manual installation required"
_print $"💡 Run 'tools check ($config.id)' after installation to verify"
}
# Update configuration file
export def update-configuration-file [
file_path: string
component_id: string
new_version: string
]: nothing -> nothing {
if not ($file_path | path exists) { return }
let ext = ($file_path | path parse | get extension)
match $ext {
"yaml" | "yml" => {
let data = (open $file_path)
let updated = ($data | upsert $component_id ($data | get $component_id | upsert version $new_version))
$updated | save -f $file_path
}
"json" => {
let data = (open $file_path)
let updated = ($data | upsert $component_id ($data | get $component_id | upsert version $new_version))
$updated | to json -i 2 | save -f $file_path
}
"toml" => {
# TOML update would need proper TOML writer
print $"⚠️ TOML update not implemented for ($file_path)"
}
"k" => {
# KCL update would need KCL parser/writer
print $"⚠️ KCL update not implemented for ($file_path)"
}
_ => {
print $"⚠️ Unknown file type: ($ext)"
}
}
}
# Pin/unpin component version
export def set-fixed [
component_id: string
fixed: bool
--path: string = ""
]: nothing -> nothing {
let configs = (discover-configurations --base-path=$path)
let config = ($configs | where id == $component_id | get -o 0)
if ($config | is-empty) {
print $"❌ Component '($component_id)' not found"
return
}
let source_file = $config.metadata.source_file
let data = (open $source_file)
let updated = ($data | upsert $component_id ($data | get $component_id | upsert fixed $fixed))
$updated | save -f $source_file
if $fixed {
print $"🔒 Pinned ($component_id) to version ($config.version)"
} else {
print $"🔓 Unpinned ($component_id)"
}
}

View file

@ -0,0 +1,235 @@
#!/usr/bin/env nu
# Version registry management for taskservs
# Handles the central version registry and integrates with taskserv configurations
use version_core.nu *
use version_taskserv.nu *
use interface.nu *
# Load the version registry
export def load-version-registry [
--registry-file: string = ""
]: nothing -> record {
let registry_path = if ($registry_file | is-not-empty) {
$registry_file
} else {
($env.PROVISIONING | path join "core" | path join "taskservs-versions.yaml")
}
if not ($registry_path | path exists) {
_print $"⚠️ Version registry not found: ($registry_path)"
return {}
}
open $registry_path
}
# Update registry with latest version information
export def update-registry-versions [
--components: list = [] # Specific components to update, empty for all
--dry-run = false
]: nothing -> nothing {
let registry = (load-version-registry)
if ($registry | is-empty) {
_print "❌ Could not load version registry"
return
}
let components_to_update = if ($components | is-empty) {
$registry | transpose key value | get key
} else {
$components
}
_print $"Updating versions for ($components_to_update | length) components..."
for component in $components_to_update {
let component_config = ($registry | get -o $component)
if ($component_config | is-empty) {
_print $"⚠️ Component '($component)' not found in registry"
continue
}
if ($component_config.fixed | default false) {
_print $"🔒 Skipping pinned component: ($component)"
continue
}
if ($component_config.source | is-empty) {
_print $"⚠️ No source configured for: ($component)"
continue
}
_print $"🔍 Checking latest version for: ($component)"
let latest_versions = (fetch-versions $component_config.source --limit=5)
if ($latest_versions | is-empty) {
_print $"❌ Could not fetch versions for: ($component)"
continue
}
let latest = ($latest_versions | get 0)
let current = ($component_config.current_version | default "")
if $latest != $current {
_print $"📦 ($component): ($current) -> ($latest)"
if not $dry_run {
# Update registry with new version
update-registry-component $component "current_version" $latest
update-registry-component $component "latest_check" (date now | format date "%Y-%m-%d %H:%M:%S")
}
} else {
_print $"✅ ($component): up to date at ($current)"
}
}
if not $dry_run {
_print "✅ Registry update completed"
} else {
_print "🔍 Dry run completed - no changes made"
}
}
# Update a specific component field in the registry
export def update-registry-component [
component_id: string
field: string
value: string
]: nothing -> nothing {
let registry_path = ($env.PROVISIONING | path join "core" | path join "taskservs-versions.yaml")
if not ($registry_path | path exists) {
_print $"❌ Registry file not found: ($registry_path)"
return
}
let registry = (open $registry_path)
let component_config = ($registry | get -o $component_id)
if ($component_config | is-empty) {
_print $"❌ Component '($component_id)' not found in registry"
return
}
let updated_component = ($component_config | upsert $field $value)
let updated_registry = ($registry | upsert $component_id $updated_component)
$updated_registry | save -f $registry_path
}
# Compare registry versions with taskserv configurations
export def compare-registry-with-taskservs [
--taskservs-path: string = ""
]: nothing -> list {
let registry = (load-version-registry)
let taskserv_configs = (discover-taskserv-configurations --base-path=$taskservs_path)
if ($registry | is-empty) or ($taskserv_configs | is-empty) {
_print "❌ Could not load registry or taskserv configurations"
return []
}
# Group taskservs by component type
let taskserv_by_component = ($taskserv_configs | group-by { |config|
# Extract component name from ID (handle both "component" and "server::component" formats)
if ($config.id | str contains "::") {
($config.id | split row "::" | get 1)
} else {
$config.id
}
})
let comparisons = ($registry | transpose component registry_config | each { |registry_item|
let component = $registry_item.component
let registry_version = ($registry_item.registry_config.current_version | default "")
let taskservs = ($taskserv_by_component | get -o $component | default [])
if ($taskservs | is-empty) {
{
component: $component
registry_version: $registry_version
taskserv_configs: []
status: "unused"
summary: "Not used in any taskservs"
}
} else {
let taskserv_versions = ($taskservs | each { |ts| {
id: $ts.id
version: $ts.version
file: $ts.kcl_file
matches_registry: ($ts.version == $registry_version)
}})
let all_match = ($taskserv_versions | all { |ts| $ts.matches_registry })
let any_outdated = ($taskserv_versions | any { |ts| not $ts.matches_registry })
let status = if $all_match {
"in_sync"
} else if $any_outdated {
"out_of_sync"
} else {
"mixed"
}
{
component: $component
registry_version: $registry_version
taskserv_configs: $taskserv_versions
status: $status
summary: $"($taskserv_versions | length) taskservs, ($taskserv_versions | where matches_registry | length) in sync"
}
}
})
$comparisons
}
# Show version status summary
export def show-version-status [
--taskservs-path: string = ""
--format: string = "table" # table, detail, json
]: nothing -> nothing {
let comparisons = (compare-registry-with-taskservs --taskservs-path=$taskservs_path)
match $format {
"table" => {
_print "Taskserv Version Status:"
_print ($comparisons | select component registry_version status summary | table)
}
"detail" => {
for comparison in $comparisons {
_print $"\n🔧 ($comparison.component) \\(Registry: ($comparison.registry_version)\\)"
_print $" Status: ($comparison.status) - ($comparison.summary)"
if ($comparison.taskserv_configs | length) > 0 {
for config in $comparison.taskserv_configs {
let status_icon = if $config.matches_registry { "✅" } else { "❌" }
_print $" ($status_icon) ($config.id): ($config.version)"
}
}
}
}
"json" => {
print ($comparisons | to json -i 2)
}
_ => {
_print $"❌ Unknown format: ($format). Use 'table', 'detail', or 'json'"
}
}
}
# Pin/unpin component in registry
export def set-registry-fixed [
component_id: string
fixed: bool
]: nothing -> nothing {
update-registry-component $component_id "fixed" ($fixed | into string)
if $fixed {
_print $"🔒 Pinned ($component_id) in registry"
} else {
_print $"🔓 Unpinned ($component_id) in registry"
}
}

View file

@ -0,0 +1,277 @@
#!/usr/bin/env nu
# Taskserv version extraction and management utilities
# Handles KCL taskserv files and version configuration
use version_core.nu *
use version_loader.nu *
use interface.nu *
# Extract version field from KCL taskserv files
export def extract-kcl-version [
file_path: string
]: nothing -> string {
if not ($file_path | path exists) { return "" }
let content = (open $file_path --raw)
# Look for version assignment in taskserv configuration files
let version_matches = ($content | lines | each { |line|
let trimmed_line = ($line | str trim)
# Match "version = " pattern (but not major_version, cni_version, etc.)
if ($trimmed_line | str starts-with "version") and ($trimmed_line | str contains "=") {
# Split on equals and take the right side
let parts = ($trimmed_line | split row "=")
if ($parts | length) >= 2 {
let version_value = ($parts | get 1 | str trim)
if ($version_value | str starts-with '"') {
# Remove quotes and get the value
($version_value | parse -r '"([^"]*)"' | get -o 0.capture0 | default "")
} else if ($version_value | str starts-with "'") {
# Handle single quotes
($version_value | parse -r "'([^']*)'" | get -o 0.capture0 | default "")
} else {
# Handle unquoted values (remove any trailing comments)
($version_value | str replace "\\s*#.*$" "" | str trim)
}
} else {
""
}
} else if ($trimmed_line | str starts-with "version:") and not ($trimmed_line | str contains "str") {
# Handle schema-style "version: value" (not type declarations)
let version_part = ($trimmed_line | str replace "version:\\s*" "")
if ($version_part | str starts-with '"') {
($version_part | parse -r '"([^"]*)"' | get -o 0.capture0 | default "")
} else if ($version_part | str starts-with "'") {
($version_part | parse -r "'([^']*)'" | get -o 0.capture0 | default "")
} else {
($version_part | str replace "\\s*#.*$" "" | str trim)
}
} else {
""
}
} | where { |v| $v != "" })
if ($version_matches | length) > 0 {
$version_matches | get 0
} else {
""
}
}
# Discover all taskserv KCL files and their versions
export def discover-taskserv-configurations [
--base-path: string = ""
]: nothing -> list {
let taskservs_path = if ($base_path | is-not-empty) {
$base_path
} else {
$env.PROVISIONING_TASKSERVS_PATH
}
if not ($taskservs_path | path exists) {
_print $"⚠️ Taskservs path not found: ($taskservs_path)"
return []
}
# Find all .k files recursively in the taskservs directory
let all_k_files = (glob $"($taskservs_path)/**/*.k")
let kcl_configs = ($all_k_files | each { |kcl_file|
let version = (extract-kcl-version $kcl_file)
if ($version | is-not-empty) {
let relative_path = ($kcl_file | str replace $"($taskservs_path)/" "")
let path_parts = ($relative_path | split row "/" | where { |p| $p != "" })
# Determine ID from the path structure
let id = if ($path_parts | length) >= 2 {
# If it's a server-specific file like "wuji-strg-1/kubernetes.k"
let filename = ($kcl_file | path basename | str replace ".k" "")
$"($path_parts.0)::($filename)"
} else {
# If it's a general file like "proxy.k"
($kcl_file | path basename | str replace ".k" "")
}
{
id: $id
type: "taskserv"
kcl_file: $kcl_file
version: $version
metadata: {
source_file: $kcl_file
category: "taskserv"
path_structure: $path_parts
}
}
} else {
null
}
} | where { |item| $item != null })
$kcl_configs
}
# Update version in KCL file
export def update-kcl-version [
file_path: string
new_version: string
]: nothing -> nothing {
if not ($file_path | path exists) {
_print $"❌ File not found: ($file_path)"
return
}
let content = (open $file_path --raw)
# Replace version field while preserving formatting
let updated_content = ($content | lines | each { |line|
if ($line | str trim | str starts-with "version:") {
# Preserve indentation and update version
let indent = ($line | str replace "^(\\s*).*" '$1')
let line_trimmed = ($line | str trim)
if ($line_trimmed | str contains '"') {
$"($indent)version: \"($new_version)\""
} else if ($line_trimmed | str contains "'") {
$"($indent)version: '($new_version)'"
} else {
$"($indent)version: str = \"($new_version)\""
}
} else {
$line
}
} | str join "\n")
$updated_content | save -f $file_path
_print $"✅ Updated version in ($file_path) to ($new_version)"
}
# Check taskserv versions against available versions
export def check-taskserv-versions [
--fetch-latest = false
]: nothing -> list {
let configs = (discover-taskserv-configurations)
if ($configs | is-empty) {
_print "No taskserv configurations found"
return []
}
$configs | each { |config|
# For now, return basic info - can be extended with version checking logic
{
id: $config.id
type: $config.type
configured: $config.version
kcl_file: $config.kcl_file
status: "configured"
}
}
}
# Update taskserv version in KCL file
export def update-taskserv-version [
taskserv_id: string
new_version: string
--dry-run = false
]: nothing -> nothing {
let configs = (discover-taskserv-configurations)
let config = ($configs | where id == $taskserv_id | get -o 0)
if ($config | is-empty) {
_print $"❌ Taskserv '($taskserv_id)' not found"
return
}
if $dry_run {
_print $"🔍 Would update ($taskserv_id) from ($config.version) to ($new_version) in ($config.kcl_file)"
return
}
update-kcl-version $config.kcl_file $new_version
}
# Bulk update multiple taskservs
export def bulk-update-taskservs [
updates: list # List of {id: string, version: string}
--dry-run = false
]: nothing -> nothing {
if ($updates | is-empty) {
_print "No updates provided"
return
}
_print $"Updating ($updates | length) taskservs..."
for update in $updates {
let taskserv_id = ($update | get -o id | default "")
let new_version = ($update | get -o version | default "")
if ($taskserv_id | is-empty) or ($new_version | is-empty) {
_print $"⚠️ Invalid update entry: ($update)"
continue
}
update-taskserv-version $taskserv_id $new_version --dry-run=$dry_run
}
if not $dry_run {
_print "✅ Bulk update completed"
}
}
# Sync taskserv versions with registry
export def taskserv-sync-versions [
--taskservs-path: string = ""
--component: string = "" # Specific component to sync
--dry-run = false
]: nothing -> nothing {
let registry = (load-version-registry)
let comparisons = (compare-registry-with-taskservs --taskservs-path=$taskservs_path)
if ($comparisons | is-empty) {
_print "❌ No taskserv configurations found"
return
}
# Filter to out-of-sync components
mut out_of_sync = ($comparisons | where status == "out_of_sync")
if ($component | is-not-empty) {
let filtered = ($out_of_sync | where component == $component)
if ($filtered | is-empty) {
_print $"✅ Component '($component)' is already in sync or not found"
return
}
$out_of_sync = $filtered
}
if ($out_of_sync | is-empty) {
_print "✅ All taskservs are in sync with registry"
return
}
_print $"Found ($out_of_sync | length) components with version mismatches:"
for comp in $out_of_sync {
_print $"\n🔧 ($comp.component) [Registry: ($comp.registry_version)]"
# Find taskservs that need updating
let outdated_taskservs = ($comp.taskserv_configs | where not matches_registry)
for taskserv in $outdated_taskservs {
if $dry_run {
_print $"🔍 Would update ($taskserv.id): ($taskserv.version) -> ($comp.registry_version)"
} else {
_print $"🔄 Updating ($taskserv.id): ($taskserv.version) -> ($comp.registry_version)"
update-kcl-version $taskserv.file $comp.registry_version
}
}
}
if $dry_run {
_print "\n🔍 Dry run completed - no changes made"
} else {
_print "\n✅ Sync completed"
}
}