src/public/System/New-AitherProject.ps1
|
#Requires -Version 7.0 <# .SYNOPSIS Scaffolds a new AitherZero project workspace. .DESCRIPTION Creates a standard AitherZero project structure with all necessary directories, configuration files, and boilerplate code. Supports templates, CI/CD generation, IDE configuration, and multiple languages (PowerShell, Python, OpenTofu). .PARAMETER Path The path where the new project should be created. .PARAMETER Name The name of the project. Defaults to the leaf folder name. .PARAMETER Template Project template to use. Options: 'Standard' (default), 'Minimal'. .PARAMETER Language Project language/type. Options: 'PowerShell' (default), 'Python', 'OpenTofu'. .PARAMETER IncludeCI Generate GitHub Actions CI/CD workflow. .PARAMETER IncludeVSCode Generate VS Code configuration (.vscode). .PARAMETER IncludeModule Create a default PowerShell module within the project (PowerShell only). .PARAMETER GitInit Initialize a git repository in the new project. .PARAMETER RegisterProject Register the project in the AitherZero registry. .PARAMETER Force Overwrite existing files if they exist. .EXAMPLE New-AitherProject -Path ./MyPyService -Language Python -IncludeCI #> function New-AitherProject { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory, Position = 0, HelpMessage = "The path where the new project should be created.")] [string]$Path, [Parameter(HelpMessage = "The name of the project.")] [string]$Name, [Parameter(HelpMessage = "Project template.")] [ValidateSet('Standard', 'Minimal')] [string]$Template = 'Standard', [Parameter(HelpMessage = "Project language.")] [ValidateSet('PowerShell', 'Python', 'OpenTofu')] [string]$Language = 'PowerShell', [Parameter(HelpMessage = "Generate CI/CD workflows.")] [switch]$IncludeCI, [Parameter(HelpMessage = "Generate VS Code config.")] [switch]$IncludeVSCode, [Parameter(HelpMessage = "Create default module.")] [switch]$IncludeModule, [Parameter(HelpMessage = "Initialize a git repository.")] [switch]$GitInit, [Parameter(HelpMessage = "Register the project in the AitherZero registry.")] [switch]$RegisterProject, [Parameter(HelpMessage = "Overwrite existing files.")] [switch]$Force, [Parameter(HelpMessage = "Show command output in console.")] [switch]$ShowOutput ) begin { # Manage logging targets for this execution $originalLogTargets = $script:AitherLogTargets if ($ShowOutput) { if ($script:AitherLogTargets -notcontains 'Console') { $script:AitherLogTargets += 'Console' } } else { $script:AitherLogTargets = $script:AitherLogTargets | Where-Object { $_ -ne 'Console' } } } process { if ($PSCmdlet.ShouldProcess($Path, "Create AitherZero $Language project '$Name'")) { try { # Handle Path resolution $fullPath = $Path if (-not (Test-Path $Path)) { New-Item -Path $Path -ItemType Directory -Force | Out-Null $fullPath = Resolve-Path -Path $Path } else { $fullPath = Resolve-Path -Path $Path } if (-not $Name) { $Name = Split-Path $fullPath -Leaf } Write-AitherLog -Level Information -Message "Scaffolding project '$Name' at '$fullPath' (Template: $Template, Language: $Language)" # 1. Create Base Directory Structure $directories = @( "automation-scripts", "AitherZero/config", "logs", "docs" ) if ($Template -eq 'Standard') { $directories += @("AitherZero/library/templates") } # Language-specific directories if ($Language -eq 'PowerShell') { if ($Template -eq 'Standard') { $directories += @("AitherZero/library/modules", "tests") } } elseif ($Language -eq 'Python') { $directories += @("src/$Name", "tests") } elseif ($Language -eq 'OpenTofu') { $directories += @("modules", "environments/dev", "environments/prod", "scripts", "tests", "AitherZero/config") } foreach ($dir in $directories) { $dirPath = Join-Path $fullPath $dir if (-not (Test-Path $dirPath)) { New-Item -Path $dirPath -ItemType Directory -Force | Out-Null } } # 2. Create Config (config.psd1) - Common $configPath = Join-Path $fullPath "AitherZero/config/config.psd1" if (-not (Test-Path $configPath) -or $Force) { $configContent = @" @{ Core = @{ Name = '$Name' Environment = 'Development' Version = '0.1.0' ProjectRoot = '$fullPath' Language = '$Language' } Automation = @{ ScriptPath = './automation-scripts' LogPath = './logs' } Logging = @{ Level = 'Information' Path = './logs' RetentionDays = 30 } Testing = @{ Path = './tests' } } "@ # Add Infrastructure section for OpenTofu if ($Language -eq 'OpenTofu') { $configContent = @" @{ Core = @{ Name = '$Name' Environment = 'Development' Version = '0.1.0' ProjectRoot = '$fullPath' Language = '$Language' } Infrastructure = @{ Provider = 'aws' # aws, hyperv, proxmox Region = 'us-east-1' # Mass deployment configuration example Resources = @{ VMs = @( @{ Name = 'web-01'; Size = 't3.micro'; Role = 'web' } @{ Name = 'db-01'; Size = 't3.medium'; Role = 'db' } ) Networks = @( @{ Name = 'vpc-main'; Cidr = '10.0.0.0/16' } ) } } Automation = @{ ScriptPath = './automation-scripts' LogPath = './logs' } Logging = @{ Level = 'Information' Path = './logs' RetentionDays = 30 } } "@ } Set-Content -Path $configPath -Value $configContent } # 3. Create Language Specific Files if ($Language -eq 'PowerShell') { $reqPath = Join-Path $fullPath "requirements.psd1" if (-not (Test-Path $reqPath) -or $Force) { $reqContent = @" @{ Modules = @{ 'AitherZero' = 'latest' 'Pester' = '5.0.0' } } "@ Set-Content -Path $reqPath -Value $reqContent } } elseif ($Language -eq 'Python') { # requirements.txt $reqPath = Join-Path $fullPath "requirements.txt" if (-not (Test-Path $reqPath) -or $Force) { $reqContent = @" pytest>=7.0.0 black>=23.0.0 flake8>=6.0.0 "@ Set-Content -Path $reqPath -Value $reqContent } # pyproject.toml $tomlPath = Join-Path $fullPath "pyproject.toml" if (-not (Test-Path $tomlPath) -or $Force) { $tomlContent = @" [project] name = "$Name" version = "0.1.0" description = "AitherZero Python Project" readme = "README.md" requires-python = ">=3.8" dependencies = [] [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [tool.pytest.ini_options] minversion = "6.0" addopts = "-ra -q" testpaths = [ "tests", ] "@ Set-Content -Path $tomlPath -Value $tomlContent } # __init__.py files New-Item -Path (Join-Path $fullPath "src/$Name/__init__.py") -ItemType File -Force | Out-Null New-Item -Path (Join-Path $fullPath "tests/__init__.py") -ItemType File -Force | Out-Null # main.py $mainPath = Join-Path $fullPath "src/$Name/main.py" if (-not (Test-Path $mainPath) -or $Force) { $mainContent = @" def main(): print("Hello from $Name!") if __name__ == "__main__": main() "@ Set-Content -Path $mainPath -Value $mainContent } # test_main.py $testPath = Join-Path $fullPath "tests/test_main.py" if (-not (Test-Path $testPath) -or $Force) { $testContent = @" import pytest from src.$Name.main import main def test_basic(): assert True "@ Set-Content -Path $testPath -Value $testContent } } elseif ($Language -eq 'OpenTofu') { # --- OpenTofu / Terraform Scaffolding --- # 1. versions.tf $versionsPath = Join-Path $fullPath "versions.tf" $versionsContent = @" terraform { required_version = ">= 1.6.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } azurerm = { source = "hashicorp/azurerm" version = "~> 3.0" } proxmox = { source = "Telmate/proxmox" version = "2.9.14" } hyperv = { source = "taliesins/hyperv" version = "1.0.3" } } } "@ Set-Content -Path $versionsPath -Value $versionsContent # 2. providers.tf $providersPath = Join-Path $fullPath "providers.tf" $providersContent = @" # Provider configurations # These are often configured via environment variables for auth provider "aws" { region = var.config.Infrastructure.Region } # provider "proxmox" { # pm_api_url = "https://proxmox.example.com:8006/api2/json" # } # provider "hyperv" { # user = "Administrator" # password = "CHANGE_ME" # host = "192.168.1.100" # } "@ Set-Content -Path $providersPath -Value $providersContent # 3. variables.tf $varsPath = Join-Path $fullPath "variables.tf" $varsContent = @" variable "config" { description = "Global configuration object imported from config.psd1" type = any } variable "environment" { description = "Deployment environment (dev, prod)" type = string default = "dev" } "@ Set-Content -Path $varsPath -Value $varsContent # 4. backend.tf $backendPath = Join-Path $fullPath "backend.tf" $backendContent = @" # Backend Configuration # By default, this uses local state. For remote state, uncomment and configure. terraform { # backend "local" { # path = "terraform.tfstate" # } # backend "s3" { # bucket = "my-terraform-state" # key = "infra/terraform.tfstate" # region = "us-east-1" # } } "@ Set-Content -Path $backendPath -Value $backendContent # 5. Sync Script (Config to Tofu Bridge) $syncScriptPath = Join-Path $fullPath "scripts/Sync-ConfigToTofu.ps1" $syncScriptContent = @" #Requires -Version 7.0 <# .SYNOPSIS Syncs config.psd1 to terraform.tfvars.json .DESCRIPTION Reads the AitherZero configuration manifest and exports it as a JSON variable file that OpenTofu can natively consume. This bridges the PowerShell config world with HCL. #> param( [string]`$ConfigPath = '../AitherZero/config/config.psd1', [string]`$OutputPath = '../terraform.tfvars.json' ) `$ErrorActionPreference = 'Stop' # Resolve paths `$root = `$PSScriptRoot `$absConfig = Join-Path `$root `$ConfigPath `$absOutput = Join-Path `$root `$OutputPath Write-Host "Reading configuration from `$absConfig..." if (-not (Test-Path `$absConfig)) { throw "Configuration file not found: `$absConfig" } `$config = Import-PowerShellDataFile -Path `$absConfig # Transform for Terraform (flatten if needed, or keep nested) # We wrap it in a 'config' object to match variables.tf `$tfVars = @{ config = `$config } Write-Host "Exporting to `$absOutput..." `$tfVars | ConvertTo-Json -Depth 10 | Set-Content -Path `$absOutput -Encoding UTF8 Write-Host "Done. You can now run 'tofu plan'." -ForegroundColor Green "@ Set-Content -Path $syncScriptPath -Value $syncScriptContent # 6. Terratest Setup $goModPath = Join-Path $fullPath "tests/go.mod" $goModContent = @" module test go 1.18 require ( github.com/gruntwork-io/terratest v0.41.0 github.com/stretchr/testify v1.8.1 ) "@ Set-Content -Path $goModPath -Value $goModContent $testFilePath = Join-Path $fullPath "tests/infrastructure_test.go" $testFileContent = @" package test import ( "testing" "github.com/gruntwork-io/terratest/modules/terraform" "github.com/stretchr/testify/assert" ) func TestInfrastructure(t *testing.T) { t.Parallel() terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ TerraformDir: "../", // Pass variables (mocking the config object) Vars: map[string]interface{}{ "config": map[string]interface{}{ "Infrastructure": map[string]interface{}{ "Region": "us-east-1", }, }, }, }) defer terraform.Destroy(t, terraformOptions) terraform.InitAndApply(t, terraformOptions) // Validate outputs (example) // output := terraform.Output(t, terraformOptions, "some_output") // assert.Equal(t, "expected_value", output) } "@ Set-Content -Path $testFilePath -Value $testFileContent } # 4. EditorConfig $editorConfigPath = Join-Path $fullPath ".editorconfig" if (-not (Test-Path $editorConfigPath) -or $Force) { $ecContent = @" root = true [*] charset = utf-8 indent_style = space indent_size = 4 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false [*.{yml,yaml}] indent_size = 2 [*.py] indent_style = space indent_size = 4 [*.{tf,tfvars,hcl}] indent_style = space indent_size = 2 "@ Set-Content -Path $editorConfigPath -Value $ecContent } # 5. Sample Script (Common) $scriptPath = Join-Path $fullPath "automation-scripts/0000_Initialize.ps1" if (-not (Test-Path $scriptPath) -or $Force) { $scriptContent = @" #Requires -Version 7.0 <# .SYNOPSIS Initialize the environment. .DESCRIPTION Bootstraps the project environment using AitherZero. #> [CmdletBinding()] param() Write-Host "Initializing $Name..." Write-Host "Environment configured successfully." "@ Set-Content -Path $scriptPath -Value $scriptContent } # 6. VS Code Integration if ($IncludeVSCode) { $vscodeDir = Join-Path $fullPath ".vscode" if (-not (Test-Path $vscodeDir)) { New-Item -Path $vscodeDir -ItemType Directory -Force | Out-Null } $settingsPath = Join-Path $vscodeDir "settings.json" $settingsContent = "" if ($Language -eq 'PowerShell') { $settingsContent = @" { "powershell.codeFormatting.preset": "OTBS", "powershell.integratedConsole.showOnStartup": false, "files.exclude": { "**/logs/**": true, "**/.git/**": true }, "search.exclude": { "**/logs/**": true } } "@ } elseif ($Language -eq 'Python') { $settingsContent = @" { "python.defaultInterpreterPath": "\${workspaceFolder}/.venv/bin/python", "python.analysis.typeCheckingMode": "basic", "editor.formatOnSave": true, "python.formatting.provider": "black", "files.exclude": { "**/logs/**": true, "**/.git/**": true, "**/__pycache__/**": true, "**/.venv/**": true } } "@ } elseif ($Language -eq 'OpenTofu') { $settingsContent = @" { "hashicorp.terraform.path": "tofu", "hashicorp.terraform.languageServer.enable": true, "editor.formatOnSave": true, "files.associations": { "*.tf": "terraform", "*.tfvars": "terraform", "*.tfvars.json": "terraform" }, "files.exclude": { "**/.terraform/**": true, "**/terraform.tfstate": true, "**/terraform.tfstate.backup": true } } "@ } Set-Content -Path $settingsPath -Value $settingsContent } # 7. CI/CD Integration if ($IncludeCI) { $githubDir = Join-Path $fullPath ".github/workflows" if (-not (Test-Path $githubDir)) { New-Item -Path $githubDir -ItemType Directory -Force | Out-Null } if ($Language -eq 'PowerShell') { $ciPath = Join-Path $githubDir "ci.yml" $ciContent = @" name: CI on: [push, pull_request] jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install PowerShell Modules shell: pwsh run: | Install-Module PSScriptAnalyzer -Force Install-Module Pester -Force - name: Lint Code shell: pwsh run: | Invoke-ScriptAnalyzer -Path . -Recurse -Severity Error - name: Run Tests shell: pwsh run: | if (Test-Path ./tests) { Invoke-Pester -Path ./tests -Passthru } "@ Set-Content -Path $ciPath -Value $ciContent } elseif ($Language -eq 'Python') { $ciPath = Join-Path $githubDir "python-ci.yml" $ciContent = @" name: Python CI on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.9' - name: Install dependencies run: | python -m pip install --upgrade pip pip install pytest flake8 black if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: | pytest "@ Set-Content -Path $ciPath -Value $ciContent } elseif ($Language -eq 'OpenTofu') { $ciPath = Join-Path $githubDir "tofu-ci.yml" $ciContent = @" name: OpenTofu CI on: [push, pull_request] jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup OpenTofu uses: opentofu/setup-opentofu@v1 - name: OpenTofu Init run: tofu init - name: OpenTofu Validate run: tofu validate - name: OpenTofu Plan run: tofu plan env: TF_VAR_config: '{"Infrastructure": {"Region": "us-east-1"}}' # Mock config for simple validation "@ Set-Content -Path $ciPath -Value $ciContent } } # 8. Default Module (PowerShell Only) if ($IncludeModule -and $Language -eq 'PowerShell') { $modulesPath = Join-Path $fullPath "AitherZero/library/modules" if (-not (Test-Path $modulesPath)) { New-Item -Path $modulesPath -ItemType Directory -Force | Out-Null } if (Get-Command New-AitherModule -ErrorAction SilentlyContinue) { New-AitherModule -Name "$Name.Core" -Path $modulesPath -Description "Core module for $Name" -Force } else { Write-AitherLog -Level Warning -Message "New-AitherModule cmdlet not found. Skipping module creation." -Source 'New-AitherProject' } } # 9. Git Init if ($GitInit) { if (Get-Command git -ErrorAction SilentlyContinue) { $currentLocation = Get-Location try { Set-Location $fullPath if (-not (Test-Path ".git")) { git init | Out-Null } # Create .gitignore $gitignorePath = ".gitignore" if (-not (Test-Path $gitignorePath) -or $Force) { $gitignoreContent = "" if ($Language -eq 'PowerShell') { $gitignoreContent = @" logs/ *.log config/*.local.psd1 .vscode/ bin/ dist/ *.tmp .DS_Store "@ } elseif ($Language -eq 'Python') { $gitignoreContent = @" logs/ *.log config/*.local.psd1 .vscode/ __pycache__/ *.py[cod] *$py.class .venv env/ venv/ ENV/ build/ dist/ *.egg-info/ .pytest_cache/ .coverage htmlcov/ .DS_Store "@ } elseif ($Language -eq 'OpenTofu') { $gitignoreContent = @" logs/ *.log config/*.local.psd1 .vscode/ .DS_Store .terraform/ terraform.tfstate terraform.tfstate.backup *.tfvars !*.example.tfvars *.tfvars.json crash.log override.tf override.tf.json *_override.tf *_override.tf.json .terraform.lock.hcl "@ } Set-Content -Path $gitignorePath -Value $gitignoreContent } Write-AitherLog -Level Information -Message "Initialized Git repository" -Source 'New-AitherProject' } catch { Write-AitherLog -Level Warning -Message "Failed to initialize git: $_" -Source 'New-AitherProject' -Exception $_ } finally { Set-Location $currentLocation } } } # 10. Register Project if ($RegisterProject -and (Get-Command Register-AitherProject -ErrorAction SilentlyContinue)) { Register-AitherProject -Name $Name -Path $fullPath -Language $Language -Template $Template Write-AitherLog -Level Information -Message "Registered project in AitherZero registry" -Source 'New-AitherProject' } Write-AitherLog -Level Information -Message "Project created successfully at $fullPath" -Source 'New-AitherProject' return Get-Item $fullPath } catch { Write-AitherLog -Level Error -Message "Failed to create project: $_" -Source 'New-AitherProject' -Exception $_ throw } finally { $script:AitherLogTargets = $originalLogTargets } } } } |