diff --git a/README.md b/README.md index 3dfd14e..c753803 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,14 @@ cadMasterCLI est une interface graphique qui vous permet de **lancer des scripts Il suffit de : 1. Faire un clique-droit sur le fichier cadMasterCLI.ps1 et choisir "Exécuter avec Powershell" -2. Sélectionner le script que vous souhaitez exécuter +2. Sélectionner le script que vous souhaitez exécuter dans la liste à gauche 3. Remplir les informations demandées (adresse IP, fichier CSV, etc.) -4. Cliquer sur **Lancer le script** +4. Cliquer sur **Lancer le script** et suivre le résultat dans le journal d'exécution intégré Les scripts sont téléchargés automatiquement depuis un serveur centralisé (Gitea). Si vous n'avez pas accès au réseau, les scripts déjà utilisés restent disponibles en mode hors ligne. +L'interface est disponible en **français et en anglais**, et cadMasterCLI **vérifie et propose ses propres mises à jour** au démarrage. + --- ## Prérequis @@ -94,28 +96,31 @@ Set-ExecutionPolicy RemoteSigned -Scope CurrentUser ## Utilisation -### 1. Sélectionner un script +cadMasterCLI tient dans **une seule fenêtre** : la liste des scripts à gauche, et un panneau à droite qui affiche la documentation, le formulaire et le journal d'exécution du script sélectionné. -Au démarrage, une fenêtre de sélection s'affiche avec la liste des scripts disponibles. +### 1. Choisir la langue de l'interface -| Colonne | Signification | +Un bouton **FR / EN** en haut à droite de la fenêtre permet de basculer l'interface entre français et anglais à tout moment. Au premier lancement, la langue de Windows est détectée automatiquement ; votre choix est ensuite mémorisé pour les prochaines fois. + +### 2. Sélectionner un script + +La liste à gauche affiche en permanence tous les scripts disponibles, avec pour chacun : + +| Élément | Signification | |---|---| | Nom | Nom du script | -| Description | Ce que fait le script | -| Mis à jour | Date de la dernière mise à jour | -| Cache local | Indique si le script a déjà été téléchargé | -| MAJ dispo | `[NOUVEAU]` si une mise à jour est disponible | +| Date | Date de la dernière mise à jour sur le serveur | +| Badge vert **En cache** | Le script a déjà été téléchargé sur cet ordinateur | +| Badge orange **Nouveau** | Une mise à jour est disponible pour ce script | -Double-cliquez sur le script souhaité, ou sélectionnez-le et cliquez sur **OK**. +Cliquez sur un script pour afficher sa documentation et son formulaire de paramètres dans le panneau de droite. -> Pour fermer sans rien exécuter, fermez simplement la fenêtre. +### 3. Remplir les paramètres -### 2. Remplir les paramètres - -Une fenêtre s'ouvre avec : +Le panneau de droite affiche : - **À gauche** : la documentation du script (README), pour comprendre à quoi il sert et comment le remplir. -- **À droite** : le formulaire de paramètres à remplir. +- **À droite** : le formulaire de paramètres à remplir, ainsi que le journal d'exécution en dessous. **Types de champs :** @@ -134,27 +139,40 @@ Cliquez sur **[?] Voir l'aide** sous un champ pour afficher une description dét - Cliquez sur **Fermer README** pour masquer la documentation et agrandir le formulaire. - Cliquez sur **Ouvrir README** pour la réafficher. - Vous pouvez redimensionner le panneau README en faisant glisser la barre de séparation entre les deux zones. +- Si le README du script propose une version française et anglaise, la section affichée suit automatiquement la langue choisie dans l'interface. -### 3. Lancer le script +### 4. Lancer le script et suivre l'exécution Une fois tous les champs obligatoires remplis : - Cliquez sur **Lancer le script** (bouton vert). -- Le script s'exécute dans le terminal. Attendez la fin de l'exécution. -- Appuyez sur **Entrée** pour fermer. +- Le script s'exécute en arrière-plan ; sa sortie s'affiche **en direct dans le journal d'exécution**, directement dans la fenêtre (plus de fenêtre de terminal séparée). +- Pendant l'exécution, la liste des scripts et les boutons sont désactivés ; ils redeviennent disponibles à la fin. +- Un message final indique si l'exécution s'est terminée avec succès ou en erreur. -> Cliquez sur **Annuler** pour revenir à la liste des scripts sans rien exécuter. +Cliquez sur **Retour à la liste** pour revenir à la liste des scripts et en sélectionner un autre. --- ## Mise à jour automatique des scripts -Lorsqu'une nouvelle version d'un script est disponible sur le serveur, cadMasterCLI vous le signale avec `[NOUVEAU]` dans la liste et vous propose de télécharger la mise à jour. +Lorsqu'une nouvelle version d'un script est disponible sur le serveur, cadMasterCLI le signale avec un badge **Nouveau** dans la liste et vous propose de télécharger la mise à jour au moment où vous sélectionnez ce script. Les scripts sont stockés localement dans le sous-dossier `scripts\` à côté de `cadMasterCLI.ps1`. --- +## Mise à jour automatique de cadMasterCLI lui-même + +À chaque démarrage, cadMasterCLI vérifie s'il existe une version plus récente de lui-même sur le serveur. + +- Si c'est le cas, une fenêtre vous propose de l'installer. +- En acceptant, la nouvelle version est téléchargée, remplace automatiquement le fichier `cadMasterCLI.ps1` actuel, et l'application redémarre toute seule. +- En refusant, l'application continue de démarrer normalement avec la version actuelle (la question sera reposée au prochain lancement). +- Si le serveur est inaccessible, aucune vérification n'est possible : cadMasterCLI démarre normalement, sans message d'erreur. + +--- + ## Mode hors ligne Si le serveur Gitea est inaccessible (pas de réseau), cadMasterCLI utilise automatiquement les scripts déjà téléchargés. Un message vous prévient que vous êtes en mode hors ligne. @@ -176,11 +194,15 @@ Si le serveur Gitea est inaccessible (pas de réseau), cadMasterCLI utilise auto ``` cadMasterCLI/ -├── cadMasterCLI.ps1 ← fichier principal à lancer -├── README.md ← cette documentation -├── CONTRIBUTING_SCRIPTS.md ← guide pour créer de nouveaux scripts -├── _template_script.ps1 ← modèle de script -└── scripts/ ← scripts téléchargés (créé automatiquement) +├── cadMasterCLI.ps1 ← fichier principal à lancer +├── version.txt ← version de référence pour l'auto-mise à jour +├── cadmastercli.config.json ← préférence de langue (créé automatiquement) +├── README.md ← cette documentation +├── doc_contribution/ +│ ├── CONTRIBUTING_SCRIPTS.md ← guide pour créer de nouveaux scripts +│ ├── _template_script.ps1 ← modèle de script +│ └── _template_script.README.md ← modèle de README bilingue FR/EN +└── scripts/ ← scripts téléchargés (créé automatiquement) └── NomDuScript/ └── NomDuScript.ps1 ``` diff --git a/cadMasterCLI.ps1 b/cadMasterCLI.ps1 index 9346a2f..9272428 100644 --- a/cadMasterCLI.ps1 +++ b/cadMasterCLI.ps1 @@ -1,4 +1,4 @@ -#Requires -Version 5.1 +#Requires -Version 5.1 # ===================================================================== # CONFIGURATION @@ -8,6 +8,143 @@ $GITEA_ORG = "ScriptPowerShell" # Nom de l'organisation Gitea $GITEA_BRANCH = "main" $LOCAL_CACHE_DIR = "$PSScriptRoot\scripts" +$CADMASTER_VERSION = "2.0.0" +$CADMASTER_UPDATE_ORG = "DistechControls" # Organisation Gitea hebergeant cadMasterCLI lui-meme +$CADMASTER_UPDATE_REPO = "cadMasterCLI" +$CADMASTER_CONFIG_PATH = "$PSScriptRoot\cadmastercli.config.json" + +Add-Type -AssemblyName PresentationFramework +Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Data + +# ===================================================================== +# I18N : dictionnaire de traduction FR/EN et fonctions associees +# ===================================================================== +$script:I18N = @{ + fr = @{ + "app.subtitle" = "Lanceur de scripts PowerShell" + "list.header" = "Scripts disponibles" + "list.cacheBadge" = "En cache" + "list.newBadge" = "Nouveau" + "detail.placeholder" = "Selectionnez un script dans la liste a gauche pour afficher ses parametres." + "detail.paramsTitle" = "Parametres de :" + "detail.readmeOpen" = "Ouvrir README" + "detail.readmeClose" = "Fermer README" + "detail.mandatoryNote" = "* Champs obligatoires" + "detail.launch" = "Lancer le script" + "detail.browse" = "Parcourir..." + "detail.orPasteIps" = "- ou - coller une liste d'IPs :" + "detail.convertCsv" = "Convertir en CSV" + "detail.previewCsv" = "Apercu (10 premieres lignes) :" + "detail.helpShow" = "[?] Voir l'aide" + "detail.helpHide" = "[^] Masquer l'aide" + "detail.mandatory" = "Obligatoire" + "detail.optional" = "Optionnel" + "detail.switchEnable" = "Activer" + "detail.noParams" = "Ce script ne necessite aucun parametre." + "detail.noReadme" = "Aucune documentation disponible." + "log.header" = "Journal d'execution" + "log.placeholder" = "En attente d'execution..." + "log.starting" = "Demarrage du script..." + "log.finishedOk" = "Execution terminee avec succes." + "log.finishedError" = "Execution terminee avec des erreurs." + "msg.errorTitle" = "Erreur" + "msg.errorExecTitle" = "Erreur d'execution" + "msg.errorExec" = "Erreur lors de l'execution de" + "msg.downloadFailed" = "Impossible de telecharger le script" + "msg.updateAvailableTitle"= "Mise a jour disponible" + "msg.updateAvailableScript" = "Une mise a jour est disponible pour" + "msg.updateConfirm" = "Voulez-vous telecharger la derniere version ?" + "msg.selfUpdateTitle" = "Mise a jour de cadMasterCLI" + "msg.selfUpdateAvailable" = "Une nouvelle version de cadMasterCLI est disponible" + "msg.selfUpdateConfirm" = "Voulez-vous l'installer maintenant ? L'application va redemarrer." + "msg.offline" = "Impossible de contacter Gitea et le cache local est vide.`n`nVerifiez votre connexion reseau et relancez le script." + } + en = @{ + "app.subtitle" = "PowerShell script launcher" + "list.header" = "Available scripts" + "list.cacheBadge" = "Cached" + "list.newBadge" = "New" + "detail.placeholder" = "Select a script from the list on the left to view its parameters." + "detail.paramsTitle" = "Parameters for:" + "detail.readmeOpen" = "Open README" + "detail.readmeClose" = "Close README" + "detail.mandatoryNote" = "* Required fields" + "detail.launch" = "Run script" + "detail.browse" = "Browse..." + "detail.orPasteIps" = "- or - paste a list of IPs:" + "detail.convertCsv" = "Convert to CSV" + "detail.previewCsv" = "Preview (first 10 rows):" + "detail.helpShow" = "[?] Show help" + "detail.helpHide" = "[^] Hide help" + "detail.mandatory" = "Required" + "detail.optional" = "Optional" + "detail.switchEnable" = "Enable" + "detail.noParams" = "This script does not require any parameters." + "detail.noReadme" = "No documentation available." + "log.header" = "Execution log" + "log.placeholder" = "Waiting for execution..." + "log.starting" = "Starting script..." + "log.finishedOk" = "Execution completed successfully." + "log.finishedError" = "Execution finished with errors." + "msg.errorTitle" = "Error" + "msg.errorExecTitle" = "Execution error" + "msg.errorExec" = "Error while running" + "msg.downloadFailed" = "Unable to download the script" + "msg.updateAvailableTitle"= "Update available" + "msg.updateAvailableScript" = "An update is available for" + "msg.updateConfirm" = "Do you want to download the latest version?" + "msg.selfUpdateTitle" = "cadMasterCLI update" + "msg.selfUpdateAvailable" = "A new version of cadMasterCLI is available" + "msg.selfUpdateConfirm" = "Do you want to install it now? The application will restart." + "msg.offline" = "Unable to reach Gitea and the local cache is empty.`n`nCheck your network connection and restart the script." + } +} + +function Get-Text { + param([string]$Key) + if ($script:I18N[$script:CurrentLang].ContainsKey($Key)) { return $script:I18N[$script:CurrentLang][$Key] } + return $Key +} +function T { param([string]$Key) Get-Text -Key $Key } + +function Get-CadMasterConfig { + if (Test-Path $CADMASTER_CONFIG_PATH) { + try { + $cfg = Get-Content $CADMASTER_CONFIG_PATH -Raw -Encoding UTF8 | ConvertFrom-Json + if ($cfg.Language) { return $cfg.Language } + } + catch { } + } + return $null +} + +function Save-CadMasterConfig { + param([string]$Language) + try { + @{ Language = $Language } | ConvertTo-Json | Set-Content -Path $CADMASTER_CONFIG_PATH -Encoding UTF8 + } + catch { } +} + +$script:CurrentLang = Get-CadMasterConfig +if (-not $script:CurrentLang) { + $script:CurrentLang = if ([System.Globalization.CultureInfo]::CurrentUICulture.TwoLetterISOLanguageName -eq "fr") { "fr" } else { "en" } +} + +# Note : $script:CurrentLang ne doit JAMAIS etre lu/ecrit directement depuis un +# scriptblock passe par .GetNewClosure() (WPF event handlers) : GetNewClosure() +# fige une copie privee de toute variable de scope $script: des sa creation, qui +# ne se resynchronise plus jamais avec la vraie valeur. Passer systematiquement +# par ces fonctions (fonctions racine = resolution dynamique a chaque appel). +function Get-CurrentLang { return $script:CurrentLang } + +function Set-CadMasterLanguage { + param([string]$Lang) + $script:CurrentLang = $Lang + Save-CadMasterConfig -Language $Lang +} + # ===================================================================== # UTILITAIRES # ===================================================================== @@ -30,7 +167,6 @@ function Show-MessageBox { [string]$Title = "cadMasterCLI", [string]$Type = "Info" ) - Add-Type -AssemblyName PresentationFramework $icon = switch ($Type) { "Warning" { [System.Windows.MessageBoxImage]::Warning } "Error" { [System.Windows.MessageBoxImage]::Error } @@ -101,7 +237,8 @@ function Get-ScriptReadme { # ===================================================================== # FONCTION : Get-ReadmeSummary -# Extrait la premiere ligne de description d'un README +# Extrait la premiere ligne de description d'un README (conservee pour +# usage eventuel en infobulle, non utilisee dans la liste depuis la v2) # ===================================================================== function Get-ReadmeSummary { param([string]$ReadmeContent) @@ -122,6 +259,35 @@ function Get-ReadmeSummary { return "Aucune description disponible" } +# ===================================================================== +# FONCTION : Get-ReadmeSection +# Extrait la section FR ou EN d'un README marque avec / . +# Si aucun marqueur n'est present, retourne le contenu brut (retro-compatibilite). +# Si la langue demandee est absente, retombe sur l'autre langue disponible. +# ===================================================================== +function Get-ReadmeSection { + param( + [string]$Markdown, + [string]$Lang + ) + if ([string]::IsNullOrWhiteSpace($Markdown)) { return $Markdown } + + $pattern = '(?is)(.*?)(?=|$)' + $found = [regex]::Matches($Markdown, $pattern) + if ($found.Count -eq 0) { return $Markdown } + + $sections = @{} + foreach ($m in $found) { + $key = $m.Groups[1].Value.ToUpper() + if (-not $sections.ContainsKey($key)) { $sections[$key] = $m.Groups[2].Value.Trim() } + } + + $want = $Lang.ToUpper() + if ($sections.ContainsKey($want)) { return $sections[$want] } + $other = $sections.Keys | Select-Object -First 1 + return $sections[$other] +} + # ===================================================================== # FONCTION : Get-ScriptLastCommitDate # Recupere la date du dernier commit du depot du script @@ -250,23 +416,45 @@ function Invoke-ScriptDownload { } # ===================================================================== -# FONCTION : Show-ScriptMenu -# Affiche le menu Out-GridView de selection des scripts +# FONCTION : Test-CadMasterUpdate / Invoke-CadMasterSelfUpdate +# Verifie et applique une mise a jour de cadMasterCLI.ps1 lui-meme, +# depuis le depot Gitea dedie $CADMASTER_UPDATE_ORG/$CADMASTER_UPDATE_REPO. # ===================================================================== -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 { "" } - } +function Test-CadMasterUpdate { + try { + $url = "$GITEA_BASE_URL/api/v1/repos/$CADMASTER_UPDATE_ORG/$CADMASTER_UPDATE_REPO/raw/version.txt?ref=$GITEA_BRANCH" + $remote = (Invoke-RestMethod -Uri $url -Method Get -TimeoutSec 10 -ErrorAction Stop).ToString().Trim() + if ([version]$remote -gt [version]$CADMASTER_VERSION) { return $remote } } + catch { } + return $null +} - return $items | Out-GridView -Title "cadMasterCLI - Selectionnez un script a executer" -PassThru +function Invoke-CadMasterSelfUpdate { + param([string]$RemoteVersion) + try { + $tempFile = Join-Path $env:TEMP "cadMasterCLI_new_$([Guid]::NewGuid().ToString('N')).ps1" + $rawUrl = "$GITEA_BASE_URL/$CADMASTER_UPDATE_ORG/$CADMASTER_UPDATE_REPO/raw/branch/$GITEA_BRANCH/cadMasterCLI.ps1" + Invoke-WebRequest -Uri $rawUrl -OutFile $tempFile -ErrorAction Stop + + if (-not (Test-Path $tempFile) -or (Get-Item $tempFile).Length -eq 0) { + Write-Log "Fichier telecharge invalide, mise a jour annulee." "ERROR" + return $false + } + + $currentPath = $PSCommandPath + $relaunchCmd = "Start-Sleep -Seconds 1; Copy-Item -Path `"$tempFile`" -Destination `"$currentPath`" -Force; " + + "Remove-Item -Path `"$tempFile`" -Force -ErrorAction SilentlyContinue; " + + "Start-Process powershell -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','$currentPath'" + + Start-Process powershell -ArgumentList "-NoProfile", "-WindowStyle", "Hidden", "-Command", $relaunchCmd | Out-Null + Write-Log "Redemarrage pour appliquer la mise a jour vers $RemoteVersion..." "OK" + return $true + } + catch { + Write-Log "Echec de la mise a jour automatique : $($_.Exception.Message)" "ERROR" + return $false + } } # ===================================================================== @@ -315,21 +503,19 @@ function Convert-IpListToCsv { # ===================================================================== # FONCTION : ConvertTo-FriendlyTypeName -# Traduit les types .NET en libelles lisibles (WPF UI, accents OK) +# Traduit les types .NET en libelles lisibles (FR/EN) # ===================================================================== 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 } + param( + [string]$TypeName, + [string]$Lang = $script:CurrentLang + ) + $map = @{ + fr = @{ String = "Texte"; Int32 = "Nombre entier"; Int64 = "Nombre entier"; Double = "Nombre decimal"; Single = "Nombre decimal"; Boolean = "Vrai/Faux"; DateTime = "Date"; FileInfo = "Fichier" } + en = @{ String = "Text"; Int32 = "Integer"; Int64 = "Integer"; Double = "Decimal number"; Single = "Decimal number"; Boolean = "True/False"; DateTime = "Date"; FileInfo = "File" } } + if ($map[$Lang].ContainsKey($TypeName)) { return $map[$Lang][$TypeName] } + return $TypeName } # ===================================================================== @@ -366,7 +552,8 @@ function ConvertTo-SimpleHtml { param([string]$Markdown) if ([string]::IsNullOrWhiteSpace($Markdown)) { - return "

Aucune documentation disponible.

" + $emptyMsg = T "detail.noReadme" + return "

$emptyMsg

" } $css = "body{font-family:'Segoe UI',sans-serif;font-size:13px;padding:12px 16px;color:#222;line-height:1.6;margin:0}" + @@ -499,553 +686,980 @@ function ConvertTo-SimpleHtml { } # ===================================================================== -# 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. +# FONCTION : Start-ScriptExecution +# Lance le script en arriere-plan (Job isole) en redirigeant tous les +# flux (y compris Write-Host) vers un fichier de log, lu en direct par +# Show-MainWindow via un DispatcherTimer. # ===================================================================== -function Show-ParametersWindow { +function Start-ScriptExecution { param( - [string]$ScriptName, [string]$ScriptPath, - [string]$ReadmeContent = "" + [hashtable]$ParamValues, + [string]$LogFile, + [string]$WorkingDirectory + ) + # Start-Job demarre dans un runspace dont le repertoire courant n'est PAS + # herite de celui de cadMasterCLI (contrairement a l'ancienne execution + # synchrone) : sans ce Set-Location, les scripts enfants qui ecrivent des + # fichiers en chemin relatif (logs, exports) les deposent au mauvais endroit. + return Start-Job -ScriptBlock { + param($Path, $Params, $Log, $WorkDir) + Set-Location -Path $WorkDir + & $Path @Params *> $Log + } -ArgumentList $ScriptPath, $ParamValues, $LogFile, $WorkingDirectory +} + +# ===================================================================== +# FONCTION : Add-LogLine +# Ajoute une ligne coloree (selon son niveau INFO/WARN/ERROR/OK detecte +# par mot-cle) dans un RichTextBox, pour retrouver la colorisation du +# terminal d'origine. Fonction racine (pas de closure) : appelable sans +# risque depuis n'importe quel niveau d'imbrication de closures WPF. +# ===================================================================== +function Add-LogLine { + param( + [System.Windows.Controls.RichTextBox]$LogBox, + [string]$Line, + [ValidateSet("Auto", "Info", "Warn", "Error", "Ok")] + [string]$Level = "Auto" ) - 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 @{} + $color = switch ($Level) { + "Info" { [System.Windows.Media.Brushes]::LightSkyBlue } + "Warn" { [System.Windows.Media.Brushes]::Orange } + "Error" { [System.Windows.Media.Brushes]::Tomato } + "Ok" { [System.Windows.Media.Brushes]::LightGreen } + default { + if ($Line -imatch '\[?\s*(ERROR|ERR|FAIL(ED)?)\s*\]?') { [System.Windows.Media.Brushes]::Tomato } + elseif ($Line -imatch '\[?\s*(WARN(ING)?)\s*\]?') { [System.Windows.Media.Brushes]::Orange } + elseif ($Line -imatch '\[?\s*(OK|SUCCESS|DONE)\s*\]?') { [System.Windows.Media.Brushes]::LightGreen } + elseif ($Line -imatch '\[?\s*(INFO)\s*\]?') { [System.Windows.Media.Brushes]::LightSkyBlue } + else { [System.Windows.Media.Brushes]::Gainsboro } + } } - if ($scriptParams.Count -eq 0) { - Write-Log "Aucun parametre detecte pour $ScriptName, lancement direct." "WARN" - return @{} + $para = $LogBox.Document.Blocks.LastBlock + if (-not $para) { + $para = New-Object System.Windows.Documents.Paragraph + $para.Margin = New-Object System.Windows.Thickness(0) + $LogBox.Document.Blocks.Add($para) + } + if ($para.Inlines.Count -gt 0) { + $para.Inlines.Add((New-Object System.Windows.Documents.LineBreak)) | Out-Null + } + $run = New-Object System.Windows.Documents.Run($Line) + $run.Foreground = $color + $para.Inlines.Add($run) | Out-Null +} + +# ===================================================================== +# FONCTION : Clear-LogBox +# Vide un RichTextBox de logs et y affiche eventuellement un texte initial. +# ===================================================================== +function Clear-LogBox { + param( + [System.Windows.Controls.RichTextBox]$LogBox, + [string]$InitialLine, + [ValidateSet("Auto", "Info", "Warn", "Error", "Ok")] + [string]$Level = "Auto" + ) + $LogBox.Document.Blocks.Clear() + if ($InitialLine) { Add-LogLine -LogBox $LogBox -Line $InitialLine -Level $Level } +} + +# ===================================================================== +# FONCTION : Update-ScriptBadge +# Met a jour les controles visuels (badges/date) d'un item de la liste +# des scripts. Les references UI sont stockees directement sur l'objet +# (pas de data binding WPF, non fiable sur des PSCustomObject). +# ===================================================================== +function Update-ScriptBadge { + param($ScriptItem) + + $cacheExists = [bool](Get-ChildItem -Path "$LOCAL_CACHE_DIR\$($ScriptItem.Nom)\" -Filter "*.ps1" -ErrorAction SilentlyContinue) + + $ScriptItem.UiCacheBadge.Visibility = if ($cacheExists) { [System.Windows.Visibility]::Visible } else { [System.Windows.Visibility]::Collapsed } + $ScriptItem.UiNewBadge.Visibility = if ($ScriptItem.MajDispo) { [System.Windows.Visibility]::Visible } else { [System.Windows.Visibility]::Collapsed } + $ScriptItem.UiCacheText.Text = T "list.cacheBadge" + $ScriptItem.UiNewText.Text = T "list.newBadge" + $ScriptItem.UiDateBlock.Text = if ($ScriptItem.DateGitea) { $ScriptItem.DateGitea.ToString("dd/MM/yyyy HH:mm") } else { "" } +} + +# ===================================================================== +# FONCTION : New-FlagIcon +# Dessine un mini-drapeau en formes vectorielles WPF (Rectangle/Line). +# WPF sur .NET Framework ne sait pas afficher les glyphes emoji couleur +# (drapeaux) quelle que soit la police : on dessine donc de vraies formes. +# ===================================================================== +function New-FlagIcon { + param([string]$Country) # "FR" ou "GB" + + $w = 24 + $h = 16 + $canvas = New-Object System.Windows.Controls.Canvas + $canvas.Width = $w + $canvas.Height = $h + $canvas.ClipToBounds = $true + + if ($Country -eq "FR") { + $colors = @("#0055A4", "#FFFFFF", "#EF4135") + for ($i = 0; $i -lt 3; $i++) { + $rect = New-Object System.Windows.Shapes.Rectangle + $rect.Width = $w / 3 + $rect.Height = $h + $rect.Fill = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.ColorConverter]::ConvertFromString($colors[$i])) + [System.Windows.Controls.Canvas]::SetLeft($rect, $i * ($w / 3)) + [System.Windows.Controls.Canvas]::SetTop($rect, 0) + $canvas.Children.Add($rect) | Out-Null + } + } + else { + $blueBrush = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.ColorConverter]::ConvertFromString("#00247D")) + $redBrush = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.ColorConverter]::ConvertFromString("#CF142B")) + $whiteBrush = [System.Windows.Media.Brushes]::White + + $bg = New-Object System.Windows.Shapes.Rectangle + $bg.Width = $w; $bg.Height = $h; $bg.Fill = $blueBrush + $canvas.Children.Add($bg) | Out-Null + + # Sautoir blanc (diagonales epaisses), puis rouge (fines, par-dessus) + foreach ($def in @( + @{ Brush = $whiteBrush; Thickness = 4.4 }, + @{ Brush = $redBrush; Thickness = 1.6 } + )) { + $d1 = New-Object System.Windows.Shapes.Line + $d1.X1 = 0; $d1.Y1 = 0; $d1.X2 = $w; $d1.Y2 = $h + $d1.Stroke = $def.Brush; $d1.StrokeThickness = $def.Thickness + $d2 = New-Object System.Windows.Shapes.Line + $d2.X1 = $w; $d2.Y1 = 0; $d2.X2 = 0; $d2.Y2 = $h + $d2.Stroke = $def.Brush; $d2.StrokeThickness = $def.Thickness + $canvas.Children.Add($d1) | Out-Null + $canvas.Children.Add($d2) | Out-Null + } + + # Croix blanche, puis rouge (plus fine, par-dessus) + $hWhite = New-Object System.Windows.Shapes.Rectangle + $hWhite.Width = $w; $hWhite.Height = 6; $hWhite.Fill = $whiteBrush + [System.Windows.Controls.Canvas]::SetLeft($hWhite, 0); [System.Windows.Controls.Canvas]::SetTop($hWhite, ($h - 6) / 2) + $vWhite = New-Object System.Windows.Shapes.Rectangle + $vWhite.Width = 6; $vWhite.Height = $h; $vWhite.Fill = $whiteBrush + [System.Windows.Controls.Canvas]::SetLeft($vWhite, ($w - 6) / 2); [System.Windows.Controls.Canvas]::SetTop($vWhite, 0) + $canvas.Children.Add($hWhite) | Out-Null + $canvas.Children.Add($vWhite) | Out-Null + + $hRed = New-Object System.Windows.Shapes.Rectangle + $hRed.Width = $w; $hRed.Height = 3; $hRed.Fill = $redBrush + [System.Windows.Controls.Canvas]::SetLeft($hRed, 0); [System.Windows.Controls.Canvas]::SetTop($hRed, ($h - 3) / 2) + $vRed = New-Object System.Windows.Shapes.Rectangle + $vRed.Width = 3; $vRed.Height = $h; $vRed.Fill = $redBrush + [System.Windows.Controls.Canvas]::SetLeft($vRed, ($w - 3) / 2); [System.Windows.Controls.Canvas]::SetTop($vRed, 0) + $canvas.Children.Add($hRed) | Out-Null + $canvas.Children.Add($vRed) | Out-Null } - Write-Log "$($scriptParams.Count) parametre(s) detecte(s) pour $ScriptName" "OK" + return $canvas +} - $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) +# ===================================================================== +# FONCTION : Show-MainWindow +# Fenetre unique : liste des scripts a gauche (permanente), panneau +# droit dynamique (README + formulaire de parametres + logs d'execution +# en direct). Remplace les anciennes Show-ScriptMenu / Show-ParametersWindow. +# ===================================================================== +function Show-MainWindow { + param([array]$Scripts) $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) + $w = New-Object System.Windows.Window + $w.Title = "cadMasterCLI" + $w.Width = 1200 + $w.Height = 780 + $w.MinWidth = 900 + $w.MinHeight = 560 + $w.WindowStartupLocation = "CenterScreen" - $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) + $root = New-Object System.Windows.Controls.DockPanel - $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" + # --- Barre d'en-tete globale --- + $headerBar = New-Object System.Windows.Controls.DockPanel + $headerBar.Margin = New-Object System.Windows.Thickness(14, 12, 14, 8) + [System.Windows.Controls.DockPanel]::SetDock($headerBar, [System.Windows.Controls.Dock]::Top) - $footer.Children.Add($btnLancer) | Out-Null - $footer.Children.Add($btnAnnuler) | Out-Null - $footer.Children.Add($statusLbl) | Out-Null - $formPanel.Children.Add($footer) | Out-Null + $langActiveBorder = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(56, 142, 60)) + $langInactiveBorder = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(210, 210, 210)) + $langActiveBg = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(232, 245, 233)) - # 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) + $langPanel = New-Object System.Windows.Controls.StackPanel + $langPanel.Orientation = "Horizontal" + [System.Windows.Controls.DockPanel]::SetDock($langPanel, [System.Windows.Controls.Dock]::Right) - $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 + $btnFlagFr = New-Object System.Windows.Controls.Button + $btnFlagFr.Content = New-FlagIcon -Country "FR" + $btnFlagFr.Padding = New-Object System.Windows.Thickness(4) + $btnFlagFr.Margin = New-Object System.Windows.Thickness(0, 0, 4, 0) + $btnFlagFr.Cursor = [System.Windows.Input.Cursors]::Hand + $btnFlagFr.BorderThickness = New-Object System.Windows.Thickness(2) - $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 + $btnFlagEn = New-Object System.Windows.Controls.Button + $btnFlagEn.Content = New-FlagIcon -Country "GB" + $btnFlagEn.Padding = New-Object System.Windows.Thickness(4) + $btnFlagEn.Cursor = [System.Windows.Input.Cursors]::Hand + $btnFlagEn.BorderThickness = New-Object System.Windows.Thickness(2) - $formPanel.Children.Add($header) | Out-Null + $langPanel.Children.Add($btnFlagFr) | Out-Null + $langPanel.Children.Add($btnFlagEn) | 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 + $titleStack = New-Object System.Windows.Controls.StackPanel + $appTitle = New-Object System.Windows.Controls.TextBlock + $appTitle.Text = "cadMasterCLI" + $appTitle.FontSize = 18 + $appTitle.FontWeight = [System.Windows.FontWeights]::Bold + $appSubtitle = New-Object System.Windows.Controls.TextBlock + $appSubtitle.FontSize = 11 + $appSubtitle.Foreground = [System.Windows.Media.Brushes]::Gray + $titleStack.Children.Add($appTitle) | Out-Null + $titleStack.Children.Add($appSubtitle) | 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()) + $headerBar.Children.Add($langPanel) | Out-Null + $headerBar.Children.Add($titleStack) | Out-Null + $root.Children.Add($headerBar) | Out-Null - # Dictionnaire nom -> controle pour lire les valeurs apres ShowDialog - $controlMap = @{} + # --- Separateur horizontal entre l'en-tete et le contenu --- + $headerSeparator = New-Object System.Windows.Controls.Border + $headerSeparator.Height = 1 + $headerSeparator.Background = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(221, 221, 221)) + $headerSeparator.Margin = New-Object System.Windows.Thickness(14, 0, 14, 8) + [System.Windows.Controls.DockPanel]::SetDock($headerSeparator, [System.Windows.Controls.Dock]::Top) + $root.Children.Add($headerSeparator) | Out-Null - foreach ($p in $scriptParams) { - $group = New-Object System.Windows.Controls.StackPanel - $group.Margin = New-Object System.Windows.Thickness(0, 6, 0, 12) + # --- Grille principale : liste (gauche) | splitter | detail (droite) --- + $mainGrid = New-Object System.Windows.Controls.Grid + $colList = New-Object System.Windows.Controls.ColumnDefinition + $colList.Width = New-Object System.Windows.GridLength(300) + $colList.MinWidth = 220 + $colSplit = New-Object System.Windows.Controls.ColumnDefinition + $colSplit.Width = New-Object System.Windows.GridLength(5) + $colDetail = New-Object System.Windows.Controls.ColumnDefinition + $colDetail.Width = New-Object System.Windows.GridLength(1, [System.Windows.GridUnitType]::Star) + $mainGrid.ColumnDefinitions.Add($colList) | Out-Null + $mainGrid.ColumnDefinitions.Add($colSplit) | Out-Null + $mainGrid.ColumnDefinitions.Add($colDetail) | Out-Null + $root.Children.Add($mainGrid) | Out-Null + $w.Content = $root - # Type traduit et affichage dans le label - $friendlyType = ConvertTo-FriendlyTypeName -TypeName $p.TypeName - $typeDisplay = if ($p.ValidValues) { $p.ValidValues -join " / " } else { $friendlyType } + # --- Panneau liste (col 0) --- + $listPanel = New-Object System.Windows.Controls.DockPanel + $listPanel.Margin = New-Object System.Windows.Thickness(14, 0, 8, 14) + [System.Windows.Controls.Grid]::SetColumn($listPanel, 0) + $mainGrid.Children.Add($listPanel) | Out-Null - # 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 + $lblList = New-Object System.Windows.Controls.TextBlock + $lblList.FontWeight = [System.Windows.FontWeights]::SemiBold + $lblList.FontSize = 13 + $lblList.Margin = New-Object System.Windows.Thickness(0, 0, 0, 6) + [System.Windows.Controls.DockPanel]::SetDock($lblList, [System.Windows.Controls.Dock]::Top) + $listPanel.Children.Add($lblList) | 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 + $lv = New-Object System.Windows.Controls.ListBox + $lv.BorderThickness = New-Object System.Windows.Thickness(1) + $lv.BorderBrush = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(221, 221, 221)) + $listPanel.Children.Add($lv) | Out-Null - $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) + # --- Construction manuelle des items de la liste (badges Cache/MAJ) --- + foreach ($item in $Scripts) { + $lvi = New-Object System.Windows.Controls.ListBoxItem + $lvi.Tag = $item - $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()) + $card = New-Object System.Windows.Controls.StackPanel + $card.Margin = New-Object System.Windows.Thickness(2) - $group.Children.Add($btnHelp) | Out-Null - $group.Children.Add($helpBlock) | Out-Null + $nameBlock = New-Object System.Windows.Controls.TextBlock + $nameBlock.Text = $item.Nom + $nameBlock.FontWeight = [System.Windows.FontWeights]::Bold + $nameBlock.FontSize = 13 + $nameBlock.TextWrapping = "Wrap" + $card.Children.Add($nameBlock) | Out-Null + + $dateBlock = New-Object System.Windows.Controls.TextBlock + $dateBlock.FontSize = 10 + $dateBlock.Foreground = [System.Windows.Media.Brushes]::Gray + $dateBlock.Margin = New-Object System.Windows.Thickness(0, 2, 0, 0) + $card.Children.Add($dateBlock) | Out-Null + + $badgeRow = New-Object System.Windows.Controls.WrapPanel + $badgeRow.Margin = New-Object System.Windows.Thickness(0, 4, 0, 0) + + $cacheBadge = New-Object System.Windows.Controls.Border + $cacheBadge.Background = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(56, 142, 60)) + $cacheBadge.CornerRadius = New-Object System.Windows.CornerRadius(3) + $cacheBadge.Padding = New-Object System.Windows.Thickness(5, 1, 5, 1) + $cacheBadge.Margin = New-Object System.Windows.Thickness(0, 0, 4, 0) + $cacheBadgeText = New-Object System.Windows.Controls.TextBlock + $cacheBadgeText.FontSize = 10 + $cacheBadgeText.Foreground = [System.Windows.Media.Brushes]::White + $cacheBadge.Child = $cacheBadgeText + $badgeRow.Children.Add($cacheBadge) | Out-Null + + $newBadge = New-Object System.Windows.Controls.Border + $newBadge.Background = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(245, 124, 0)) + $newBadge.CornerRadius = New-Object System.Windows.CornerRadius(3) + $newBadge.Padding = New-Object System.Windows.Thickness(5, 1, 5, 1) + $newBadgeText = New-Object System.Windows.Controls.TextBlock + $newBadgeText.FontSize = 10 + $newBadgeText.Foreground = [System.Windows.Media.Brushes]::White + $newBadge.Child = $newBadgeText + $badgeRow.Children.Add($newBadge) | Out-Null + + $card.Children.Add($badgeRow) | Out-Null + $lvi.Content = $card + + Add-Member -InputObject $item -Force -NotePropertyMembers @{ + UiNameBlock = $nameBlock + UiDateBlock = $dateBlock + UiCacheBadge = $cacheBadge + UiCacheText = $cacheBadgeText + UiNewBadge = $newBadge + UiNewText = $newBadgeText } - # 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 + Update-ScriptBadge -ScriptItem $item + $lv.Items.Add($lvi) | Out-Null } - # Mise a jour visuelle du bouton Lancer selon l'etat des champs obligatoires - $checkCanLaunch = { - $allFilled = $true + # --- Splitter entre liste et detail (col 1) --- + $splitter = New-Object System.Windows.Controls.GridSplitter + $splitter.Width = 5 + $splitter.HorizontalAlignment = "Stretch" + $splitter.VerticalAlignment = "Stretch" + $splitter.Background = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(220, 220, 220)) + [System.Windows.Controls.Grid]::SetColumn($splitter, 1) + $mainGrid.Children.Add($splitter) | Out-Null + + # --- Panneau detail (col 2), reconstruit a chaque selection --- + $detailHost = New-Object System.Windows.Controls.Grid + $detailHost.Margin = New-Object System.Windows.Thickness(8, 0, 14, 14) + [System.Windows.Controls.Grid]::SetColumn($detailHost, 2) + $mainGrid.Children.Add($detailHost) | Out-Null + + $placeholder = New-Object System.Windows.Controls.TextBlock + $placeholder.HorizontalAlignment = "Center" + $placeholder.VerticalAlignment = "Center" + $placeholder.Foreground = [System.Windows.Media.Brushes]::Gray + $placeholder.FontSize = 14 + $placeholder.TextWrapping = "Wrap" + $placeholder.MaxWidth = 320 + + # --- Etat d'execution partage (job/timer en cours) --- + $execState = @{ + Job = $null + Timer = $null + LogFile = $null + LastLen = 0 + } + + $SetDetailPlaceholder = { + $detailHost.ColumnDefinitions.Clear() + $detailHost.Children.Clear() + $placeholder.Text = T "detail.placeholder" + $detailHost.Children.Add($placeholder) | Out-Null + }.GetNewClosure() + + $StopRunningExecution = { + if ($execState.Timer) { $execState.Timer.Stop() } + if ($execState.Job) { + Stop-Job -Job $execState.Job -ErrorAction SilentlyContinue + Remove-Job -Job $execState.Job -Force -ErrorAction SilentlyContinue + } + if ($execState.LogFile) { Remove-Item $execState.LogFile -Force -ErrorAction SilentlyContinue } + $execState.Job = $null + $execState.Timer = $null + $execState.LogFile = $null + }.GetNewClosure() + + $UpdateTexts = { + $appSubtitle.Text = T "app.subtitle" + $isFr = ((Get-CurrentLang) -eq "fr") + $btnFlagFr.BorderBrush = if ($isFr) { $langActiveBorder } else { $langInactiveBorder } + $btnFlagEn.BorderBrush = if (-not $isFr) { $langActiveBorder } else { $langInactiveBorder } + $btnFlagFr.Background = if ($isFr) { $langActiveBg } else { [System.Windows.Media.Brushes]::Transparent } + $btnFlagEn.Background = if (-not $isFr) { $langActiveBg } else { [System.Windows.Media.Brushes]::Transparent } + $lblList.Text = T "list.header" + foreach ($item in $Scripts) { Update-ScriptBadge -ScriptItem $item } + if (-not $lv.SelectedItem) { & $SetDetailPlaceholder } + }.GetNewClosure() + + $ShowScriptDetail = { + param($ScriptData) + + # Re-aliasage local : .GetNewClosure() ne capture QUE le scope direct + # ou il est appele. Une variable simplement "heritee" d'un niveau de + # closure superieur (ex: $lv/$execState captures par Show-MainWindow) + # redevient invisible pour un closure cree DANS ce bloc (ex: le clic + # du bouton Lancer). Une reassignation locale ici la rend "native" de + # CE niveau, et donc a nouveau capturable par les closures imbriquees + # ci-dessous (a repeter a chaque niveau d'imbrication supplementaire). + $lv = $lv + $execState = $execState + $grayBrush = $grayBrush + $greenBrush = $greenBrush + + & $StopRunningExecution + $detailHost.ColumnDefinitions.Clear() + $detailHost.Children.Clear() + + $scriptName = $ScriptData.Nom + + # --- Telechargement initial / proposition de 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) { + Show-MessageBox -Message "$(T 'msg.downloadFailed') '$scriptName'." -Title (T "msg.errorTitle") -Type "Error" + & $SetDetailPlaceholder + return + } + $ScriptData.MajDispo = $false + } + elseif ($ScriptData.MajDispo) { + $rep = Show-MessageBox -Message "$(T 'msg.updateAvailableScript') '$scriptName'.`n$(T 'msg.updateConfirm')" ` + -Title (T "msg.updateAvailableTitle") -Type "Question" + if ($rep -eq "Yes") { Invoke-ScriptDownload -ScriptName $scriptName -Branch $scriptBranch | Out-Null } + $ScriptData.MajDispo = $false + } + Update-ScriptBadge -ScriptItem $ScriptData + + $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" + + # --- README : section langue courante (Feature 6) --- + $readmeContent = $ScriptData.ReadmeFull + $localReadmePath = "$LOCAL_CACHE_DIR\$scriptName\README.md" + if (-not $readmeContent -and (Test-Path $localReadmePath)) { + $readmeContent = Get-Content $localReadmePath -Raw -Encoding UTF8 + } + $readmeSection = Get-ReadmeSection -Markdown $readmeContent -Lang (Get-CurrentLang) + $htmlContent = ConvertTo-SimpleHtml -Markdown $readmeSection + + # --- Parametres reels du script (Get-Command) --- + $commonParams = 'Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'ErrorVariable', + 'WarningVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable', + 'InformationAction', 'InformationVariable', 'WhatIf', 'Confirm' + $scriptParams = @() + try { + $cmd = Get-Command $cachedScriptPath -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" + } + + $paramHelp = Get-ScriptParamHelp -ScriptPath $cachedScriptPath + + # === Construction du panneau detail : readme | splitter | form+log === + $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(480) + $col2.MinWidth = 380 + $detailHost.ColumnDefinitions.Add($col0) | Out-Null + $detailHost.ColumnDefinitions.Add($col1) | Out-Null + $detailHost.ColumnDefinitions.Add($col2) | Out-Null + + $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) + $detailHost.Children.Add($readmeBorder) | Out-Null + + $wb = New-Object System.Windows.Controls.WebBrowser + $readmeBorder.Child = $wb + $wb.NavigateToString($htmlContent) + + $splitter2 = New-Object System.Windows.Controls.GridSplitter + $splitter2.Width = 5 + $splitter2.HorizontalAlignment = "Stretch" + $splitter2.VerticalAlignment = "Stretch" + $splitter2.ResizeBehavior = "PreviousAndNext" + $splitter2.Background = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(220, 220, 220)) + [System.Windows.Controls.Grid]::SetColumn($splitter2, 1) + $detailHost.Children.Add($splitter2) | Out-Null + + $formPanel = New-Object System.Windows.Controls.DockPanel + $formPanel.Margin = New-Object System.Windows.Thickness(12, 0, 0, 0) + [System.Windows.Controls.Grid]::SetColumn($formPanel, 2) + $detailHost.Children.Add($formPanel) | Out-Null + + # -- Header du panneau formulaire : titre + toggle README -- + $formHeader = New-Object System.Windows.Controls.StackPanel + $formHeader.Margin = New-Object System.Windows.Thickness(0, 0, 0, 10) + [System.Windows.Controls.DockPanel]::SetDock($formHeader, [System.Windows.Controls.Dock]::Top) + + $titleBlock = New-Object System.Windows.Controls.TextBlock + $titleBlock.Text = "$(T 'detail.paramsTitle') $scriptName" + $titleBlock.FontSize = 15 + $titleBlock.FontWeight = [System.Windows.FontWeights]::Bold + $titleBlock.Margin = New-Object System.Windows.Thickness(0, 0, 0, 6) + $formHeader.Children.Add($titleBlock) | Out-Null + + $readmeVisible = $true + $btnReadme = New-Object System.Windows.Controls.Button + $btnReadme.Content = T "detail.readmeClose" + $btnReadme.HorizontalAlignment = "Left" + $btnReadme.Padding = New-Object System.Windows.Thickness(10, 4, 10, 4) + $btnReadme.Cursor = [System.Windows.Input.Cursors]::Hand + $btnReadme.FontSize = 11 + $formHeader.Children.Add($btnReadme) | Out-Null + $formPanel.Children.Add($formHeader) | Out-Null + + $btnReadme.Add_Click({ + if ($readmeVisible) { + $col0.Width = New-Object System.Windows.GridLength(0) + $col1.Width = New-Object System.Windows.GridLength(0) + $readmeVisible = $false + $btnReadme.Content = T "detail.readmeOpen" + } else { + $col0.Width = New-Object System.Windows.GridLength(1, [System.Windows.GridUnitType]::Star) + $col1.Width = New-Object System.Windows.GridLength(5) + $readmeVisible = $true + $btnReadme.Content = T "detail.readmeClose" + } + }.GetNewClosure()) + + # -- Footer : Lancer -- + $footer = New-Object System.Windows.Controls.DockPanel + $footer.Margin = New-Object System.Windows.Thickness(0, 10, 0, 0) + [System.Windows.Controls.DockPanel]::SetDock($footer, [System.Windows.Controls.Dock]::Bottom) + + $btnLancer = New-Object System.Windows.Controls.Button + $btnLancer.Content = T "detail.launch" + $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) + + $statusLbl = New-Object System.Windows.Controls.TextBlock + $statusLbl.Text = T "detail.mandatoryNote" + $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($statusLbl) | Out-Null + $formPanel.Children.Add($footer) | Out-Null + + # -- Zone de logs d'execution (Feature 4), en bas du panneau -- + $logHeader = New-Object System.Windows.Controls.TextBlock + $logHeader.Text = T "log.header" + $logHeader.FontWeight = [System.Windows.FontWeights]::SemiBold + $logHeader.FontSize = 12 + $logHeader.Margin = New-Object System.Windows.Thickness(0, 10, 0, 4) + [System.Windows.Controls.DockPanel]::SetDock($logHeader, [System.Windows.Controls.Dock]::Bottom) + $formPanel.Children.Add($logHeader) | Out-Null + + $logBox = New-Object System.Windows.Controls.RichTextBox + $logBox.Height = 160 + $logBox.IsReadOnly = $true + $logBox.IsDocumentEnabled = $true + $logBox.VerticalScrollBarVisibility = "Auto" + $logBox.HorizontalScrollBarVisibility = "Disabled" + $logBox.FontFamily = New-Object System.Windows.Media.FontFamily("Consolas") + $logBox.FontSize = 11 + $logBox.Background = [System.Windows.Media.Brushes]::Black + $logBox.Foreground = [System.Windows.Media.Brushes]::Gainsboro + $logBox.BorderThickness = New-Object System.Windows.Thickness(1) + Clear-LogBox -LogBox $logBox -InitialLine (T "log.placeholder") + [System.Windows.Controls.DockPanel]::SetDock($logBox, [System.Windows.Controls.Dock]::Bottom) + $formPanel.Children.Add($logBox) | 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 + + if ($scriptParams.Count -eq 0) { + $noParamLbl = New-Object System.Windows.Controls.TextBlock + $noParamLbl.Text = T "detail.noParams" + $noParamLbl.Foreground = [System.Windows.Media.Brushes]::Gray + $noParamLbl.Margin = New-Object System.Windows.Thickness(0, 8, 0, 0) + $paramPanel.Children.Add($noParamLbl) | Out-Null + } + + $controlMap = @{} + + foreach ($p in $scriptParams) { + $group = New-Object System.Windows.Controls.StackPanel + $group.Margin = New-Object System.Windows.Thickness(0, 6, 0, 12) + + $friendlyType = ConvertTo-FriendlyTypeName -TypeName $p.TypeName + $typeDisplay = if ($p.ValidValues) { $p.ValidValues -join " / " } else { $friendlyType } + + $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] - $(T 'detail.mandatory')" + $lbl.FontWeight = [System.Windows.FontWeights]::SemiBold + } else { + $lbl.Text = "$($p.Name) [$typeDisplay] - $(T 'detail.optional')" + $lbl.Foreground = [System.Windows.Media.Brushes]::Gray + } + $group.Children.Add($lbl) | Out-Null + + $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 = T "detail.helpShow" + $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 = T "detail.helpHide" + } else { + $helpBlock.Visibility = [System.Windows.Visibility]::Collapsed + $btnHelp.Content = T "detail.helpShow" + } + }.GetNewClosure()) + + $group.Children.Add($btnHelp) | Out-Null + $group.Children.Add($helpBlock) | Out-Null + } + + if ($p.ValidValues) { + $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 = T "detail.switchEnable" + $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 = T "detail.browse" + $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 = T "detail.orPasteIps" + $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 = T "detail.convertCsv" + $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 = T "detail.previewCsv" + $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 { + $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 + } + + $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() + 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 } + $e.Control.Add_TextChanged($checkCanLaunch) } } - $btnLancer.Background = if ($allFilled) { $greenBrush } else { $grayBrush } + & $checkCanLaunch + + # -- Lancement asynchrone avec logs en direct (Feature 4) -- + $btnLancer.Add_Click({ + # Re-aliasage local (voir note en tete de Show-ScriptDetail) : ces + # variables doivent redevenir "natives" de CE niveau pour que le + # $timer.Add_Tick imbrique plus bas puisse les capturer a son tour. + $execState = $execState + $lv = $lv + $logBox = $logBox + $btnLancer = $btnLancer + $scriptName = $scriptName + + $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 (-not $ok) { return } + + $paramValues = @{} + 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")) { $paramValues[$name] = $value } + } + + $btnLancer.IsEnabled = $false + $lv.IsEnabled = $false + Clear-LogBox -LogBox $logBox -InitialLine (T "log.starting") -Level Info + + $logFile = "$env:TEMP\cadmastercli_run_$([Guid]::NewGuid().ToString('N')).log" + $execState.LogFile = $logFile + $execState.LastLen = 0 + $execState.Job = Start-ScriptExecution -ScriptPath $cachedScriptPath -ParamValues $paramValues -LogFile $logFile -WorkingDirectory $PSScriptRoot + + $timer = New-Object System.Windows.Threading.DispatcherTimer + $timer.Interval = [TimeSpan]::FromMilliseconds(300) + $execState.Timer = $timer + + $timer.Add_Tick({ + if (Test-Path $execState.LogFile) { + $content = Get-Content $execState.LogFile -Raw -Encoding UTF8 -ErrorAction SilentlyContinue + if ($content -and $content.Length -gt $execState.LastLen) { + $delta = $content.Substring($execState.LastLen) + foreach ($deltaLine in ($delta -split "`r`n|`n")) { + if ($deltaLine -ne "") { Add-LogLine -LogBox $logBox -Line $deltaLine } + } + $logBox.ScrollToEnd() + $execState.LastLen = $content.Length + } + } + + if ($execState.Job.State -in @("Completed", "Failed", "Stopped")) { + $timer.Stop() + $jobFailed = ($execState.Job.State -eq "Failed") + Remove-Job -Job $execState.Job -Force -ErrorAction SilentlyContinue + Remove-Item $execState.LogFile -Force -ErrorAction SilentlyContinue + $execState.Job = $null + $execState.Timer = $null + $execState.LogFile = $null + + $btnLancer.IsEnabled = $true + $lv.IsEnabled = $true + + if ($jobFailed) { + Add-LogLine -LogBox $logBox -Line (T "log.finishedError") -Level Error + Show-MessageBox -Message "$(T 'msg.errorExec') '$scriptName'." -Title (T "msg.errorExecTitle") -Type "Error" + } else { + Add-LogLine -LogBox $logBox -Line (T "log.finishedOk") -Level Ok + } + $logBox.ScrollToEnd() + } + }.GetNewClosure()) + + $timer.Start() + }.GetNewClosure()) + }.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) + $lv.Add_SelectionChanged({ + if ($lv.SelectedItem) { + & $ShowScriptDetail -ScriptData $lv.SelectedItem.Tag } - } - & $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 + $btnFlagFr.Add_Click({ + Set-CadMasterLanguage -Lang "fr" + & $UpdateTexts + if ($lv.SelectedItem -and -not $execState.Job) { + & $ShowScriptDetail -ScriptData $lv.SelectedItem.Tag } - if (-not [string]::IsNullOrWhiteSpace("$value")) { - $result[$name] = $value + }.GetNewClosure()) + + $btnFlagEn.Add_Click({ + Set-CadMasterLanguage -Lang "en" + & $UpdateTexts + if ($lv.SelectedItem -and -not $execState.Job) { + & $ShowScriptDetail -ScriptData $lv.SelectedItem.Tag } - } - return $result -} + }.GetNewClosure()) -# ===================================================================== -# FONCTION : Invoke-SelectedScript -# Execute le script local avec les parametres saisis par l'utilisateur. -# ===================================================================== -function Invoke-SelectedScript { - param( - [string]$ScriptName, - [hashtable]$ParamValues = @{} - ) + $w.Add_Closing({ + & $StopRunningExecution + }.GetNewClosure()) - $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" + & $UpdateTexts + & $SetDetailPlaceholder - 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" - } + $w.ShowDialog() | Out-Null } # ===================================================================== @@ -1055,7 +1669,7 @@ function Invoke-SelectedScript { Clear-Host Write-Host "" Write-Host " +==========================================+" -ForegroundColor Cyan -Write-Host " | cadMasterCLI v1.1 |" -ForegroundColor Cyan +Write-Host " | cadMasterCLI v$CADMASTER_VERSION |" -ForegroundColor Cyan Write-Host " | Launcher de scripts PowerShell |" -ForegroundColor Cyan Write-Host " +==========================================+" -ForegroundColor Cyan Write-Host "" @@ -1072,6 +1686,18 @@ if (-not (Test-Path $LOCAL_CACHE_DIR)) { Write-Log "Dossier cache cree : $LOCAL_CACHE_DIR" "OK" } +# === Etape 0 : Verifier une mise a jour de cadMasterCLI lui-meme === +$remoteVersion = Test-CadMasterUpdate +if ($remoteVersion) { + $rep = Show-MessageBox -Message "$(T 'msg.selfUpdateAvailable') ($remoteVersion).`n$(T 'msg.selfUpdateConfirm')" ` + -Title (T "msg.selfUpdateTitle") -Type "Question" + if ($rep -eq "Yes") { + if (Invoke-CadMasterSelfUpdate -RemoteVersion $remoteVersion) { + exit 0 + } + } +} + # === Etape 1 : Recuperer la liste des scripts depuis Gitea === $rawList = Get-ScriptList $modeOffline = $false @@ -1084,7 +1710,7 @@ if ($null -eq $rawList) { 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" + Show-MessageBox -Message (T "msg.offline") -Title (T "msg.errorTitle") -Type "Error" exit 1 } } @@ -1105,12 +1731,8 @@ $scripts = @($rawList) | ForEach-Object { 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 @@ -1118,74 +1740,7 @@ $scripts = @($rawList) | ForEach-Object { } } -# === 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" - - - +# === Etape 3 : Fenetre unique (liste + parametres + logs) === +Show-MainWindow -Scripts $scripts +Write-Log "Fermeture de cadMasterCLI." diff --git a/doc_contribution/CONTRIBUTING_SCRIPTS.md b/doc_contribution/CONTRIBUTING_SCRIPTS.md index 4f7e77b..8712018 100644 --- a/doc_contribution/CONTRIBUTING_SCRIPTS.md +++ b/doc_contribution/CONTRIBUTING_SCRIPTS.md @@ -55,7 +55,40 @@ cadMasterCLI détecte automatiquement un paramètre comme **fichier** et affiche 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 +### 4. README bilingue FR/EN + +Depuis cadMasterCLI v2, l'interface peut s'afficher en français ou en anglais. Pour que la documentation du script (panneau de gauche) suive la langue active, le `README.md` du dépôt peut contenir les deux versions dans un seul fichier, séparées par des marqueurs HTML : + +```markdown + +# Nom du script + +Description en français... + +## Paramètres + +- `MonParam` : ce que fait ce paramètre. + + +# Script name + +Description in English... + +## Parameters + +- `MonParam`: what this parameter does. +``` + +Règles : + +- Les marqueurs `` et `` doivent être seuls sur leur ligne. +- L'ordre des sections n'a pas d'importance. +- Si une seule des deux langues est présente, cadMasterCLI l'affiche quelle que soit la langue active (pas d'erreur). +- **Rétro-compatibilité** : un `README.md` sans aucun marqueur continue de s'afficher tel quel dans les deux langues (comportement identique aux versions précédentes de cadMasterCLI). + +Voir `_template_script.README.md` dans ce dépôt pour un exemple complet. + +### 5. Structure du dépôt Gitea Le dépôt doit contenir à sa racine : @@ -88,6 +121,6 @@ MonScript/ ## Exemple minimal conforme -Voir `_template_script.ps1` dans ce dépôt pour un squelette prêt à l'emploi. +Voir `_template_script.ps1` (script) et `_template_script.README.md` (documentation bilingue) 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/version.txt b/version.txt new file mode 100644 index 0000000..227cea2 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +2.0.0