Files
ROMFASTSQL/proxmox/lxc108-oracle/scripts/build-client-kit.ps1
Marius 7fce925349 feat(oracle): kit de instalare la client + corectarea lipsurilor din instalare
Blocul de instalare/migrare Oracle 21c XE, terminat si validat pe VM 302.

Kit de instalare (nou)

  scripts/build-client-kit.ps1 exporta CONTAFIN_ORACLE si FIRMANOUA de pe
  LXC 108 (PDB ROA2, nu ROA - ROA e productia ROA_CENTRAL), le aduce local
  prin cele trei hopuri container -> LXC -> host -> statie, le redenumeste la
  numele pe care le asteapta scripturile si le impacheteaza cu o copie a
  directorului roa-windows-setup. Citeste logul expdp si raporteaza cate
  firme are NOM_FIRME, ca sa nu plece din greseala datele unui client.

  roa-windows-setup/INSTALEAZA.cmd ruleaza pasii 01..08 fara "set /p", deci
  merge si prin SSH - RunAll.cmd nu poate fi rulat neinteractiv. Trateaza
  corect codul 5 de la pasul 03 si se opreste la prima eroare reala.

Corectii

  01-setup-database.ps1 nu mai scrie SQLNET.RECV_TIMEOUT / SEND_TIMEOUT in
  sqlnet.ora. Le hardcoda la 30 s, iar fisierul e citit si de impdp: in timpul
  unui import serverul poate lucra minute fara sa trimita un pachet, clientul
  murea cu ORA-12609 iar scriptul raporta esec pentru un import care se
  terminase cu bine pe server. Reprodus si reparat pe VM 302.

  01-setup-database.ps1: Step 4c optional (-ResetSystemPassword) care curata
  EXPIRED(GRACE) pe SYSTEM in CDB root; ALTER PROFILE opreste expirarile
  viitoare dar nu reseteaza un cont deja intrat in gratie.

  uninstall-roa.sql: retry la DROP USER dupa KILL SESSION, plus un bloc final
  de verdict care numara ce a ramas si iese cu ORA-20900 daca baza nu e
  curata. Pana acum toate sectiunile prindeau exceptiile in WHEN OTHERS si
  scriptul tiparea "UNINSTALL COMPLETE" chiar si cand un user supravietuise,
  iar instalarea urmatoare dadea ORA-31684 in lant. 99-uninstall-roa.ps1
  propaga codul de iesire.

  08-post-install-config.ps1: Step 7b seteaza SMTP_OUT_SERVER si ACL-ul de
  retea pentru CONTAFIN_ORACLE. UTL_MAIL se instala si primea EXECUTE, adica
  destul cat sa compileze, dar nu si cat sa trimita. Privilegiul resolve nu
  accepta interval de porturi (ORA-24244), deci se acorda separat de connect.

  07-verify-installation.ps1: sectiune noua care verifica prezenta lui
  FIRMANOUA.dmp in DMPDIR si o raporteaza ca eroare daca lipseste. Fara el,
  adaugarea unei firme noi pica in DBMS_DATAPUMP.ADD_FILE peste luni de la
  instalare, cand nimeni nu mai leaga eroarea de instalare.

  configure-profile.sql: antetul spune ca e unealta de remediere manuala, nu
  parte din flux; pas nou la final pentru CDB root.

Documentatie

  README-ul roa-windows-setup descrie acum explicit cele doua scenarii -
  instalare pe curat si migrare - de unde se iau DMP-urile sablon, si capcana
  ORA-12609. Handoff-urile de sesiune au fost sterse; ce era durabil in ele a
  intrat in README-uri si in docs/diagnostic-spatiu-clienti.md.

Validare pe VM 302: ciclu complet din kit, instalare pe curat, verificarea
finala "[OK] All checks passed!".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzF1hf4aFS1tmJWpPMiwGp
2026-08-25 18:05:56 +03:00

460 lines
19 KiB
PowerShell

#Requires -Version 5.1
<#
.SYNOPSIS
Construieste kitul de instalare ROA pentru un client: DMP-urile sablon
exportate din LXC 108 + scripturile de instalare/migrare, intr-un singur
director gata de copiat pe serverul clientului.
.DESCRIPTION
Ce face, in ordine:
1. Ruleaza exportul pe LXC 108 (`export-roa2.sh`, acelasi script folosit
de `export-roa2.ps1`). Sursa e PDB-ul ROA2, nu ROA - ROA e instanta
de productie ROA_CENTRAL si nu se scrie in ea.
2. Aduce arhivele .tar.gz pe statia locala. Sunt trei niveluri:
container Docker -> LXC 108 -> host pvemini -> statia ta. `docker cp`
te scoate doar din container, de aceea e nevoie si de `pct pull`.
3. Despacheteaza si redenumeste DMP-urile la numele pe care le asteapta
scripturile: `contafin_oracle.dmp` si `FIRMANOUA.dmp`.
4. Citeste logul expdp si raporteaza cate firme are NOM_FIRME in dump.
Asa vezi daca ai construit un sablon curat sau ai prins din greseala
datele unui client.
5. Copiaza peste ele continutul din `roa-windows-setup/`.
Rezultatul e un director care se dezarhiveaza la client si se ruleaza cu
`INSTALEAZA.cmd curat` sau `INSTALEAZA.cmd migrare`.
.PARAMETER OracleVersion
Sursa exportului: '21' (PDB ROA2, VERSION=19) sau '18' (container
oracle18-xe, VERSION=11.2, pentru baze mai vechi). Implicit '21'.
.PARAMETER OutputDir
Directorul parinte in care se creeaza kitul. Implicit E:\backups\roa-kit.
.PARAMETER KitName
Numele directorului de kit. Implicit roa-kit-<data>.
.PARAMETER SkipExport
Nu ruleaza export nou; foloseste cele mai recente arhive deja prezente pe
LXC 108. Util cand ai exportat deja si vrei doar sa reconstruiesti kitul.
.PARAMETER Zip
Creeaza si o arhiva .zip a kitului, langa director.
.PARAMETER PushTo
Optional, destinatie scp pentru kit dupa constructie, in forma
`utilizator@gazda:/cale`. Se trimite directorul intreg, recursiv.
.EXAMPLE
.\build-client-kit.ps1
Export nou din ROA2 si kit in E:\backups\roa-kit\roa-kit-<data>.
.EXAMPLE
.\build-client-kit.ps1 -Zip -KitName "roa-kit-clientX"
Kit cu nume fix, plus arhiva .zip de trimis clientului.
.EXAMPLE
.\build-client-kit.ps1 -SkipExport -PushTo "romfast@10.0.20.130:C:/roa-kit"
Refoloseste ultimele arhive si trimite kitul direct pe serverul tinta.
.NOTES
Necesita ssh/scp (OpenSSH din Windows 10+) si tar (bsdtar, tot din Windows).
Acces cu cheie la host-ul Proxmox - vezi docs/acces-ssh-chei-angajati.md.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[ValidateSet('21', '18')]
[string]$OracleVersion = '21',
[Parameter(Mandatory = $false)]
[string]$OutputDir = 'E:\backups\roa-kit',
[Parameter(Mandatory = $false)]
[string]$KitName,
[Parameter(Mandatory = $false)]
[switch]$SkipExport,
[Parameter(Mandatory = $false)]
[switch]$Zip,
[Parameter(Mandatory = $false)]
[string]$PushTo,
[Parameter(Mandatory = $false)]
[string]$ProxmoxHost = 'root@10.0.20.201',
[Parameter(Mandatory = $false)]
[int]$Ctid = 108
)
$ErrorActionPreference = 'Stop'
# Schemele exportate si numele sub care ajung in kit. FIRMANOUA.dmp trebuie sa
# aiba exact numele asta: SYS.NEWSCHEMA il compune ca <schema-sursa> || '.dmp'.
$SchemaMap = [ordered]@{
'CONTAFIN_ORACLE' = 'contafin_oracle.dmp'
'FIRMANOUA' = 'FIRMANOUA.dmp'
}
if ($OracleVersion -eq '18') {
$Container = 'oracle18-xe'
$RemoteDmpDir = '/opt/oracle18/oradata/dmpdir'
$SourceLabel = 'Oracle 18c (XEPDB1), VERSION=11.2'
}
else {
$Container = 'oracle-xe'
$RemoteDmpDir = '/opt/oracle/oradata/dmpdir'
$SourceLabel = 'Oracle 21c (PDB ROA2), VERSION=19'
}
function Write-Step { param([string]$m) Write-Host "`n=== $m ===" -ForegroundColor Cyan }
function Write-Ok { param([string]$m) Write-Host " [OK] $m" -ForegroundColor Green }
function Write-Info { param([string]$m) Write-Host " $m" }
function Write-Warn { param([string]$m) Write-Host " [ATENTIE] $m" -ForegroundColor Yellow }
function Invoke-Remote {
<# Ruleaza o comanda pe host-ul Proxmox si intoarce iesirea.
Opreste scriptul daca ssh iese cu cod nenul. #>
param(
[Parameter(Mandatory = $true)][string]$Command,
[switch]$AllowFailure
)
# Fara "2>&1": in Windows PowerShell 5.1 redirectarea stderr-ului unei
# comenzi native impacheteaza fiecare linie intr-un ErrorRecord, iar
# mesajul de eroare devine "System.Management.Automation.RemoteException".
# Lasam stderr sa curga direct in consola si ne bazam pe codul de iesire.
$out = & ssh -o BatchMode=yes $ProxmoxHost $Command
if ($LASTEXITCODE -ne 0 -and -not $AllowFailure) {
throw "Comanda pe $ProxmoxHost a esuat (cod $LASTEXITCODE): $Command"
}
return $out
}
# =============================================================================
# 0. Validari locale
# =============================================================================
Write-Step "Verificari initiale"
$SetupDir = Join-Path (Split-Path $PSScriptRoot -Parent) 'roa-windows-setup'
if (-not (Test-Path $SetupDir -PathType Container)) {
throw "Nu gasesc directorul cu scripturile de instalare: $SetupDir"
}
$InstallCmd = Join-Path $SetupDir 'INSTALEAZA.cmd'
if (-not (Test-Path $InstallCmd)) {
throw "Nu gasesc $InstallCmd - kitul ar fi inutilizabil fara el."
}
Write-Ok "Scripturi de instalare: $SetupDir"
foreach ($tool in @('ssh', 'scp', 'tar')) {
if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) {
throw "Lipseste '$tool' din PATH. Pe Windows 10+ vine cu OpenSSH / bsdtar."
}
}
Write-Ok "ssh, scp, tar disponibile"
if (-not $KitName) {
$KitName = "roa-kit-$(Get-Date -Format 'yyyyMMdd_HHmmss')"
}
$KitDir = Join-Path $OutputDir $KitName
if (Test-Path $KitDir) {
throw "Directorul $KitDir exista deja. Sterge-l sau foloseste alt -KitName."
}
$Staging = Join-Path ([System.IO.Path]::GetTempPath()) "roa-kit-staging-$([System.Guid]::NewGuid().ToString('N').Substring(0,8))"
New-Item -ItemType Directory -Path $Staging -Force | Out-Null
try {
# =========================================================================
# 1. Starea LXC 108 si a containerului Oracle
# =========================================================================
Write-Step "Starea LXC $Ctid si a containerului $Container"
$lxcStatus = Invoke-Remote "pct status $Ctid"
if ($lxcStatus -notmatch 'running') {
throw "LXC $Ctid nu ruleaza ($lxcStatus). Porneste-l cu: ssh $ProxmoxHost `"pct start $Ctid`""
}
Write-Ok "LXC $Ctid ruleaza"
$running = Invoke-Remote "pct exec $Ctid -- docker ps --format '{{.Names}}'"
if ($running -notcontains $Container) {
throw "Containerul $Container nu ruleaza in LXC $Ctid. Porneste-l cu: ssh $ProxmoxHost `"pct exec $Ctid -- docker start $Container`" si asteapta ~60s."
}
Write-Ok "Containerul $Container ruleaza"
Write-Info "Sursa exportului: $SourceLabel"
# =========================================================================
# 2. Exportul
# =========================================================================
if ($SkipExport) {
Write-Step "Export sarit (-SkipExport) - folosesc arhivele existente"
}
else {
Write-Step "Export din $SourceLabel"
Write-Info "Dureaza cateva minute. Ruleaza export-roa2.sh pe LXC $Ctid..."
$exportOut = Invoke-Remote "pct exec $Ctid -- bash /opt/oracle/oradata/export-roa2.sh $OracleVersion"
$exportOut | Where-Object { $_ -match 'EROARE|ORA-|successfully completed|elapsed' } |
ForEach-Object { Write-Info $_ }
Write-Ok "Export terminat"
}
# =========================================================================
# 3. Aducerea arhivelor: container -> LXC -> host -> statie
# =========================================================================
Write-Step "Aduc arhivele pe statia locala"
$archives = @{}
foreach ($schema in $SchemaMap.Keys) {
$newest = (Invoke-Remote "pct exec $Ctid -- bash -c 'ls -t $RemoteDmpDir/${schema}*.tar.gz 2>/dev/null | head -1'" | Select-Object -First 1)
if ([string]::IsNullOrWhiteSpace($newest)) {
throw "Nu am gasit nicio arhiva ${schema}*.tar.gz in $RemoteDmpDir. Ruleaza fara -SkipExport."
}
$newest = $newest.Trim()
$leaf = Split-Path $newest -Leaf
# pct pull scoate fisierul din LXC pe host-ul Proxmox; scp il aduce mai
# departe. Fara pct pull, scp ar citi de pe host, unde nu exista nimic.
Invoke-Remote "pct pull $Ctid $newest /tmp/$leaf" | Out-Null
& scp -o BatchMode=yes "${ProxmoxHost}:/tmp/$leaf" (Join-Path $Staging $leaf) | Out-Null
if ($LASTEXITCODE -ne 0) { throw "scp a esuat pentru $leaf" }
Invoke-Remote "rm -f /tmp/$leaf" -AllowFailure | Out-Null
$localArchive = Join-Path $Staging $leaf
$sizeMb = [math]::Round((Get-Item $localArchive).Length / 1MB, 1)
Write-Ok "$leaf ($sizeMb MB)"
$archives[$schema] = $localArchive
}
# =========================================================================
# 4. Despachetare, redenumire, verificarea continutului
# =========================================================================
Write-Step "Despachetez si verific"
$KitDmpDir = Join-Path $KitDir 'DMPDIR'
New-Item -ItemType Directory -Path $KitDmpDir -Force | Out-Null
$nomFirmeRows = $null
foreach ($schema in $SchemaMap.Keys) {
$extractDir = Join-Path $Staging "x_$schema"
New-Item -ItemType Directory -Path $extractDir -Force | Out-Null
& tar -xzf $archives[$schema] -C $extractDir | Out-Null
if ($LASTEXITCODE -ne 0) { throw "tar a esuat la despachetarea $($archives[$schema])" }
$dmp = Get-ChildItem -Path $extractDir -Filter '*.dmp' -File | Select-Object -First 1
if (-not $dmp) { throw "Arhiva pentru $schema nu contine niciun fisier .dmp" }
$target = Join-Path $KitDmpDir $SchemaMap[$schema]
Copy-Item -Path $dmp.FullName -Destination $target -Force
$sizeMb = [math]::Round((Get-Item $target).Length / 1MB, 1)
Write-Ok "$($SchemaMap[$schema]) ($sizeMb MB)"
# Logul expdp spune exact cate randuri are fiecare tabela. Pentru
# CONTAFIN_ORACLE, NOM_FIRME e indicatorul care separa un sablon curat
# de datele unui client.
$log = Get-ChildItem -Path $extractDir -Filter '*.log' -File | Select-Object -First 1
if ($log -and $schema -eq 'CONTAFIN_ORACLE') {
$m = Select-String -Path $log.FullName -Pattern '"NOM_FIRME"\s+\S+\s+\S+\s+(\d+)\s+rows' |
Select-Object -First 1
if ($m) { $nomFirmeRows = [int]$m.Matches[0].Groups[1].Value }
}
}
if ($null -eq $nomFirmeRows) {
Write-Warn "Nu am putut citi numarul de firme din logul expdp. Verifica manual continutul lui NOM_FIRME."
}
elseif ($nomFirmeRows -le 5) {
Write-Ok "NOM_FIRME: $nomFirmeRows randuri - sablon curat, potrivit pentru instalare pe curat"
}
else {
Write-Warn "NOM_FIRME are $nomFirmeRows randuri - NU e un sablon curat!"
Write-Warn "Dump-ul contine firmele unei baze reale. Nu-l trimite altui client."
}
# =========================================================================
# 5. Scripturile de instalare
# =========================================================================
Write-Step "Copiez scripturile de instalare"
# Kitul are structura lui roa-windows-setup direct in radacina, plus DMPDIR:
# Run.cmd si INSTALEAZA.cmd se bazeaza pe %~dp0scripts, deci trebuie sa fie
# la acelasi nivel cu scripts\.
$excludeNames = @('logs', 'test')
# Gunoiul din working tree nu are ce cauta la client. *.dmp e exclus ca
# masura de siguranta: daca cineva a lasat dump-ul unui client in
# roa-windows-setup, nu vrem sa plece mai departe.
$excludePatterns = @('*.stackdump', '*.zip', '*.dmp', '*.log', 'config.ps1')
Get-ChildItem -Path $SetupDir -Force | Where-Object {
$item = $_
($excludeNames -notcontains $item.Name) -and
(-not ($excludePatterns | Where-Object { $item.Name -like $_ }))
} | ForEach-Object {
Copy-Item -Path $_.FullName -Destination $KitDir -Recurse -Force
}
# Acelasi filtru, aplicat si in subdirectoare (config.ps1 poate contine
# parolele altui client, logurile pot contine nume de firme).
# DMPDIR e scutit: acolo tocmai am pus sabloanele, iar '*.dmp' e in lista de
# excluderi. Fara filtrul asta, pasul de curatare isi sterge propriul rezultat.
foreach ($pat in $excludePatterns) {
Get-ChildItem -Path $KitDir -Filter $pat -Recurse -File -Force -ErrorAction SilentlyContinue |
Where-Object { $_.DirectoryName -ne $KitDmpDir } |
Remove-Item -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Path (Join-Path $KitDir 'logs') -Force | Out-Null
# Plasa de siguranta: in kit au voie exact cele doua sabloane, nimic altceva.
$strayDmp = Get-ChildItem -Path $KitDir -Filter '*.dmp' -Recurse -File |
Where-Object { $_.DirectoryName -ne $KitDmpDir }
if ($strayDmp) {
throw "Fisiere .dmp neasteptate in kit, in afara DMPDIR: $($strayDmp.FullName -join ', ')"
}
# ...si invers: sabloanele TREBUIE sa fie acolo. Verificarea asta prinde
# orice pas ulterior care le-ar sterge din greseala.
foreach ($expected in $SchemaMap.Values) {
$p = Join-Path $KitDmpDir $expected
if (-not (Test-Path $p)) {
throw "Lipseste $expected din kit - constructia e incompleta."
}
if ((Get-Item $p).Length -lt 1MB) {
throw "$expected are sub 1 MB - fisier trunchiat sau gresit."
}
}
$copied = (Get-ChildItem -Path $KitDir -Recurse -File).Count
Write-Ok "$copied fisiere in kit"
# =========================================================================
# 6. Instructiunile
# =========================================================================
$sursa = if ($SkipExport) { "$SourceLabel (arhive existente, neexportate acum)" } else { $SourceLabel }
$firme = if ($null -ne $nomFirmeRows) { "$nomFirmeRows" } else { "necunoscut" }
$citeste = @"
KIT DE INSTALARE ROA ORACLE
===========================
Construit: $(Get-Date -Format 'yyyy-MM-dd HH:mm')
Sursa DMP-urilor: $sursa
Firme in NOM_FIRME din contafin_oracle.dmp: $firme
CE E IN KIT
-----------
DMPDIR\contafin_oracle.dmp schema comuna (drepturi, actualizari, NOM_FIRME)
DMPDIR\FIRMANOUA.dmp sablon de firma goala
scripts\, sql\, config\ scripturile de instalare
INSTALEAZA.cmd ruleaza toti pasii, in ordine
README.md documentatia completa
CUM SE INSTALEAZA
-----------------
Inainte de orice: instaleaza Oracle 21c XE pe server (docs\00-INSTALL-ORACLE-XE.md),
cu parola SYS/SYSTEM 'romfastsoft'. Apoi, din radacina kitului:
A. CLIENT NOU, fara firme anterioare:
INSTALEAZA.cmd curat <IdClient> [ServerSMTP]
Prima firma se creeaza DUPA aceea, DIN APLICATIA ROA - nu din scripturi.
Aplicatia scrie firma in CONTAFIN_ORACLE.NOM_FIRME si apeleaza
SYS.NEWSCHEMA, care ii importa structura din FIRMANOUA.dmp.
B. MIGRARE de pe un Oracle mai vechi:
1. Inlocuieste DMPDIR\contafin_oracle.dmp cu cel exportat de la client
(cel din kit are NOM_FIRME goala).
2. Pune in DMPDIR si cate un .dmp per firma a clientului.
3. LASA FIRMANOUA.dmp pe loc - nu vine de la client, dar e obligatoriu.
4. INSTALEAZA.cmd migrare <IdClient> [ServerSMTP]
Instalarea e gata cand ultimul pas (07-verify-installation) iese cu codul 0.
DE STIUT
--------
- FIRMANOUA.dmp ramane in C:\DMPDIR si dupa instalare. E folosit la fiecare
firma noua, oricand pe viitor. Daca il stergi, adaugarea de firme va esua
cu o eroare in DBMS_DATAPUMP.ADD_FILE.
- Pasul 03 (import contafin) iese cu codul 5 si asta e NORMAL: e codul Data
Pump pentru "completed with errors", din ORA-31684 (userul exista deja,
creat de pasul 01) si ORA-39082 (warning-uri de compilare, rezolvate la
pasul 04). INSTALEAZA.cmd il trateaza corect.
- Conectarea se face la serviciul XEPDB1, niciodata la XE (CDB root).
- Daca reiei instalarea de la zero: scripts\99-uninstall-roa.ps1 trebuie sa
iasa cu codul 0. Orice alt cod inseamna ca au ramas obiecte in baza (uzual
o sesiune activa tinea userul ocupat) - nu porni instalarea peste ele.
- Logurile fiecarui pas sunt in logs\.
"@
Set-Content -Path (Join-Path $KitDir 'CITESTE-MA.txt') -Value $citeste -Encoding UTF8
# =========================================================================
# 7. Arhivare si livrare
# =========================================================================
$zipPath = $null
if ($Zip) {
Write-Step "Creez arhiva .zip"
$zipPath = Join-Path $OutputDir "$KitName.zip"
if (Test-Path $zipPath) { Remove-Item $zipPath -Force }
Compress-Archive -Path (Join-Path $KitDir '*') -DestinationPath $zipPath -CompressionLevel Optimal
$zipMb = [math]::Round((Get-Item $zipPath).Length / 1MB, 1)
Write-Ok "$zipPath ($zipMb MB)"
}
if ($PushTo) {
Write-Step "Trimit kitul catre $PushTo"
& scp -o BatchMode=yes -r $KitDir $PushTo | Out-Null
if ($LASTEXITCODE -ne 0) { throw "scp catre $PushTo a esuat (cod $LASTEXITCODE)" }
Write-Ok "Kit trimis"
}
# =========================================================================
# 8. Sumar
# =========================================================================
$totalMb = [math]::Round(((Get-ChildItem -Path $KitDir -Recurse -File | Measure-Object Length -Sum).Sum) / 1MB, 1)
Write-Host ""
Write-Host "============================================================" -ForegroundColor Green
Write-Host " KIT GATA" -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
Write-Host ""
Write-Host " Director: $KitDir"
if ($zipPath) { Write-Host " Arhiva: $zipPath" }
Write-Host " Marime: $totalMb MB"
Write-Host " NOM_FIRME: $firme randuri in contafin_oracle.dmp"
Write-Host ""
Write-Host " La client, din radacina kitului:"
Write-Host " INSTALEAZA.cmd curat <IdClient> (client nou)" -ForegroundColor Cyan
Write-Host " INSTALEAZA.cmd migrare <IdClient> (are date existente)" -ForegroundColor Cyan
Write-Host ""
Write-Host " Detalii complete: $KitDir\CITESTE-MA.txt"
Write-Host ""
}
catch {
Write-Host ""
Write-Host "[EROARE] $_" -ForegroundColor Red
if (Test-Path $KitDir) {
Write-Host "[INFO] Kit incomplet la $KitDir - sterge-l inainte de a relua." -ForegroundColor Yellow
}
exit 1
}
finally {
if (Test-Path $Staging) {
Remove-Item -Path $Staging -Recurse -Force -ErrorAction SilentlyContinue
}
}