provisioning/tools/distribution/prepare-core-dist.nu
2025-10-07 11:12:02 +01:00

958 lines
31 KiB
Plaintext

#!/usr/bin/env nu
# Core distribution preparation tool - prepares core files for distribution
#
# Prepares:
# - Nushell core libraries and modules
# - Configuration templates and schemas
# - CLI wrapper scripts and entry points
# - Plugin definitions and extensions
# - Template system and utilities
use std log
def main [
--source-root: string = "" # Source root directory (auto-detected if empty)
--output-dir: string = "dist/core" # Output directory for core distribution
--include-dev: bool = false # Include development files and tools
--minify-scripts: bool = false # Minify Nushell scripts (remove comments/whitespace)
--validate-syntax: bool = true # Validate all Nushell scripts
--generate-index: bool = true # Generate module index files
--bundle-plugins: bool = true # Bundle plugin definitions
--create-stubs: bool = false # Create type definition stubs
--verbose: bool = false # Enable verbose logging
] -> record {
let repo_root = if $source_root == "" {
($env.PWD | path dirname | path dirname | path dirname)
} else {
($source_root | path expand)
}
let core_config = {
source_root: $repo_root
output_dir: ($output_dir | path expand)
include_dev: $include_dev
minify_scripts: $minify_scripts
validate_syntax: $validate_syntax
generate_index: $generate_index
bundle_plugins: $bundle_plugins
create_stubs: $create_stubs
verbose: $verbose
}
log info $"Starting core distribution preparation with config: ($core_config)"
# Ensure output directory exists
mkdir ($core_config.output_dir)
let preparation_results = []
try {
# Phase 1: Discover and validate core components
let discovery_result = discover_core_components $core_config
let preparation_results = ($preparation_results | append { phase: "discovery", result: $discovery_result })
if $discovery_result.status != "success" {
log error $"Core component discovery failed: ($discovery_result.reason)"
exit 1
}
# Phase 2: Prepare core libraries
let libraries_result = prepare_core_libraries $core_config $discovery_result
let preparation_results = ($preparation_results | append { phase: "libraries", result: $libraries_result })
# Phase 3: Prepare CLI components
let cli_result = prepare_cli_components $core_config $discovery_result
let preparation_results = ($preparation_results | append { phase: "cli", result: $cli_result })
# Phase 4: Prepare configuration system
let config_result = prepare_configuration_system $core_config $discovery_result
let preparation_results = ($preparation_results | append { phase: "configuration", result: $config_result })
# Phase 5: Bundle plugins and extensions
let plugins_result = if $core_config.bundle_plugins {
prepare_plugin_system $core_config $discovery_result
} else {
{ status: "skipped", reason: "plugin bundling disabled" }
}
let preparation_results = ($preparation_results | append { phase: "plugins", result: $plugins_result })
# Phase 6: Generate indexes and metadata
let index_result = if $core_config.generate_index {
generate_core_indexes $core_config $preparation_results
} else {
{ status: "skipped", reason: "index generation disabled" }
}
let preparation_results = ($preparation_results | append { phase: "indexes", result: $index_result })
# Phase 7: Create distribution metadata
let metadata_result = create_core_metadata $core_config $preparation_results
let preparation_results = ($preparation_results | append { phase: "metadata", result: $metadata_result })
let summary = {
source_root: $core_config.source_root
output_directory: $core_config.output_dir
successful_phases: ($preparation_results | where {|r| $r.result.status == "success"} | length)
total_phases: ($preparation_results | length)
core_files_prepared: ($preparation_results | get result.files_processed | math sum)
total_size: (get_directory_size $core_config.output_dir)
core_config: $core_config
phases: $preparation_results
}
log info $"Core distribution preparation completed successfully - ($summary.core_files_prepared) files prepared"
return $summary
} catch {|err|
log error $"Core distribution preparation failed: ($err.msg)"
exit 1
}
}
# Discover core components in the source tree
def discover_core_components [core_config: record] -> record {
log info "Discovering core components..."
let start_time = (date now)
try {
# Define core component locations
let core_locations = {
provisioning_cli: ($core_config.source_root | path join "provisioning" "core" "nulib" "provisioning")
core_libraries: ($core_config.source_root | path join "provisioning" "core" "nulib" "lib_provisioning")
workflows: ($core_config.source_root | path join "provisioning" "core" "nulib" "workflows")
servers: ($core_config.source_root | path join "provisioning" "core" "nulib" "servers")
extensions: ($core_config.source_root | path join "provisioning" "extensions")
config_system: ($core_config.source_root | path join "provisioning" "config.defaults.toml")
}
# Discover Nushell files
let nu_files = []
for location_name in ($core_locations | columns) {
let location_path = ($core_locations | get $location_name)
if ($location_path | path exists) {
let found_files = (find $location_path -name "*.nu" -type f)
let nu_files = ($nu_files | append ($found_files | each {|file|
{
path: $file
component: $location_name
relative_path: ($file | str replace $core_config.source_root "")
size: (ls $file | get 0.size)
}
}))
}
}
# Discover configuration files
let config_files = (find ($core_config.source_root | path join "provisioning") -name "*.toml" -type f)
# Discover template files
let template_files = (find ($core_config.source_root | path join "provisioning") -name "*.j2" -o -name "*.template" -type f)
# Validate critical components exist
let missing_components = []
let critical_components = ["provisioning_cli", "core_libraries"]
for component in $critical_components {
let component_path = ($core_locations | get $component)
if not ($component_path | path exists) {
let missing_components = ($missing_components | append $component)
}
}
if ($missing_components | length) > 0 {
return {
status: "failed"
reason: $"Missing critical components: ($missing_components | str join ', ')"
duration: ((date now) - $start_time)
}
}
{
status: "success"
core_locations: $core_locations
nu_files: $nu_files
config_files: $config_files
template_files: $template_files
total_nu_files: ($nu_files | length)
total_config_files: ($config_files | length)
total_template_files: ($template_files | length)
duration: ((date now) - $start_time)
}
} catch {|err|
{
status: "failed"
reason: $err.msg
duration: ((date now) - $start_time)
}
}
}
# Prepare core libraries for distribution
def prepare_core_libraries [
core_config: record
discovery_result: record
] -> record {
log info "Preparing core libraries..."
let start_time = (date now)
let lib_output_dir = ($core_config.output_dir | path join "lib")
mkdir $lib_output_dir
try {
let mut prepared_files = []
let mut validation_errors = []
# Process core library files
let core_lib_files = ($discovery_result.nu_files | where component == "core_libraries")
for file_info in $core_lib_files {
let processing_result = process_nushell_file $file_info $core_config $lib_output_dir
if $processing_result.status == "success" {
$prepared_files = ($prepared_files | append $processing_result)
} else {
$validation_errors = ($validation_errors | append $processing_result.errors)
}
}
# Process workflow files
let workflow_files = ($discovery_result.nu_files | where component == "workflows")
for file_info in $workflow_files {
let processing_result = process_nushell_file $file_info $core_config ($lib_output_dir | path join "workflows")
if $processing_result.status == "success" {
$prepared_files = ($prepared_files | append $processing_result)
} else {
$validation_errors = ($validation_errors | append $processing_result.errors)
}
}
# Process server management files
let server_files = ($discovery_result.nu_files | where component == "servers")
for file_info in $server_files {
let processing_result = process_nushell_file $file_info $core_config ($lib_output_dir | path join "servers")
if $processing_result.status == "success" {
$prepared_files = ($prepared_files | append $processing_result)
} else {
$validation_errors = ($validation_errors | append $processing_result.errors)
}
}
let status = if ($validation_errors | length) > 0 { "partial" } else { "success" }
{
status: $status
files_processed: ($prepared_files | length)
validation_errors: ($validation_errors | length)
prepared_files: $prepared_files
errors: $validation_errors
lib_output_dir: $lib_output_dir
duration: ((date now) - $start_time)
}
} catch {|err|
{
status: "failed"
reason: $err.msg
duration: ((date now) - $start_time)
}
}
}
# Prepare CLI components for distribution
def prepare_cli_components [
core_config: record
discovery_result: record
] -> record {
log info "Preparing CLI components..."
let start_time = (date now)
let cli_output_dir = ($core_config.output_dir | path join "bin")
mkdir $cli_output_dir
try {
# Process main provisioning CLI
let cli_location = ($discovery_result.core_locations.provisioning_cli)
if ($cli_location | path exists) {
let target_cli = ($cli_output_dir | path join "provisioning")
# Copy CLI script
cp $cli_location $target_cli
# Make executable on Unix-like systems
if $nu.os-info.name != "windows" {
chmod +x $target_cli
}
# Create wrapper scripts for different environments
create_cli_wrappers $cli_output_dir $core_config
{
status: "success"
cli_prepared: $target_cli
wrappers_created: 3 # Unix, Windows, Development
files_processed: 4
duration: ((date now) - $start_time)
}
} else {
{
status: "failed"
reason: "provisioning CLI not found"
duration: ((date now) - $start_time)
}
}
} catch {|err|
{
status: "failed"
reason: $err.msg
duration: ((date now) - $start_time)
}
}
}
# Create CLI wrapper scripts
def create_cli_wrappers [cli_output_dir: string, core_config: record] {
# Unix shell wrapper
let unix_wrapper = $"#!/bin/bash
# Provisioning System CLI Wrapper
# This wrapper sets up the environment and executes the main CLI
# Set environment variables
export PROVISIONING_HOME=\"$(dirname \"$(readlink -f \"$0\")\")/../\"
export PROVISIONING_LIB=\"$PROVISIONING_HOME/lib\"
export PROVISIONING_CONFIG=\"$PROVISIONING_HOME/../config\"
# Execute the main CLI with proper library path
exec nu \"$PROVISIONING_HOME/bin/provisioning\" \"$@\"
"
$unix_wrapper | save ($cli_output_dir | path join "provisioning.sh")
chmod +x ($cli_output_dir | path join "provisioning.sh")
# Windows batch wrapper
let windows_wrapper = $"@echo off
REM Provisioning System CLI Wrapper
REM This wrapper sets up the environment and executes the main CLI
REM Set environment variables
set \"PROVISIONING_HOME=%~dp0..\\\"
set \"PROVISIONING_LIB=%PROVISIONING_HOME%lib\"
set \"PROVISIONING_CONFIG=%PROVISIONING_HOME%..\\config\"
REM Execute the main CLI
nu \"%PROVISIONING_HOME%bin\\provisioning\" %*
"
$windows_wrapper | save ($cli_output_dir | path join "provisioning.bat")
# Development wrapper (preserves source paths)
let dev_wrapper = $"#!/usr/bin/env nu
# Provisioning Development CLI Wrapper
# This wrapper is used during development with source paths
# Set development paths
$env.PROVISIONING_HOME = ($env.PWD | path dirname)
$env.PROVISIONING_LIB = ($env.PROVISIONING_HOME | path join \"lib\")
$env.PROVISIONING_CONFIG = ($env.PROVISIONING_HOME | path join \"../config\")
$env.PROVISIONING_DEV = true
# Execute the main CLI
source ($env.PROVISIONING_HOME | path join \"bin\" \"provisioning\")
"
$dev_wrapper | save ($cli_output_dir | path join "provisioning-dev.nu")
chmod +x ($cli_output_dir | path join "provisioning-dev.nu")
}
# Prepare configuration system for distribution
def prepare_configuration_system [
core_config: record
discovery_result: record
] -> record {
log info "Preparing configuration system..."
let start_time = (date now)
let config_output_dir = ($core_config.output_dir | path join "config")
mkdir $config_output_dir
try {
let mut processed_configs = []
# Process configuration files
for config_file in $discovery_result.config_files {
let config_name = ($config_file | path basename)
let target_config = ($config_output_dir | path join $config_name)
# Process configuration file (validate and optionally minify)
let processing_result = process_config_file $config_file $target_config $core_config
if $processing_result.status == "success" {
$processed_configs = ($processed_configs | append $processing_result)
}
}
# Create configuration templates
create_config_templates $config_output_dir $core_config
{
status: "success"
files_processed: ($processed_configs | length)
config_files: $processed_configs
templates_created: 3 # user, dev, prod
config_output_dir: $config_output_dir
duration: ((date now) - $start_time)
}
} catch {|err|
{
status: "failed"
reason: $err.msg
duration: ((date now) - $start_time)
}
}
}
# Process a single Nushell file
def process_nushell_file [
file_info: record
core_config: record
output_dir: string
] -> record {
let relative_path = ($file_info.relative_path | str trim-left "/")
let target_file = ($output_dir | path join ($file_info.path | path basename))
# Ensure target directory exists
mkdir ($target_file | path dirname)
try {
let content = (open $file_info.path --raw)
# Validate syntax if requested
if $core_config.validate_syntax {
let validation_result = validate_nu_syntax $file_info.path $content
if $validation_result.status != "success" {
return {
status: "failed"
file: $file_info.path
target: $target_file
errors: $validation_result.errors
}
}
}
# Process content based on configuration
let processed_content = if $core_config.minify_scripts {
minify_nushell_script $content $core_config
} else {
$content
}
# Filter development code if not including dev files
let final_content = if not $core_config.include_dev {
filter_development_code $processed_content
} else {
$processed_content
}
# Save processed file
$final_content | save $target_file
{
status: "success"
source: $file_info.path
target: $target_file
size: ($final_content | str length)
component: $file_info.component
}
} catch {|err|
{
status: "failed"
file: $file_info.path
target: $target_file
errors: [$err.msg]
}
}
}
# Validate Nushell syntax
def validate_nu_syntax [file_path: string, content: string] -> record {
try {
# Use Nushell's built-in syntax checking
nu --check $file_path
{
status: "success"
file: $file_path
}
} catch {|err|
{
status: "failed"
file: $file_path
errors: [$err.msg]
}
}
}
# Minify Nushell script by removing comments and extra whitespace
def minify_nushell_script [content: string, core_config: record] -> string {
if not $core_config.minify_scripts {
return $content
}
# Simple minification - remove comment lines and extra whitespace
let lines = ($content | lines)
let minified_lines = $lines | each {|line|
let trimmed = ($line | str trim)
# Keep non-comment lines and preserve some important comments
if ($trimmed | str starts-with "#") {
if ($trimmed | str contains "!/usr/bin") or ($trimmed | str contains "!/bin/") {
$line # Keep shebang lines
} else {
"" # Remove comment lines
}
} else {
$trimmed # Keep code lines, trimmed
}
} | where $it != ""
return ($minified_lines | str join "\n")
}
# Filter out development-specific code
def filter_development_code [content: string] -> string {
let lines = ($content | lines)
let mut filtered_lines = []
let mut in_dev_block = false
for line in $lines {
# Check for development block markers
if ($line | str contains "# DEV_START") or ($line | str contains "# DEBUG_START") {
$in_dev_block = true
continue
}
if ($line | str contains "# DEV_END") or ($line | str contains "# DEBUG_END") {
$in_dev_block = false
continue
}
# Skip lines in development blocks
if $in_dev_block {
continue
}
# Skip individual development lines
if ($line | str contains "# DEV_ONLY") or ($line | str contains "# DEBUG") {
continue
}
$filtered_lines = ($filtered_lines | append $line)
}
return ($filtered_lines | str join "\n")
}
# Process configuration file
def process_config_file [source_file: string, target_file: string, core_config: record] -> record {
try {
# Validate TOML syntax
let config_data = (open $source_file)
# Copy file (could add processing here if needed)
cp $source_file $target_file
{
status: "success"
source: $source_file
target: $target_file
validated: true
}
} catch {|err|
{
status: "failed"
source: $source_file
target: $target_file
error: $err.msg
}
}
}
# Create configuration templates
def create_config_templates [config_output_dir: string, core_config: record] {
# User configuration template
let user_template = $"# User Configuration Template
# Copy this file to config.user.toml and customize as needed
# User-specific paths and preferences
[paths]
# Override default paths if needed
# home = \"/custom/path\"
[user]
name = \"Your Name\"
email = \"your.email@example.com\"
# Development settings
[dev]
debug = false
verbose = false
"
$user_template | save ($config_output_dir | path join "config.user.toml.template")
# Development configuration template
let dev_template = $"# Development Configuration Template
# Copy this file to config.dev.toml for development environment
[general]
environment = \"development\"
debug = true
log_level = \"debug\"
[paths]
cache_ttl = 60 # Short cache for development
"
$dev_template | save ($config_output_dir | path join "config.dev.toml.template")
# Production configuration template
let prod_template = $"# Production Configuration Template
# Copy this file to config.prod.toml for production environment
[general]
environment = \"production\"
debug = false
log_level = \"info\"
[security]
# Enable security features for production
strict_mode = true
"
$prod_template | save ($config_output_dir | path join "config.prod.toml.template")
}
# Prepare plugin system for distribution
def prepare_plugin_system [
core_config: record
discovery_result: record
] -> record {
log info "Preparing plugin system..."
let start_time = (date now)
let plugins_output_dir = ($core_config.output_dir | path join "plugins")
mkdir $plugins_output_dir
try {
# Process extension files
let extension_files = ($discovery_result.nu_files | where component == "extensions")
let mut processed_extensions = []
for file_info in $extension_files {
let processing_result = process_nushell_file $file_info $core_config $plugins_output_dir
if $processing_result.status == "success" {
$processed_extensions = ($processed_extensions | append $processing_result)
}
}
# Create plugin registry
let plugin_registry = create_plugin_registry $processed_extensions $core_config
{
status: "success"
files_processed: ($processed_extensions | length)
extensions: $processed_extensions
plugin_registry: $plugin_registry
plugins_output_dir: $plugins_output_dir
duration: ((date now) - $start_time)
}
} catch {|err|
{
status: "failed"
reason: $err.msg
duration: ((date now) - $start_time)
}
}
}
# Create plugin registry
def create_plugin_registry [processed_extensions: list, core_config: record] -> record {
let plugin_registry = {
version: "1.0.0"
plugins: ($processed_extensions | each {|ext|
{
name: ($ext.target | path basename | str replace ".nu" "")
path: ($ext.target | path basename)
component: $ext.component
size: $ext.size
}
})
created_at: (date now)
}
let registry_file = ($core_config.output_dir | path join "plugins" "registry.json")
$plugin_registry | to json | save $registry_file
return $plugin_registry
}
# Generate core indexes and module listings
def generate_core_indexes [
core_config: record
preparation_results: list
] -> record {
log info "Generating core indexes..."
let start_time = (date now)
try {
# Generate library index
let lib_result = ($preparation_results | where {|r| $r.phase == "libraries"} | get 0.result)
let lib_index = generate_library_index $lib_result.prepared_files $core_config
# Generate CLI index
let cli_result = ($preparation_results | where {|r| $r.phase == "cli"} | get 0.result)
let cli_index = generate_cli_index $cli_result $core_config
# Generate main index
let main_index = generate_main_index $preparation_results $core_config
{
status: "success"
indexes_generated: ["library", "cli", "main"]
library_index: $lib_index
cli_index: $cli_index
main_index: $main_index
duration: ((date now) - $start_time)
}
} catch {|err|
{
status: "failed"
reason: $err.msg
duration: ((date now) - $start_time)
}
}
}
# Generate library index
def generate_library_index [prepared_files: list, core_config: record] -> string {
let index_content = $"# Core Library Index
This file provides an index of all core library modules included in this distribution.
## Library Modules
"
let modules = ($prepared_files | group-by component)
let mut full_content = [$index_content]
for component in ($modules | columns) {
let component_files = ($modules | get $component)
$full_content = ($full_content | append $"### ($component | str title-case)")
$full_content = ($full_content | append "")
for file in $component_files {
let module_name = ($file.target | path basename | str replace ".nu" "")
$full_content = ($full_content | append $"- **($module_name)**: ($file.target)")
}
$full_content = ($full_content | append "")
}
let content = ($full_content | str join "\n")
$content | save ($core_config.output_dir | path join "lib" "INDEX.md")
return $content
}
# Generate CLI index
def generate_cli_index [cli_result: record, core_config: record] -> string {
let cli_index = $"# CLI Components Index
## Main CLI
- **provisioning**: Main provisioning CLI entry point
## Wrapper Scripts
- **provisioning.sh**: Unix/Linux shell wrapper
- **provisioning.bat**: Windows batch wrapper
- **provisioning-dev.nu**: Development wrapper
## Usage
The main CLI provides access to all provisioning functionality:
```
./bin/provisioning help
```
Generated: (date now | format date '%Y-%m-%d %H:%M:%S')
"
$cli_index | save ($core_config.output_dir | path join "bin" "INDEX.md")
return $cli_index
}
# Generate main index
def generate_main_index [preparation_results: list, core_config: record] -> string {
let successful_phases = ($preparation_results | where {|r| $r.result.status == "success"})
let total_files = ($successful_phases | get result.files_processed | math sum)
let main_index = $"# Core Distribution Index
This distribution contains the core components of the Provisioning System.
## Directory Structure
- **bin/**: CLI executables and wrapper scripts
- **lib/**: Core Nushell libraries and modules
- **config/**: Configuration files and templates
- **plugins/**: Extensions and plugin definitions
## Statistics
- **Total files processed**: ($total_files)
- **Successful phases**: ($successful_phases | length)
- **Generated**: (date now | format date '%Y-%m-%d %H:%M:%S')
## Usage
To use this core distribution:
1. Add bin/ to your PATH
2. Set PROVISIONING_LIB to point to lib/
3. Configure PROVISIONING_CONFIG to point to config/
4. Run `provisioning help` to get started
For more information, see the documentation in each subdirectory.
"
$main_index | save ($core_config.output_dir | path join "INDEX.md")
return $main_index
}
# Create core metadata
def create_core_metadata [
core_config: record
preparation_results: list
] -> record {
log info "Creating core metadata..."
let start_time = (date now)
try {
let metadata = {
name: "provisioning-core"
version: (detect_version $core_config.source_root)
type: "core-distribution"
created_at: (date now)
created_by: "core-distribution-tool"
source_root: $core_config.source_root
configuration: $core_config
preparation_phases: ($preparation_results | each {|r|
{
phase: $r.phase
status: $r.result.status
files_processed: (if "files_processed" in ($r.result | columns) { $r.result.files_processed } else { 0 })
duration: (if "duration" in ($r.result | columns) { $r.result.duration } else { 0 })
}
})
statistics: {
total_phases: ($preparation_results | length)
successful_phases: ($preparation_results | where {|r| $r.result.status == "success"} | length)
total_files: ($preparation_results | get result.files_processed | math sum)
distribution_size: (get_directory_size $core_config.output_dir)
}
}
let metadata_file = ($core_config.output_dir | path join "core-metadata.json")
$metadata | to json | save $metadata_file
{
status: "success"
metadata_file: $metadata_file
metadata: $metadata
duration: ((date now) - $start_time)
}
} catch {|err|
{
status: "failed"
reason: $err.msg
duration: ((date now) - $start_time)
}
}
}
# Detect version from git or other sources
def detect_version [repo_root: string] -> string {
cd $repo_root
try {
let git_version = (git describe --tags --always --dirty 2>/dev/null | str trim)
if $git_version != "" {
return ($git_version | str replace "^v" "")
}
return $"dev-(date now | format date '%Y%m%d')"
} catch {
return "unknown"
}
}
# Get directory size helper
def get_directory_size [dir: string] -> int {
if not ($dir | path exists) {
return 0
}
try {
find $dir -type f | each {|file| ls $file | get 0.size } | math sum | if $in == null { 0 } else { $in }
} catch {
0
}
}
# Show core distribution status
def "main status" [] {
let repo_root = ($env.PWD | path dirname | path dirname | path dirname)
let version = (detect_version $repo_root)
# Check for core components
let provisioning_cli = ($repo_root | path join "provisioning" "core" "nulib" "provisioning")
let core_libraries = ($repo_root | path join "provisioning" "core" "nulib" "lib_provisioning")
{
repository: $repo_root
version: $version
core_components: {
provisioning_cli: ($provisioning_cli | path exists)
core_libraries: ($core_libraries | path exists)
}
nu_files_found: (try { find $repo_root -name "*.nu" -type f | length } catch { 0 })
config_files_found: (try { find ($repo_root | path join "provisioning") -name "*.toml" -type f | length } catch { 0 })
ready_for_distribution: (($provisioning_cli | path exists) and ($core_libraries | path exists))
}
}
# Quick core preparation with minimal options
def "main quick" [output_dir: string = "dist/core"] {
main --output-dir $output_dir --validate-syntax --generate-index
}