From 004fdde52884d1d27341ac00c9ed028e3c90173c Mon Sep 17 00:00:00 2001 From: Charles Date: Fri, 26 Jun 2026 03:08:21 +0200 Subject: [PATCH] Initialisation cadMasterCLI v1.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Description détaillée : Launcher WPF PowerShell pour piloter des scripts hébergés sur Gitea. Fonctionnalités principales : - Connexion API Gitea : liste les dépôts de l'organisation, téléchargement récursif (git/trees) - Détection intelligente des paramètres : types traduits en français, ValidateSet → RadioButtons, IsPath → bouton Parcourir + zone IPs - Fenêtre paramètres WPF : panneau README Markdown rendu (WebBrowser IE) à gauche (Star), formulaire fixe à droite, GridSplitter redimensionnable - Bouton Lancer : gris/désactivé si champs obligatoires manquants, vert si tout est rempli - Toggle README : la fenêtre avance son bord gauche, le formulaire ne bouge pas - Boucle principale : Annuler dans les paramètres revient au menu de sélection - Mode hors ligne : fallback sur le cache local si Gitea inaccessible - Convertisseur Markdown→HTML interne (sans dépendance externe) : titres, tableaux, blockquotes, listes, code - CONTRIBUTING_SCRIPTS.md + _template_script.ps1 : guide et gabarit pour les auteurs de scripts --- .gitignore | 6 + cadMasterCLI.ps1 | 1191 ++++++++++++++++++++++ doc_contribution/CONTRIBUTING_SCRIPTS.md | 93 ++ doc_contribution/_template_script.ps1 | 77 ++ 4 files changed, 1367 insertions(+) create mode 100644 .gitignore create mode 100644 cadMasterCLI.ps1 create mode 100644 doc_contribution/CONTRIBUTING_SCRIPTS.md create mode 100644 doc_contribution/_template_script.ps1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8442ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/.idea +/.claude +/plan +/scripts +*.log +*.csv diff --git a/cadMasterCLI.ps1 b/cadMasterCLI.ps1 new file mode 100644 index 0000000..9346a2f --- /dev/null +++ b/cadMasterCLI.ps1 @@ -0,0 +1,1191 @@ +#Requires -Version 5.1 + +# ===================================================================== +# CONFIGURATION +# ===================================================================== +$GITEA_BASE_URL = "https://git.cadjou.net" +$GITEA_ORG = "ScriptPowerShell" # Nom de l'organisation Gitea +$GITEA_BRANCH = "main" +$LOCAL_CACHE_DIR = "$PSScriptRoot\scripts" + +# ===================================================================== +# UTILITAIRES +# ===================================================================== + +function Write-Log { + param( + [string]$Message, + [ValidateSet("INFO", "WARN", "ERROR", "OK")] + [string]$Level = "INFO" + ) + $ts = Get-Date -Format "HH:mm:ss" + $tag = switch ($Level) { "INFO" {"[INFO ]"} "WARN" {"[WARN ]"} "ERROR" {"[ERR ]"} "OK" {"[ OK ]"} } + $color = switch ($Level) { "INFO" {"Cyan"} "WARN" {"Yellow"} "ERROR" {"Red"} "OK" {"Green"} } + Write-Host "$ts $tag $Message" -ForegroundColor $color +} + +function Show-MessageBox { + param( + [string]$Message, + [string]$Title = "cadMasterCLI", + [string]$Type = "Info" + ) + Add-Type -AssemblyName PresentationFramework + $icon = switch ($Type) { + "Warning" { [System.Windows.MessageBoxImage]::Warning } + "Error" { [System.Windows.MessageBoxImage]::Error } + "Question" { [System.Windows.MessageBoxImage]::Question } + default { [System.Windows.MessageBoxImage]::Information } + } + $buttons = if ($Type -eq "Question") { + [System.Windows.MessageBoxButton]::YesNo + } else { + [System.Windows.MessageBoxButton]::OK + } + return [System.Windows.MessageBox]::Show($Message, $Title, $buttons, $icon) +} + +# ===================================================================== +# FONCTION : Get-ScriptList +# Recupere la liste des depots de l'organisation Gitea +# ===================================================================== +function Get-ScriptList { + $page = 1 + $limit = 50 + $allRepos = @() + + try { + Write-Log "Connexion a Gitea (organisation : $GITEA_ORG)..." + do { + $url = "$GITEA_BASE_URL/api/v1/orgs/$GITEA_ORG/repos?limit=$limit&page=$page" + $response = Invoke-RestMethod -Uri $url -Method Get -TimeoutSec 10 -ErrorAction Stop + $allRepos += $response + $page++ + } while ($response.Count -eq $limit) + + if ($allRepos.Count -eq 0) { + Write-Log "Aucun depot trouve dans l'organisation $GITEA_ORG" "WARN" + return @() + } + + Write-Log "$($allRepos.Count) script(s) trouve(s) sur Gitea" "OK" + return $allRepos | ForEach-Object { + [PSCustomObject]@{ + Nom = $_.name + Description = $_.description + DefaultBranch = if ($_.default_branch) { $_.default_branch } else { $GITEA_BRANCH } + } + } + } + catch { + Write-Log "API Gitea inaccessible : $($_.Exception.Message)" "ERROR" + return $null + } +} + +# ===================================================================== +# FONCTION : Get-ScriptReadme +# Telecharge le README.md a la racine du depot du script +# ===================================================================== +function Get-ScriptReadme { + param( + [string]$ScriptName, + [string]$Branch = $GITEA_BRANCH + ) + $url = "$GITEA_BASE_URL/api/v1/repos/$GITEA_ORG/$ScriptName/raw/README.md?ref=$Branch" + try { + return [string](Invoke-RestMethod -Uri $url -Method Get -TimeoutSec 10 -ErrorAction Stop) + } + catch { return $null } +} + +# ===================================================================== +# FONCTION : Get-ReadmeSummary +# Extrait la premiere ligne de description d'un README +# ===================================================================== +function Get-ReadmeSummary { + param([string]$ReadmeContent) + + if ([string]::IsNullOrWhiteSpace($ReadmeContent)) { return "Aucune description disponible" } + + $lines = $ReadmeContent -split "`n" + $foundTitle = $false + foreach ($line in $lines) { + $t = $line.Trim() + if ($t -eq "") { continue } + if ($t -match "^#") { $foundTitle = $true; continue } + if ($foundTitle) { return $t -replace "\*\*|__|~~|``", "" } + } + + $first = ($lines | Where-Object { $_.Trim() -ne "" } | Select-Object -First 1) + if ($first) { return $first.Trim().TrimStart('#').Trim() } + return "Aucune description disponible" +} + +# ===================================================================== +# FONCTION : Get-ScriptLastCommitDate +# Recupere la date du dernier commit du depot du script +# ===================================================================== +function Get-ScriptLastCommitDate { + param( + [string]$ScriptName, + [string]$Branch = $GITEA_BRANCH + ) + $url = "$GITEA_BASE_URL/api/v1/repos/$GITEA_ORG/$ScriptName/commits?limit=1&sha=$Branch" + try { + $commits = Invoke-RestMethod -Uri $url -Method Get -TimeoutSec 10 -ErrorAction Stop + if ($commits -and @($commits).Count -gt 0) { + return [DateTime]::Parse($commits[0].commit.committer.date) + } + } + catch { } + return $null +} + +# ===================================================================== +# FONCTION : Test-ScriptUpdate +# Retourne $true si la version Gitea est plus recente que le cache +# ===================================================================== +function Test-ScriptUpdate { + param( + [string]$ScriptName, + [Nullable[DateTime]]$RemoteDate + ) + if (-not $RemoteDate) { return $false } + $localFile = Get-ChildItem -Path "$LOCAL_CACHE_DIR\$ScriptName\" -Filter "*.ps1" -ErrorAction SilentlyContinue | + Select-Object -First 1 + if (-not $localFile) { return $false } + return $RemoteDate.ToUniversalTime() -gt $localFile.LastWriteTimeUtc +} + +# ===================================================================== +# FONCTION : Find-ScriptPs1Name +# Cherche le nom du fichier .ps1 a la racine du depot +# ===================================================================== +function Find-ScriptPs1Name { + param( + [string]$ScriptName, + [string]$Branch = $GITEA_BRANCH + ) + $url = "$GITEA_BASE_URL/api/v1/repos/$GITEA_ORG/$ScriptName/contents/?ref=$Branch" + try { + $contents = Invoke-RestMethod -Uri $url -Method Get -TimeoutSec 10 -ErrorAction Stop + $ps1File = $contents | Where-Object { $_.type -eq "file" -and $_.name -like "*.ps1" } | + Select-Object -First 1 + if ($ps1File) { return $ps1File.name } + } + catch { } + return "$ScriptName.ps1" +} + +# ===================================================================== +# FONCTION : Invoke-ScriptDownload +# Telecharge la totalite du depot via l'API git/trees recursive +# ===================================================================== +function Invoke-ScriptDownload { + param( + [string]$ScriptName, + [string]$Branch = $GITEA_BRANCH + ) + + $targetDir = "$LOCAL_CACHE_DIR\$ScriptName" + + try { + $branchInfo = Invoke-RestMethod -Uri "$GITEA_BASE_URL/api/v1/repos/$GITEA_ORG/$ScriptName/branches/$Branch" ` + -Method Get -TimeoutSec 10 -ErrorAction Stop + $treeSha = $branchInfo.commit.id + } + catch { + Write-Log "Impossible de lire la branche $Branch de $ScriptName : $($_.Exception.Message)" "ERROR" + return $false + } + + try { + $treeData = Invoke-RestMethod -Uri "$GITEA_BASE_URL/api/v1/repos/$GITEA_ORG/$ScriptName/git/trees/$treeSha`?recursive=true" ` + -Method Get -TimeoutSec 15 -ErrorAction Stop + $files = @($treeData.tree | Where-Object { $_.type -eq "blob" }) + } + catch { + Write-Log "Impossible de lister les fichiers du depot $ScriptName : $($_.Exception.Message)" "ERROR" + return $false + } + + Write-Log "$($files.Count) fichier(s) a telecharger pour $ScriptName..." + + if (-not (Test-Path $targetDir)) { New-Item -ItemType Directory -Path $targetDir -Force | Out-Null } + + $ps1FileName = $null + $mainScriptOk = $false + + foreach ($file in $files) { + $filePath = $file.path + $localPath = Join-Path $targetDir ($filePath -replace "/", "\") + $localDir = Split-Path $localPath -Parent + + if (-not (Test-Path $localDir)) { New-Item -ItemType Directory -Path $localDir -Force | Out-Null } + + $rawUrl = "$GITEA_BASE_URL/$GITEA_ORG/$ScriptName/raw/branch/$Branch/$filePath" + + try { + Invoke-WebRequest -Uri $rawUrl -OutFile $localPath -ErrorAction Stop + Unblock-File -Path $localPath -ErrorAction SilentlyContinue + if ($filePath -like "*.ps1" -and $filePath -notlike "*/*") { + $ps1FileName = Split-Path $localPath -Leaf + $mainScriptOk = $true + } + } + catch { + Write-Log "Erreur telechargement $filePath : $($_.Exception.Message)" "WARN" + if ($filePath -like "*.ps1" -and $filePath -notlike "*/*") { + Write-Log "CRITIQUE : echec du telechargement du script principal" "ERROR" + return $false + } + } + } + + if (-not $ps1FileName) { $ps1FileName = "$ScriptName.ps1" } + $ps1FileName | Set-Content -Path "$targetDir\.ps1name" -Encoding UTF8 + Write-Log "$ScriptName telecharge : $($files.Count) fichier(s)." "OK" + return $mainScriptOk +} + +# ===================================================================== +# FONCTION : Show-ScriptMenu +# Affiche le menu Out-GridView de selection des scripts +# ===================================================================== +function Show-ScriptMenu { + param([array]$Scripts) + + $items = $Scripts | ForEach-Object { + [PSCustomObject]@{ + "Nom" = $_.Nom + "Description" = $_.Description + "Mis a jour" = if ($_.DateGitea) { $_.DateGitea.ToString("dd/MM/yyyy HH:mm") } else { "Inconnu" } + "Cache local" = if (Get-ChildItem "$LOCAL_CACHE_DIR\$($_.Nom)\" -Filter "*.ps1" -ErrorAction SilentlyContinue) { "Oui" } else { "Non" } + "MAJ dispo" = if ($_.MajDispo) { "[NOUVEAU]" } else { "" } + } + } + + return $items | Out-GridView -Title "cadMasterCLI - Selectionnez un script a executer" -PassThru +} + +# ===================================================================== +# FONCTION : ConvertTo-WpfDataTable +# Convertit les premieres lignes d'un CSV en DataTable pour le DataGrid +# ===================================================================== +function ConvertTo-WpfDataTable { + param([string]$CsvPath) + + $table = New-Object System.Data.DataTable + try { + $rows = @(Import-Csv -Path $CsvPath -Encoding UTF8 | Select-Object -First 10) + if ($rows.Count -gt 0) { + foreach ($prop in $rows[0].PSObject.Properties) { $table.Columns.Add($prop.Name) | Out-Null } + foreach ($row in $rows) { + $newRow = $table.NewRow() + foreach ($prop in $row.PSObject.Properties) { $newRow[$prop.Name] = $prop.Value } + $table.Rows.Add($newRow) + } + } + } + catch { } + return $table +} + +# ===================================================================== +# FONCTION : Convert-IpListToCsv +# Convertit une liste d'IPs collee en fichier CSV +# ===================================================================== +function Convert-IpListToCsv { + param([string]$IpListText) + + $ips = $IpListText -split "[,;\s\r\n]+" | + ForEach-Object { $_.Trim() } | + Where-Object { $_ -match "^\d{1,3}(\.\d{1,3}){3}(/\d{1,2})?$" } + + if ($ips.Count -eq 0) { return $null } + + $csvPath = "$PSScriptRoot\ips_temp.csv" + $ips | ForEach-Object { [PSCustomObject]@{ IP = $_ } } | + Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8 + + Write-Log "$($ips.Count) IP(s) converties -> $csvPath" "OK" + return $csvPath +} + +# ===================================================================== +# FONCTION : ConvertTo-FriendlyTypeName +# Traduit les types .NET en libelles lisibles (WPF UI, accents OK) +# ===================================================================== +function ConvertTo-FriendlyTypeName { + param([string]$TypeName) + switch ($TypeName) { + "String" { return "Texte" } + "Int32" { return "Nombre entier" } + "Int64" { return "Nombre entier" } + "Double" { return "Nombre décimal" } + "Single" { return "Nombre décimal" } + "Boolean" { return "Vrai/Faux" } + "DateTime" { return "Date" } + "FileInfo" { return "Fichier" } + default { return $TypeName } + } +} + +# ===================================================================== +# FONCTION : Get-ScriptParamHelp +# Extrait les descriptions de parametres depuis le comment-based help +# ===================================================================== +function Get-ScriptParamHelp { + param([string]$ScriptPath) + + $result = @{} + try { + $help = Get-Help $ScriptPath -ErrorAction Stop + if ($help.parameters -and $help.parameters.parameter) { + foreach ($param in @($help.parameters.parameter)) { + if ($param.description) { + $desc = ($param.description | ForEach-Object { $_.Text }) -join " " + if ($desc.Trim()) { $result[$param.name] = $desc.Trim() } + } + } + } + } + catch { } + return $result +} + +# ===================================================================== +# FONCTION : ConvertTo-SimpleHtml +# Convertit du Markdown GFM en HTML pour WebBrowser WPF. +# Supporte : titres h1/h2/h3, gras/italique, code inline, blocs de code, +# listes, tableaux GFM, blockquotes, separateurs. +# Pas de dependance externe - utilise System.Net.WebUtility. +# ===================================================================== +function ConvertTo-SimpleHtml { + param([string]$Markdown) + + if ([string]::IsNullOrWhiteSpace($Markdown)) { + return "

Aucune documentation disponible.

" + } + + $css = "body{font-family:'Segoe UI',sans-serif;font-size:13px;padding:12px 16px;color:#222;line-height:1.6;margin:0}" + + "h1{font-size:18px;border-bottom:2px solid #388E3C;padding-bottom:4px;color:#1a1a1a;margin-top:8px}" + + "h2{font-size:15px;color:#1565C0;margin-top:16px}" + + "h3{font-size:13px;color:#555;margin-top:12px}" + + "code{background:#f0f0f0;border-radius:3px;padding:1px 5px;font-family:Consolas,monospace;font-size:12px}" + + "pre{background:#f4f4f4;border:1px solid #ddd;border-radius:4px;padding:10px;overflow-x:auto;margin:6px 0}" + + "pre code{background:none;padding:0}" + + "hr{border:none;border-top:1px solid #ddd;margin:12px 0}" + + "ul{padding-left:20px;margin:4px 0}li{margin-bottom:2px}" + + "blockquote{border-left:3px solid #388E3C;margin:8px 0;padding:4px 12px;color:#555;background:#f9f9f9}" + + "blockquote p{margin:2px 0}" + + "table{border-collapse:collapse;width:100%;margin:8px 0;font-size:12px}" + + "th{background:#1565C0;color:#fff;padding:6px 10px;text-align:left}" + + "td{padding:5px 10px;border-bottom:1px solid #eee}" + + "tr:nth-child(even) td{background:#f5f5f5}" + + # Formate gras, italique et code inline sur une chaine deja encodee HTML + $applyInline = { + param([string]$s) + $s = $s -replace "\*\*(.+?)\*\*", "`$1" + $s = $s -replace "\*(.+?)\*", "`$1" + $s = $s -replace "``(.+?)``", "`$1" + return $s + } + + # Convertit un buffer de lignes brutes de tableau GFM en HTML + $flushTable = { + param([System.Collections.Generic.List[string]]$buf) + if ($buf.Count -eq 0) { return "" } + $html = "" + $isHeader = $true + foreach ($row in $buf) { + if ($row -match "^\s*\|[\s\-\|:]+$") { continue } + $cells = $row -split "\|" | Where-Object { $_ -ne "" } | ForEach-Object { $_.Trim() } + if ($isHeader) { + $html += "" + foreach ($c in $cells) { $html += "" } + $html += "" + $isHeader = $false + } else { + $html += "" + foreach ($c in $cells) { $html += "" } + $html += "" + } + } + $html += "
$([System.Net.WebUtility]::HtmlEncode($c))
$([System.Net.WebUtility]::HtmlEncode($c))
" + return $html + } + + $sb = New-Object System.Text.StringBuilder + $inCodeBlock = $false + $inTable = $false + $tableBuffer = [System.Collections.Generic.List[string]]::new() + $listOpen = $false + $bqOpen = $false + + $sb.Append("") | Out-Null + + foreach ($line in ($Markdown -split "`n")) { + $raw = $line.TrimEnd() + + # Toggle bloc de code + if ($raw -match "^``````") { + if ($listOpen) { $sb.Append("") | Out-Null; $listOpen = $false } + if ($bqOpen) { $sb.Append("") | Out-Null; $bqOpen = $false } + if ($inTable) { $sb.Append((& $flushTable $tableBuffer)) | Out-Null; $tableBuffer.Clear(); $inTable = $false } + if ($inCodeBlock) { $sb.Append("") | Out-Null; $inCodeBlock = $false } + else { $sb.Append("
")   | Out-Null; $inCodeBlock = $true  }
+            continue
+        }
+
+        if ($inCodeBlock) {
+            $sb.Append([System.Net.WebUtility]::HtmlEncode($raw) + "`n") | Out-Null
+            continue
+        }
+
+        # Tableau GFM : bufferiser les lignes commencant par |
+        if ($raw -match "^\s*\|") {
+            if ($listOpen) { $sb.Append("")         | Out-Null; $listOpen = $false }
+            if ($bqOpen)   { $sb.Append("") | Out-Null; $bqOpen   = $false }
+            $tableBuffer.Add($raw)
+            $inTable = $true
+            continue
+        }
+        if ($inTable) {
+            $sb.Append((& $flushTable $tableBuffer)) | Out-Null
+            $tableBuffer.Clear()
+            $inTable = $false
+        }
+
+        # Blockquote
+        if ($raw -match "^>[ ]?(.*)") {
+            if ($listOpen)    { $sb.Append("") | Out-Null; $listOpen = $false }
+            if (-not $bqOpen) { $sb.Append("
") | Out-Null; $bqOpen = $true } + $content = & $applyInline ([System.Net.WebUtility]::HtmlEncode($Matches[1])) + $sb.Append("

$content

") | Out-Null + continue + } + if ($bqOpen) { $sb.Append("
") | Out-Null; $bqOpen = $false } + + # Fermer la liste si la ligne n'est pas un item + if ($listOpen -and $raw -notmatch "^- ") { + $sb.Append("") | Out-Null + $listOpen = $false + } + + # Elements structurels (detection sur ligne brute avant encodage) + if ($raw -match "^# (.+)") { $sb.Append("

$(& $applyInline ([System.Net.WebUtility]::HtmlEncode($Matches[1])))

") | Out-Null } + elseif ($raw -match "^## (.+)") { $sb.Append("

$(& $applyInline ([System.Net.WebUtility]::HtmlEncode($Matches[1])))

") | Out-Null } + elseif ($raw -match "^### (.+)") { $sb.Append("

$(& $applyInline ([System.Net.WebUtility]::HtmlEncode($Matches[1])))

") | Out-Null } + elseif ($raw -match "^---$") { $sb.Append("
") | Out-Null } + elseif ($raw -match "^- (.+)") { + if (-not $listOpen) { $sb.Append("
    ") | Out-Null; $listOpen = $true } + $sb.Append("
  • $(& $applyInline ([System.Net.WebUtility]::HtmlEncode($Matches[1])))
  • ") | Out-Null + } + elseif ($raw.Trim() -eq "") { $sb.Append("
    ") | Out-Null } + else { $sb.Append("

    $(& $applyInline ([System.Net.WebUtility]::HtmlEncode($raw)))

    ") | Out-Null } + } + + # Flush des etats ouverts en fin de document + if ($inTable) { $sb.Append((& $flushTable $tableBuffer)) | Out-Null } + if ($listOpen) { $sb.Append("
") | Out-Null } + if ($bqOpen) { $sb.Append("") | Out-Null } + if ($inCodeBlock){ $sb.Append("
") | Out-Null } + + $sb.Append("") | Out-Null + return $sb.ToString() +} + +# ===================================================================== +# FONCTION : Show-ParametersWindow +# Fenetre WPF avec panneau README redimensionnable a gauche (ouvert par +# defaut) et formulaire de parametres a droite. +# Retourne un hashtable pret pour le splatting, ou $null si annule. +# ===================================================================== +function Show-ParametersWindow { + param( + [string]$ScriptName, + [string]$ScriptPath, + [string]$ReadmeContent = "" + ) + + Add-Type -AssemblyName PresentationFramework + Add-Type -AssemblyName System.Windows.Forms + Add-Type -AssemblyName System.Data + + # --- Lire les parametres reels du script via Get-Command --- + $commonParams = 'Verbose','Debug','ErrorAction','WarningAction','ErrorVariable', + 'WarningVariable','OutVariable','OutBuffer','PipelineVariable', + 'InformationAction','InformationVariable','WhatIf','Confirm' + + $scriptParams = @() + try { + $cmd = Get-Command $ScriptPath -ErrorAction Stop + $scriptParams = @($cmd.Parameters.Values | + Where-Object { $_.Name -notin $commonParams } | + ForEach-Object { + $attr = $_.Attributes | + Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] } | + Select-Object -First 1 + $vsAttr = $_.Attributes | + Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } | + Select-Object -First 1 + [PSCustomObject]@{ + Name = $_.Name + TypeName = $_.ParameterType.Name + Mandatory = [bool]($attr -and $attr.Mandatory) + IsSwitch = ($_.ParameterType -eq [System.Management.Automation.SwitchParameter]) + IsPath = ($_.Name -imatch 'Path|File|Csv|Input|Output') -or ($_.ParameterType -eq [System.IO.FileInfo]) + IsCsv = ($_.Name -imatch 'Csv|Input') + ValidValues = if ($vsAttr) { @($vsAttr.ValidValues) } else { $null } + } + } | Sort-Object { -[int]$_.Mandatory }, Name) + } + catch { + Write-Log "Impossible de lire les parametres de $ScriptName : $($_.Exception.Message)" "WARN" + return @{} + } + + if ($scriptParams.Count -eq 0) { + Write-Log "Aucun parametre detecte pour $ScriptName, lancement direct." "WARN" + return @{} + } + + Write-Log "$($scriptParams.Count) parametre(s) detecte(s) pour $ScriptName" "OK" + + $paramHelp = Get-ScriptParamHelp -ScriptPath $ScriptPath + $htmlContent = ConvertTo-SimpleHtml -Markdown $ReadmeContent + + # Etat partage entre closures (hashtable = reference, persiste entre appels) + $state = @{ + ReadmeVisible = $true + LastWindowWidth = 1100 + LastWindowLeft = [double]::NaN + } + + # --- Construire la fenetre WPF --- + # Layout : col0=README (Star, gauche), col1=GridSplitter (5px), col2=formulaire (fixe, droite) + $formWidth = 660 + + $w = New-Object System.Windows.Window + $w.Title = "cadMasterCLI - $ScriptName" + $w.Width = $state.LastWindowWidth + $w.MinWidth = $formWidth + 20 + $w.SizeToContent = "Height" + $w.MinHeight = 480 + $w.MaxHeight = 860 + $w.WindowStartupLocation = "CenterScreen" + $w.ResizeMode = "CanResize" + + $outerGrid = New-Object System.Windows.Controls.Grid + $col0 = New-Object System.Windows.Controls.ColumnDefinition + $col0.Width = New-Object System.Windows.GridLength(1, [System.Windows.GridUnitType]::Star) + $col0.MinWidth = 0 + $col1 = New-Object System.Windows.Controls.ColumnDefinition + $col1.Width = New-Object System.Windows.GridLength(5) + $col2 = New-Object System.Windows.Controls.ColumnDefinition + $col2.Width = New-Object System.Windows.GridLength($formWidth) + $col2.MinWidth = 350 + $outerGrid.ColumnDefinitions.Add($col0) + $outerGrid.ColumnDefinitions.Add($col1) + $outerGrid.ColumnDefinitions.Add($col2) + $w.Content = $outerGrid + + # --- Panneau README (col0, gauche, Star) --- + $readmeBorder = New-Object System.Windows.Controls.Border + $readmeBorder.BorderBrush = New-Object System.Windows.Media.SolidColorBrush( + [System.Windows.Media.Color]::FromRgb(204, 204, 204)) + $readmeBorder.BorderThickness = New-Object System.Windows.Thickness(0, 0, 1, 0) + [System.Windows.Controls.Grid]::SetColumn($readmeBorder, 0) + $outerGrid.Children.Add($readmeBorder) | Out-Null + + $wb = New-Object System.Windows.Controls.WebBrowser + $readmeBorder.Child = $wb + + # Charger le HTML apres initialisation de la fenetre + $w.Add_Loaded({ $wb.NavigateToString($htmlContent) }.GetNewClosure()) + + # --- GridSplitter entre README et formulaire (col1) --- + $splitter = New-Object System.Windows.Controls.GridSplitter + $splitter.Width = 5 + $splitter.HorizontalAlignment = "Stretch" + $splitter.VerticalAlignment = "Stretch" + $splitter.ResizeBehavior = "PreviousAndNext" + $splitter.Background = New-Object System.Windows.Media.SolidColorBrush( + [System.Windows.Media.Color]::FromRgb(220, 220, 220)) + [System.Windows.Controls.Grid]::SetColumn($splitter, 1) + $outerGrid.Children.Add($splitter) | Out-Null + + # --- Panneau formulaire (col2, droite, fixe) --- + $formPanel = New-Object System.Windows.Controls.DockPanel + $formPanel.Margin = New-Object System.Windows.Thickness(16) + [System.Windows.Controls.Grid]::SetColumn($formPanel, 2) + $outerGrid.Children.Add($formPanel) | Out-Null + + # Boutons du bas + $footer = New-Object System.Windows.Controls.DockPanel + $footer.Margin = New-Object System.Windows.Thickness(0, 14, 0, 0) + [System.Windows.Controls.DockPanel]::SetDock($footer, [System.Windows.Controls.Dock]::Bottom) + + $greenBrush = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(56, 142, 60)) + $grayBrush = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(189, 189, 189)) + + $btnLancer = New-Object System.Windows.Controls.Button + $btnLancer.Content = "Lancer le script" + $btnLancer.Width = 160 + $btnLancer.Padding = New-Object System.Windows.Thickness(10, 6, 10, 6) + $btnLancer.Margin = New-Object System.Windows.Thickness(4, 0, 0, 0) + $btnLancer.Cursor = [System.Windows.Input.Cursors]::Hand + $btnLancer.Background = $grayBrush + $btnLancer.Foreground = [System.Windows.Media.Brushes]::White + $btnLancer.FontWeight = [System.Windows.FontWeights]::Bold + [System.Windows.Controls.DockPanel]::SetDock($btnLancer, [System.Windows.Controls.Dock]::Right) + + $btnAnnuler = New-Object System.Windows.Controls.Button + $btnAnnuler.Content = "Annuler" + $btnAnnuler.Width = 100 + $btnAnnuler.Padding = New-Object System.Windows.Thickness(10, 6, 10, 6) + $btnAnnuler.Margin = New-Object System.Windows.Thickness(4, 0, 0, 0) + $btnAnnuler.Cursor = [System.Windows.Input.Cursors]::Hand + [System.Windows.Controls.DockPanel]::SetDock($btnAnnuler, [System.Windows.Controls.Dock]::Right) + + $statusLbl = New-Object System.Windows.Controls.TextBlock + $statusLbl.Text = "* Champs obligatoires" + $statusLbl.Foreground = [System.Windows.Media.Brushes]::Gray + $statusLbl.FontSize = 12 + $statusLbl.VerticalAlignment = "Center" + $statusLbl.Padding = New-Object System.Windows.Thickness(4, 0, 0, 0) + $statusLbl.TextWrapping = "Wrap" + + $footer.Children.Add($btnLancer) | Out-Null + $footer.Children.Add($btnAnnuler) | Out-Null + $footer.Children.Add($statusLbl) | Out-Null + $formPanel.Children.Add($footer) | Out-Null + + # Header : titre puis bouton README en dessous (a gauche) + $header = New-Object System.Windows.Controls.StackPanel + $header.Margin = New-Object System.Windows.Thickness(0, 0, 0, 12) + [System.Windows.Controls.DockPanel]::SetDock($header, [System.Windows.Controls.Dock]::Top) + + $titleBlock = New-Object System.Windows.Controls.TextBlock + $titleBlock.Text = "Paramètres de : $ScriptName" + $titleBlock.FontSize = 16 + $titleBlock.FontWeight = [System.Windows.FontWeights]::Bold + $titleBlock.Margin = New-Object System.Windows.Thickness(0, 0, 0, 6) + $header.Children.Add($titleBlock) | Out-Null + + $btnReadme = New-Object System.Windows.Controls.Button + $btnReadme.Content = "Fermer README" + $btnReadme.HorizontalAlignment = "Left" + $btnReadme.Padding = New-Object System.Windows.Thickness(10, 4, 10, 4) + $btnReadme.Cursor = [System.Windows.Input.Cursors]::Hand + $btnReadme.FontSize = 11 + $header.Children.Add($btnReadme) | Out-Null + + $formPanel.Children.Add($header) | Out-Null + + # Zone scrollable des parametres + $scroll = New-Object System.Windows.Controls.ScrollViewer + $scroll.VerticalScrollBarVisibility = "Auto" + $paramPanel = New-Object System.Windows.Controls.StackPanel + $scroll.Content = $paramPanel + $formPanel.Children.Add($scroll) | Out-Null + + # Logique toggle README + # Fermer : la fenetre se retrecit par son bord GAUCHE (Left avance vers la droite). + # Le bord droit de la fenetre et le formulaire restent exactement au meme endroit a l'ecran. + $btnReadme.Add_Click({ + if ($state.ReadmeVisible) { + $state.LastWindowWidth = [int]$w.Width + $state.LastWindowLeft = $w.Left + $readmePx = [int]$col0.ActualWidth + 5 # README + splitter + $col0.Width = New-Object System.Windows.GridLength(0) + $col1.Width = New-Object System.Windows.GridLength(0) + $w.Left = $w.Left + $readmePx + $w.Width = $state.LastWindowWidth - $readmePx + $w.MinHeight = 200 + $state.ReadmeVisible = $false + $btnReadme.Content = "Ouvrir README" + } else { + $col0.Width = New-Object System.Windows.GridLength(1, [System.Windows.GridUnitType]::Star) + $col1.Width = New-Object System.Windows.GridLength(5) + $w.Width = $state.LastWindowWidth + $w.Left = $state.LastWindowLeft + $w.MinHeight = 480 + $state.ReadmeVisible = $true + $btnReadme.Content = "Fermer README" + $wb.NavigateToString($htmlContent) + } + }.GetNewClosure()) + + # Dictionnaire nom -> controle pour lire les valeurs apres ShowDialog + $controlMap = @{} + + foreach ($p in $scriptParams) { + $group = New-Object System.Windows.Controls.StackPanel + $group.Margin = New-Object System.Windows.Thickness(0, 6, 0, 12) + + # Type traduit et affichage dans le label + $friendlyType = ConvertTo-FriendlyTypeName -TypeName $p.TypeName + $typeDisplay = if ($p.ValidValues) { $p.ValidValues -join " / " } else { $friendlyType } + + # Label + $lbl = New-Object System.Windows.Controls.TextBlock + $lbl.FontSize = 13 + $lbl.Margin = New-Object System.Windows.Thickness(0, 0, 0, 4) + if ($p.Mandatory) { + $lbl.Text = "$($p.Name) * [$typeDisplay] - Obligatoire" + $lbl.FontWeight = [System.Windows.FontWeights]::SemiBold + } else { + $lbl.Text = "$($p.Name) [$typeDisplay] - Optionnel" + $lbl.Foreground = [System.Windows.Media.Brushes]::Gray + } + $group.Children.Add($lbl) | Out-Null + + # Aide expandable si description disponible dans le comment-based help + $helpText = $paramHelp[$p.Name] + if ($helpText) { + $helpBlock = New-Object System.Windows.Controls.TextBlock + $helpBlock.Text = $helpText + $helpBlock.Foreground = [System.Windows.Media.Brushes]::DimGray + $helpBlock.FontSize = 11 + $helpBlock.TextWrapping = "Wrap" + $helpBlock.Margin = New-Object System.Windows.Thickness(0, 2, 0, 4) + $helpBlock.Visibility = [System.Windows.Visibility]::Collapsed + + $btnHelp = New-Object System.Windows.Controls.Button + $btnHelp.Content = "[?] Voir l'aide" + $btnHelp.Background = [System.Windows.Media.Brushes]::Transparent + $btnHelp.BorderThickness = New-Object System.Windows.Thickness(0) + $btnHelp.Foreground = [System.Windows.Media.Brushes]::SteelBlue + $btnHelp.FontSize = 11 + $btnHelp.Cursor = [System.Windows.Input.Cursors]::Hand + $btnHelp.HorizontalAlignment = "Left" + $btnHelp.Margin = New-Object System.Windows.Thickness(0, 0, 0, 2) + + $btnHelp.Add_Click({ + if ($helpBlock.Visibility -eq [System.Windows.Visibility]::Collapsed) { + $helpBlock.Visibility = [System.Windows.Visibility]::Visible + $btnHelp.Content = "[^] Masquer l'aide" + } else { + $helpBlock.Visibility = [System.Windows.Visibility]::Collapsed + $btnHelp.Content = "[?] Voir l'aide" + } + }.GetNewClosure()) + + $group.Children.Add($btnHelp) | Out-Null + $group.Children.Add($helpBlock) | Out-Null + } + + # Controle selon le type detecte + if ($p.ValidValues) { + # ValidateSet -> RadioButtons horizontaux + $rbPanel = New-Object System.Windows.Controls.WrapPanel + $rbPanel.Orientation = "Horizontal" + $rbPanel.Margin = New-Object System.Windows.Thickness(0, 4, 0, 0) + $firstRb = $null + foreach ($val in $p.ValidValues) { + $rb = New-Object System.Windows.Controls.RadioButton + $rb.Content = $val + $rb.Margin = New-Object System.Windows.Thickness(0, 0, 16, 0) + $rb.GroupName = $p.Name + $rb.FontSize = 13 + if (-not $firstRb) { $rb.IsChecked = $true; $firstRb = $rb } + $rbPanel.Children.Add($rb) | Out-Null + } + $group.Children.Add($rbPanel) | Out-Null + $controlMap[$p.Name] = @{ Type = "Radio"; Panel = $rbPanel } + } + elseif ($p.IsSwitch) { + $cb = New-Object System.Windows.Controls.CheckBox + $cb.Content = "Activer" + $cb.Margin = New-Object System.Windows.Thickness(4, 0, 0, 0) + $group.Children.Add($cb) | Out-Null + $controlMap[$p.Name] = @{ Type = "Switch"; Control = $cb } + } + elseif ($p.IsPath) { + $dock = New-Object System.Windows.Controls.DockPanel + $btnB = New-Object System.Windows.Controls.Button + $btnB.Content = "Parcourir..." + $btnB.Width = 110 + $btnB.Padding = New-Object System.Windows.Thickness(8, 5, 8, 5) + $btnB.Margin = New-Object System.Windows.Thickness(4, 0, 0, 0) + $btnB.Cursor = [System.Windows.Input.Cursors]::Hand + [System.Windows.Controls.DockPanel]::SetDock($btnB, [System.Windows.Controls.Dock]::Right) + $txtPath = New-Object System.Windows.Controls.TextBox + $txtPath.FontSize = 12 + $txtPath.Padding = New-Object System.Windows.Thickness(6) + $txtPath.Background = [System.Windows.Media.Brushes]::LightYellow + $dock.Children.Add($btnB) | Out-Null + $dock.Children.Add($txtPath) | Out-Null + $group.Children.Add($dock) | Out-Null + $controlMap[$p.Name] = @{ Type = "Path"; Control = $txtPath } + + if ($p.IsCsv) { + $sep = New-Object System.Windows.Controls.TextBlock + $sep.Text = "- ou - coller une liste d'IPs :" + $sep.HorizontalAlignment = "Center" + $sep.Foreground = [System.Windows.Media.Brushes]::Gray + $sep.FontStyle = [System.Windows.FontStyles]::Italic + $sep.Margin = New-Object System.Windows.Thickness(0, 8, 0, 4) + $group.Children.Add($sep) | Out-Null + + $txtIp = New-Object System.Windows.Controls.TextBox + $txtIp.Height = 65 + $txtIp.TextWrapping = "Wrap" + $txtIp.AcceptsReturn = $true + $txtIp.VerticalScrollBarVisibility = "Auto" + $txtIp.FontFamily = New-Object System.Windows.Media.FontFamily("Consolas") + $txtIp.FontSize = 12 + $txtIp.Padding = New-Object System.Windows.Thickness(6) + $group.Children.Add($txtIp) | Out-Null + + $btnConv = New-Object System.Windows.Controls.Button + $btnConv.Content = "Convertir en CSV" + $btnConv.HorizontalAlignment = "Left" + $btnConv.Width = 180 + $btnConv.Padding = New-Object System.Windows.Thickness(10, 5, 10, 5) + $btnConv.Margin = New-Object System.Windows.Thickness(0, 4, 0, 8) + $btnConv.Cursor = [System.Windows.Input.Cursors]::Hand + $group.Children.Add($btnConv) | Out-Null + + $lblPrev = New-Object System.Windows.Controls.TextBlock + $lblPrev.Text = "Aperçu (10 premières lignes) :" + $lblPrev.FontSize = 11 + $lblPrev.Foreground = [System.Windows.Media.Brushes]::Gray + $lblPrev.Margin = New-Object System.Windows.Thickness(0, 0, 0, 3) + $group.Children.Add($lblPrev) | Out-Null + + $dg = New-Object System.Windows.Controls.DataGrid + $dg.Height = 130 + $dg.IsReadOnly = $true + $dg.AutoGenerateColumns = $true + $dg.CanUserAddRows = $false + $dg.GridLinesVisibility = "Horizontal" + $dg.HeadersVisibility = "Column" + $dg.FontSize = 11 + $dg.AlternatingRowBackground = [System.Windows.Media.Brushes]::AliceBlue + $group.Children.Add($dg) | Out-Null + + $loadCsv = { + param([string]$CsvPath) + try { + Add-Type -AssemblyName System.Data + $fl = Get-Content $CsvPath -TotalCount 1 -Encoding UTF8 + $delim = if (($fl -split ";").Count -gt ($fl -split ",").Count) { ";" } else { "," } + $tbl = New-Object System.Data.DataTable + $rows = @(Import-Csv $CsvPath -Encoding UTF8 -Delimiter $delim | Select-Object -First 10) + if ($rows.Count -gt 0) { + foreach ($col in $rows[0].PSObject.Properties) { $tbl.Columns.Add($col.Name) | Out-Null } + foreach ($r in $rows) { + $nr = $tbl.NewRow() + foreach ($col in $r.PSObject.Properties) { $nr[$col.Name] = $col.Value } + $tbl.Rows.Add($nr) + } + } + $dg.ItemsSource = $tbl.DefaultView + } catch { } + }.GetNewClosure() + + $btnB.Add_Click({ + $dlg = New-Object System.Windows.Forms.OpenFileDialog + $dlg.Filter = "Fichiers CSV (*.csv)|*.csv|Tous les fichiers (*.*)|*.*" + if ($dlg.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { + $txtPath.Text = $dlg.FileName + & $loadCsv $dlg.FileName + } + }.GetNewClosure()) + + $btnConv.Add_Click({ + $ipText = $txtIp.Text.Trim() + if ([string]::IsNullOrWhiteSpace($ipText)) { return } + $ips = $ipText -split "[, ;\t\r\n]+" | ForEach-Object { $_.Trim() } | + Where-Object { $_ -match "^\d{1,3}(\.\d{1,3}){3}(/\d{1,2})?$" } + if ($ips.Count -eq 0) { return } + $out = "$PSScriptRoot\ips_temp.csv" + $ips | ForEach-Object { [PSCustomObject]@{ IP = $_ } } | Export-Csv $out -NoTypeInformation -Encoding UTF8 + $txtPath.Text = $out + & $loadCsv $out + }.GetNewClosure()) + } + else { + $btnB.Add_Click({ + $dlg = New-Object System.Windows.Forms.OpenFileDialog + $dlg.Filter = "Tous les fichiers (*.*)|*.*" + if ($dlg.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { + $txtPath.Text = $dlg.FileName + } + }.GetNewClosure()) + } + } + else { + $txt = New-Object System.Windows.Controls.TextBox + $txt.FontSize = 12 + $txt.Padding = New-Object System.Windows.Thickness(6) + $txt.Background = if ($p.Mandatory) { [System.Windows.Media.Brushes]::White } else { [System.Windows.Media.Brushes]::WhiteSmoke } + $group.Children.Add($txt) | Out-Null + $controlMap[$p.Name] = @{ Type = "Text"; Control = $txt } + } + + $paramPanel.Children.Add($group) | Out-Null + } + + # Mise a jour visuelle du bouton Lancer selon l'etat des champs obligatoires + $checkCanLaunch = { + $allFilled = $true + foreach ($n in $controlMap.Keys) { + $e = $controlMap[$n] + $sp = $scriptParams | Where-Object { $_.Name -eq $n } | Select-Object -First 1 + if ($sp -and $sp.Mandatory -and $e.Type -notin @("Switch", "Radio")) { + if ([string]::IsNullOrWhiteSpace($e.Control.Text)) { $allFilled = $false; break } + } + } + $btnLancer.Background = if ($allFilled) { $greenBrush } else { $grayBrush } + }.GetNewClosure() + + # Abonner les TextBox obligatoires a la verification en temps reel + foreach ($n in $controlMap.Keys) { + $e = $controlMap[$n] + $sp = $scriptParams | Where-Object { $_.Name -eq $n } | Select-Object -First 1 + if ($sp -and $sp.Mandatory -and $e.Type -notin @("Switch", "Radio")) { + $e.Control.Add_TextChanged($checkCanLaunch) + } + } + & $checkCanLaunch # Etat initial + + # Clic Lancer : valide une derniere fois (securite) puis ferme + $btnLancer.Add_Click({ + $ok = $true + foreach ($name in $controlMap.Keys) { + $entry = $controlMap[$name] + $sp = $scriptParams | Where-Object { $_.Name -eq $name } | Select-Object -First 1 + if ($sp -and $sp.Mandatory -and $entry.Type -notin @("Switch", "Radio")) { + if ([string]::IsNullOrWhiteSpace($entry.Control.Text)) { $ok = $false; break } + } + } + if ($ok) { $w.DialogResult = $true } + }.GetNewClosure()) + + $btnAnnuler.Add_Click({ $w.DialogResult = $false }) + + if ($w.ShowDialog() -ne $true) { return $null } + + # Collecter les valeurs depuis les controles apres fermeture + $result = @{} + foreach ($name in $controlMap.Keys) { + $entry = $controlMap[$name] + $value = if ($entry.Type -eq "Switch") { + $entry.Control.IsChecked + } elseif ($entry.Type -eq "Radio") { + $sel = $entry.Panel.Children | Where-Object { $_.IsChecked } | Select-Object -First 1 + if ($sel) { $sel.Content } else { $null } + } else { + $entry.Control.Text + } + if (-not [string]::IsNullOrWhiteSpace("$value")) { + $result[$name] = $value + } + } + return $result +} + +# ===================================================================== +# FONCTION : Invoke-SelectedScript +# Execute le script local avec les parametres saisis par l'utilisateur. +# ===================================================================== +function Invoke-SelectedScript { + param( + [string]$ScriptName, + [hashtable]$ParamValues = @{} + ) + + $ps1NameFile = "$LOCAL_CACHE_DIR\$ScriptName\.ps1name" + $ps1FileName = if (Test-Path $ps1NameFile) { (Get-Content $ps1NameFile -Encoding UTF8).Trim() } else { "$ScriptName.ps1" } + $scriptPath = "$LOCAL_CACHE_DIR\$ScriptName\$ps1FileName" + + if (-not (Test-Path $scriptPath)) { + Write-Log "Script introuvable en cache : $scriptPath" "ERROR" + return + } + + Write-Host "" + Write-Log "Demarrage : $ScriptName" + if ($ParamValues.Count -gt 0) { + $ParamValues.Keys | ForEach-Object { Write-Log " -$_ : $($ParamValues[$_])" } + } + Write-Host "" + + # Sauvegarder les fonctions internes avant l'execution du script enfant. + # Un module importe avec -Force peut ecraser Write-Log dans le scope global. + $savedFunctions = @{} + foreach ($fnName in @('Write-Log', 'Show-MessageBox')) { + $fn = Get-Item "Function:$fnName" -ErrorAction SilentlyContinue + if ($fn) { $savedFunctions[$fnName] = $fn.ScriptBlock } + } + + $execError = $null + try { + & $scriptPath @ParamValues + Write-Host "" + } + catch { + $execError = $_.Exception.Message + } + finally { + foreach ($fnName in $savedFunctions.Keys) { + Set-Item -Path "Function:$fnName" -Value $savedFunctions[$fnName] -Force + } + } + + if ($execError) { + Write-Log "Erreur d'execution : $execError" "ERROR" + Show-MessageBox -Message "Erreur lors de l'exécution de '$ScriptName' :`n`n$execError" ` + -Title "Erreur d'exécution" -Type "Error" + } else { + Write-Log "Execution terminee avec succes." "OK" + } +} + +# ===================================================================== +# FLUX PRINCIPAL +# ===================================================================== + +Clear-Host +Write-Host "" +Write-Host " +==========================================+" -ForegroundColor Cyan +Write-Host " | cadMasterCLI v1.1 |" -ForegroundColor Cyan +Write-Host " | Launcher de scripts PowerShell |" -ForegroundColor Cyan +Write-Host " +==========================================+" -ForegroundColor Cyan +Write-Host "" + +$policy = Get-ExecutionPolicy -Scope CurrentUser +if ($policy -eq "Restricted") { + Write-Log "ExecutionPolicy trop restrictive detectee : $policy" "WARN" + Write-Log "Executez cette commande pour corriger : Set-ExecutionPolicy RemoteSigned -Scope CurrentUser" "WARN" + Write-Host "" +} + +if (-not (Test-Path $LOCAL_CACHE_DIR)) { + New-Item -ItemType Directory -Path $LOCAL_CACHE_DIR -Force | Out-Null + Write-Log "Dossier cache cree : $LOCAL_CACHE_DIR" "OK" +} + +# === Etape 1 : Recuperer la liste des scripts depuis Gitea === +$rawList = Get-ScriptList +$modeOffline = $false + +if ($null -eq $rawList) { + Write-Log "Mode hors ligne : utilisation du cache local." "WARN" + $modeOffline = $true + $rawList = Get-ChildItem -Path $LOCAL_CACHE_DIR -Directory -ErrorAction SilentlyContinue | + ForEach-Object { [PSCustomObject]@{ Nom = $_.Name; GiteaPath = $null } } + + if (-not $rawList -or @($rawList).Count -eq 0) { + Write-Log "Aucun script disponible (cache vide et Gitea inaccessible)." "ERROR" + Show-MessageBox -Message "Impossible de contacter Gitea et le cache local est vide.`n`nVérifiez votre connexion réseau et relancez le script." -Type "Error" + exit 1 + } +} + +# === Etape 2 : Enrichir avec README, date et statut de mise a jour === +Write-Log "Chargement des informations ($(@($rawList).Count) script(s))..." + +$scripts = @($rawList) | ForEach-Object { + $name = $_.Nom + $branch = if ($_.DefaultBranch) { $_.DefaultBranch } else { $GITEA_BRANCH } + + $readme = if (-not $modeOffline) { Get-ScriptReadme -ScriptName $name -Branch $branch } else { $null } + $date = if (-not $modeOffline) { Get-ScriptLastCommitDate -ScriptName $name -Branch $branch } else { $null } + $majDispo = Test-ScriptUpdate -ScriptName $name -RemoteDate $date + + if (-not $readme) { + $localReadme = "$LOCAL_CACHE_DIR\$name\README.md" + if (Test-Path $localReadme) { $readme = Get-Content $localReadme -Raw -Encoding UTF8 } + } + + $summary = Get-ReadmeSummary -ReadmeContent $readme + if ($summary -eq "Aucune description disponible" -and $_.Description) { $summary = $_.Description } + + [PSCustomObject]@{ + Nom = $name + Description = $summary + ReadmeFull = $readme + DateGitea = $date + MajDispo = $majDispo + DefaultBranch = $branch + } +} + +# === Boucle principale : menu -> parametres -> execution (Annuler revient au menu) === +while ($true) { + + # === Etape 3 : Afficher le menu de selection === + $choix = Show-ScriptMenu -Scripts $scripts + + if (-not $choix) { + Write-Log "Aucun script selectionne. Au revoir !" + break + } + + $scriptName = $choix.Nom + $scriptData = $scripts | Where-Object { $_.Nom -eq $scriptName } | Select-Object -First 1 + Write-Log "Script selectionne : $scriptName" + + # === Etape 4 : Telecharger ou proposer une mise a jour === + $localPs1 = Get-ChildItem -Path "$LOCAL_CACHE_DIR\$scriptName\" -Filter "*.ps1" -ErrorAction SilentlyContinue | + Select-Object -First 1 -ExpandProperty FullName + + $scriptBranch = $scriptData.DefaultBranch + + if (-not $localPs1) { + Write-Log "Telechargement initial de $scriptName..." + $ok = Invoke-ScriptDownload -ScriptName $scriptName -Branch $scriptBranch + if (-not $ok) { + Write-Log "Echec du telechargement. Verifiez votre connexion." "ERROR" + Show-MessageBox -Message "Impossible de telecharger le script '$scriptName'.`nVerifiez votre connexion reseau." -Type "Error" + continue + } + } + elseif ($scriptData.MajDispo) { + $rep = Show-MessageBox ` + -Message "Une mise a jour est disponible pour '$scriptName'.`nVoulez-vous telecharger la derniere version ?" ` + -Title "Mise a jour disponible" -Type "Question" + if ($rep -eq "Yes") { + Invoke-ScriptDownload -ScriptName $scriptName -Branch $scriptBranch | Out-Null + } + } + else { + Write-Log "Script '$scriptName' en cache, a jour." "OK" + } + + # === Etape 5 : Charger le README === + $readmeContent = $scriptData.ReadmeFull + $localReadmePath = "$LOCAL_CACHE_DIR\$scriptName\README.md" + if (-not $readmeContent -and (Test-Path $localReadmePath)) { + $readmeContent = Get-Content $localReadmePath -Raw -Encoding UTF8 + } + + # === Etape 6 : Saisie des parametres === + $ps1NameFile = "$LOCAL_CACHE_DIR\$scriptName\.ps1name" + $ps1FileName = if (Test-Path $ps1NameFile) { (Get-Content $ps1NameFile -Encoding UTF8).Trim() } else { "$scriptName.ps1" } + $cachedScriptPath = "$LOCAL_CACHE_DIR\$scriptName\$ps1FileName" + + $paramValues = Show-ParametersWindow -ScriptName $scriptName -ScriptPath $cachedScriptPath -ReadmeContent $readmeContent + if ($null -eq $paramValues) { + Write-Log "Saisie des parametres annulee, retour au menu." + continue + } + + # === Etape 7 : Executer le script === + Invoke-SelectedScript -ScriptName $scriptName -ParamValues $paramValues + break +} + +Write-Host "" +Read-Host "Appuyez sur Entree pour quitter" + + + + diff --git a/doc_contribution/CONTRIBUTING_SCRIPTS.md b/doc_contribution/CONTRIBUTING_SCRIPTS.md new file mode 100644 index 0000000..4f7e77b --- /dev/null +++ b/doc_contribution/CONTRIBUTING_SCRIPTS.md @@ -0,0 +1,93 @@ +# Guide de conception de scripts pour cadMasterCLI + +Ce guide décrit les règles minimales pour qu'un script PowerShell soit correctement reconnu et piloté par cadMasterCLI : détection des paramètres, affichage de l'aide, types lisibles, etc. + +--- + +## Règles minimales + +### 1. Comment-based help obligatoire + +Le script **doit** avoir un bloc `.SYNOPSIS` et un bloc `.PARAMETER` pour **chaque paramètre**. +cadMasterCLI lit ces blocs via `Get-Help` pour afficher les descriptions dans l'interface (bouton "Voir l'aide"). + +```powershell +<# +.SYNOPSIS + Courte description en une ligne (affichée dans la liste de sélection). + +.PARAMETER MonParam + Description claire de ce que fait ce paramètre, ses valeurs attendues, + son impact sur l'exécution du script. +#> +``` + +### 2. Bloc `param()` avec types et ValidateSet + +- **Toujours typer** les paramètres : `[string]`, `[int]`, `[double]`, `[switch]`... +- **Utiliser `[ValidateSet(...)]`** pour les paramètres à choix limité — cadMasterCLI affichera automatiquement des **RadioButtons**. +- **Marquer les paramètres obligatoires** avec `[Parameter(Mandatory)]`. + +```powershell +param( + [Parameter(Mandatory)] + [ValidateSet("Read", "Write")] + [string]$Action, + + [Parameter(Mandatory)] + [string]$CsvInput, + + [string]$Username = "admin" +) +``` + +### 3. Nommage des paramètres fichier/CSV + +cadMasterCLI détecte automatiquement un paramètre comme **fichier** et affiche un bouton "Parcourir" si le nom contient l'un des mots-clés suivants : + +| Mot-clé | Exemple de paramètre | +|---|---| +| `Path` | `$OutputPath` | +| `File` | `$InputFile` | +| `Csv` | `$CsvExport` | +| `Input` | `$CsvInput` | +| `Output` | `$OutputFile` | + +Si le nom contient `Csv` ou `Input`, une zone de collage d'IPs avec conversion automatique en CSV est également proposée. + +### 4. Structure du dépôt Gitea + +Le dépôt doit contenir à sa racine : + +``` +MonScript/ +├── MonScript.ps1 ← script principal (nom identique au dépôt recommandé) +├── README.md ← documentation affichée dans cadMasterCLI avant exécution +└── modules/ ← (optionnel) modules PowerShell importés par le script + └── MonModule.psm1 +``` + +> cadMasterCLI télécharge tout le dépôt récursivement : les modules, configs et fichiers annexes sont préservés. + +--- + +## Types reconnus et leur affichage + +| Type PowerShell | Affiché dans l'interface | +|---|---| +| `[string]` | Texte | +| `[int]` / `[int32]` / `[int64]` | Nombre entier | +| `[double]` / `[single]` | Nombre décimal | +| `[bool]` | Vrai/Faux | +| `[datetime]` | Date | +| `[System.IO.FileInfo]` | Fichier | +| `[switch]` | Case à cocher | +| `[ValidateSet(...)]` | RadioButtons (un bouton par valeur) | + +--- + +## Exemple minimal conforme + +Voir `_template_script.ps1` dans ce dépôt pour un squelette prêt à l'emploi. + +Pour un exemple complet et fonctionnel, voir le script **DaliACLI**. diff --git a/doc_contribution/_template_script.ps1 b/doc_contribution/_template_script.ps1 new file mode 100644 index 0000000..3a77c75 --- /dev/null +++ b/doc_contribution/_template_script.ps1 @@ -0,0 +1,77 @@ +<# +.SYNOPSIS + Courte description en une ligne (affichee dans la liste cadMasterCLI). + +.DESCRIPTION + Description complete du script sur plusieurs lignes si necessaire. + Expliquer ce que le script fait, ses prerequis et ses limitations. + +.PARAMETER Action + Action a effectuer : + Read - Lire les donnees et generer un fichier de sortie + Write - Ecrire les donnees depuis le fichier d'entree + +.PARAMETER CsvInput + Chemin vers le fichier CSV d'entree (separateur point-virgule). + Pour Read : colonnes minimales requises (ex: Hostname, IP). + Pour Write : fichier CSV genere par l'action Read. + +.PARAMETER Username + Nom d'utilisateur pour l'authentification (defaut : admin). + +.PARAMETER Password + Mot de passe pour l'authentification (defaut : vide). + +.PARAMETER Verbose + Affiche des informations detaillees pendant l'execution. + +.EXAMPLE + .\MonScript.ps1 -Action Read -CsvInput ".\liste.csv" + + Lit les donnees depuis tous les equipements du CSV. + Genere un fichier de sortie dans le repertoire courant. + +.EXAMPLE + .\MonScript.ps1 -Action Write -CsvInput ".\sortie_2026-06-26.csv" -Password "MonMotDePasse" + + Ecrit les donnees du CSV vers les equipements. + +.NOTES + Prerequis : PowerShell 5.1+ (inclus dans Windows 10/11) + Auteur : Votre Nom +#> + +param( + # [ValidateSet] -> cadMasterCLI affiche des RadioButtons automatiquement + [Parameter(Mandatory)] + [ValidateSet("Read", "Write")] + [string]$Action, + + # Nom contenant "Csv" ou "Input" -> cadMasterCLI affiche un bouton Parcourir + zone IPs + [Parameter(Mandatory)] + [string]$CsvInput, + + [string]$Username = "admin", + + [string]$Password = "", + + # [switch] -> cadMasterCLI affiche une case a cocher + [switch]$DryRun +) + +# ===================================================================== +# Votre code ici +# ===================================================================== + +# Exemple de structure recommandee : + +switch ($Action) { + "Read" { + Write-Host "Action : lecture en cours..." + # Logique de lecture + } + "Write" { + Write-Host "Action : ecriture en cours..." + # Logique d'ecriture + } +}