004fdde528
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
1192 lines
52 KiB
PowerShell
1192 lines
52 KiB
PowerShell
#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 "<html><body style='font-family:Segoe UI,sans-serif;padding:12px'><p><em>Aucune documentation disponible.</em></p></body></html>"
|
|
}
|
|
|
|
$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 "\*\*(.+?)\*\*", "<strong>`$1</strong>"
|
|
$s = $s -replace "\*(.+?)\*", "<em>`$1</em>"
|
|
$s = $s -replace "``(.+?)``", "<code>`$1</code>"
|
|
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 = "<table>"
|
|
$isHeader = $true
|
|
foreach ($row in $buf) {
|
|
if ($row -match "^\s*\|[\s\-\|:]+$") { continue }
|
|
$cells = $row -split "\|" | Where-Object { $_ -ne "" } | ForEach-Object { $_.Trim() }
|
|
if ($isHeader) {
|
|
$html += "<thead><tr>"
|
|
foreach ($c in $cells) { $html += "<th>$([System.Net.WebUtility]::HtmlEncode($c))</th>" }
|
|
$html += "</tr></thead><tbody>"
|
|
$isHeader = $false
|
|
} else {
|
|
$html += "<tr>"
|
|
foreach ($c in $cells) { $html += "<td>$([System.Net.WebUtility]::HtmlEncode($c))</td>" }
|
|
$html += "</tr>"
|
|
}
|
|
}
|
|
$html += "</tbody></table>"
|
|
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("<html><head><meta charset='utf-8'><style>$css</style></head><body>") | Out-Null
|
|
|
|
foreach ($line in ($Markdown -split "`n")) {
|
|
$raw = $line.TrimEnd()
|
|
|
|
# Toggle bloc de code
|
|
if ($raw -match "^``````") {
|
|
if ($listOpen) { $sb.Append("</ul>") | Out-Null; $listOpen = $false }
|
|
if ($bqOpen) { $sb.Append("</blockquote>") | Out-Null; $bqOpen = $false }
|
|
if ($inTable) { $sb.Append((& $flushTable $tableBuffer)) | Out-Null; $tableBuffer.Clear(); $inTable = $false }
|
|
if ($inCodeBlock) { $sb.Append("</code></pre>") | Out-Null; $inCodeBlock = $false }
|
|
else { $sb.Append("<pre><code>") | 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("</ul>") | Out-Null; $listOpen = $false }
|
|
if ($bqOpen) { $sb.Append("</blockquote>") | 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("</ul>") | Out-Null; $listOpen = $false }
|
|
if (-not $bqOpen) { $sb.Append("<blockquote>") | Out-Null; $bqOpen = $true }
|
|
$content = & $applyInline ([System.Net.WebUtility]::HtmlEncode($Matches[1]))
|
|
$sb.Append("<p>$content</p>") | Out-Null
|
|
continue
|
|
}
|
|
if ($bqOpen) { $sb.Append("</blockquote>") | Out-Null; $bqOpen = $false }
|
|
|
|
# Fermer la liste si la ligne n'est pas un item
|
|
if ($listOpen -and $raw -notmatch "^- ") {
|
|
$sb.Append("</ul>") | Out-Null
|
|
$listOpen = $false
|
|
}
|
|
|
|
# Elements structurels (detection sur ligne brute avant encodage)
|
|
if ($raw -match "^# (.+)") { $sb.Append("<h1>$(& $applyInline ([System.Net.WebUtility]::HtmlEncode($Matches[1])))</h1>") | Out-Null }
|
|
elseif ($raw -match "^## (.+)") { $sb.Append("<h2>$(& $applyInline ([System.Net.WebUtility]::HtmlEncode($Matches[1])))</h2>") | Out-Null }
|
|
elseif ($raw -match "^### (.+)") { $sb.Append("<h3>$(& $applyInline ([System.Net.WebUtility]::HtmlEncode($Matches[1])))</h3>") | Out-Null }
|
|
elseif ($raw -match "^---$") { $sb.Append("<hr/>") | Out-Null }
|
|
elseif ($raw -match "^- (.+)") {
|
|
if (-not $listOpen) { $sb.Append("<ul>") | Out-Null; $listOpen = $true }
|
|
$sb.Append("<li>$(& $applyInline ([System.Net.WebUtility]::HtmlEncode($Matches[1])))</li>") | Out-Null
|
|
}
|
|
elseif ($raw.Trim() -eq "") { $sb.Append("<br/>") | Out-Null }
|
|
else { $sb.Append("<p>$(& $applyInline ([System.Net.WebUtility]::HtmlEncode($raw)))</p>") | Out-Null }
|
|
}
|
|
|
|
# Flush des etats ouverts en fin de document
|
|
if ($inTable) { $sb.Append((& $flushTable $tableBuffer)) | Out-Null }
|
|
if ($listOpen) { $sb.Append("</ul>") | Out-Null }
|
|
if ($bqOpen) { $sb.Append("</blockquote>") | Out-Null }
|
|
if ($inCodeBlock){ $sb.Append("</code></pre>") | Out-Null }
|
|
|
|
$sb.Append("</body></html>") | 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"
|
|
|
|
|
|
|
|
|