# Corporate Windows Tooling

> Crear herramientas de escritorio Windows que funcionen en entornos corporativos restringidos — PowerShell + .NET, sin admin, sin .exe

- Skill: `ntizar/corporate-windows-tooling` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add ntizar/corporate-windows-tooling`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ntizar/corporate-windows-tooling/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: ntizar (https://skillmd.com/u/ntizar)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ntizar/corporate-windows-tooling

---


# Corporate Windows Tooling

Patrones para crear herramientas de escritorio que funcionen en entornos corporativos donde PyInstaller `.exe` está bloqueado por políticas de seguridad.

## Trigger

Activar cuando el usuario mencione:
- "No funciona en el ordenador de la empresa"
- "Sin ser administrador no me deja usarlo"
- "Antivirus bloquea el .exe"
- "AppLocker" / "políticas de grupo"
- "No tengo permisos de instalación"
- Necesita herramienta de escritorio Windows para usuarios no técnicos

## Decision Matrix

| Solución | Cuándo usarla | Requisitos |
|----------|--------------|------------|
| **PyInstaller .exe** | Primera opción si no hay restricciones | Python + PyInstaller |
| **PowerShell + .NET** | Entorno corporativo restringido | Windows 10+ (ya incluido) |
| **Batch + Python** | Si Python ya está instalado en los PCs | Python en PATH |
| **HTML + JavaScript** | Solo lectura/exportación, sin modificar archivos | Navegador |

**Regla:** Si el usuario menciona "corporativo", "empresa", "sin admin" → PowerShell + .NET directamente. No preguntar.

## Patrón PowerShell + .NET

### Estructura del proyecto

```
mi-herramienta/
├── ejecutar.bat              # Lanzador (doble clic)
├── src/
│   ├── Mi-Script.ps1        # Script principal
│   └── lib/
│       └── libreria.dll     # Librería .NET (ej: itextsharp.dll)
├── README.md
└── SPEC.md
```

### ejecutar.bat (siempre el mismo patrón)

```batch
@echo off
chcp 65001 >nul 2>&1
title Título de la Herramienta
echo ========================================
echo   Nombre de la Herramienta
echo   Hecho con ❤️ por David Antizar
echo ========================================
echo.
echo Iniciando herramienta...
echo.

:: Verificar si PowerShell está disponible
where powershell.exe >nul 2>&1
if %errorlevel% neq 0 (
    echo ERROR: No se encontro PowerShell en el sistema.
    pause
    exit /b 1
)

:: Ejecutar script PowerShell
powershell.exe -ExecutionPolicy Bypass -File "%~dp0src\Mi-Script.ps1"

if %errorlevel% neq 0 (
    echo.
    echo La herramienta se cerro con errores.
    pause
)
```

**Pitfall:** `%~dp0` resuelve la ruta relativa al .bat, no al directorio de trabajo actual. Essential para que funcione desde cualquier ubicación.

### Script PowerShell — Cabecera

```powershell
#Requires -Version 5.1
<#
.SYNOPSIS
    Descripción corta
.DESCRIPTION
    Descripción detallada
.NOTES
    Autor: David Antizar (Ntizar)
    Hecho con ❤️ por David Antizar
#>

$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$LibDir = Join-Path $ScriptDir "lib"
$LibDll = Join-Path $LibDir "libreria.dll"
```

### Cargar librería .NET

```powershell
if (-not (Test-Path $LibDll)) {
    [System.Windows.Forms.MessageBox]::Show(
        "No se encuentra libreria.dll en:`n$LibDir",
        "Error", "OK", "Error"
    )
    exit 1
}
Add-Type -Path $LibDll
```

### GUI con Windows Forms

```powershell
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

$form = New-Object System.Windows.Forms.Form
$form.Text = "Título - David Antizar"
$form.Size = New-Object System.Drawing.Size(800, 650)
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedSingle"
$form.MaximizeBox = $false
$form.Font = New-Object System.Drawing.Font("Segoe UI", 9)
$form.BackColor = [System.Drawing.Color]::White
```

### Leer Excel con COM (sin librería externa)

```powershell
function Read-ExcelFile {
    param([string]$RutaArchivo)
    
    $excel = New-Object -ComObject Excel.Application
    $excel.Visible = $false
    $excel.DisplayAlerts = $false
    
    $workbook = $excel.Workbooks.Open($RutaArchivo)
    $worksheet = $workbook.Sheets[1]
    
    $usedRange = $worksheet.UsedRange
    $filas = $usedRange.Rows.Count
    $columnas = $usedRange.Columns.Count
    
    # Cabeceras (primera fila)
    $cabeceras = @()
    for ($col = 1; $col -le $columnas; $col++) {
        $cabeceras += $worksheet.Cells.Item(1, $col).Text
    }
    
    # Datos
    $datos = @()
    for ($fila = 2; $fila -le $filas; $fila++) {
        $valores = @{}
        for ($col = 1; $col -le $columnas; $col++) {
            $valores[$cabeceras[$col-1]] = $worksheet.Cells.Item($fila, $col).Text
        }
        $datos += [PSCustomObject]$valores
    }
    
    $workbook.Close($false)
    $excel.Quit()
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) | Out-Null
    
    return @{ Cabeceras = $cabeceras; Datos = $datos }
}
```

**Pitfall:** Sin `[System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel)`, el proceso EXCEL.EXE queda zombie en segundo plano.

### Escribir Excel con COM

```powershell
function Write-ExcelFile {
    param([string]$RutaArchivo, [array]$Datos)
    
    $excel = New-Object -ComObject Excel.Application
    $excel.Visible = $false
    $excel.DisplayAlerts = $false
    
    $workbook = $excel.Workbooks.Add()
    $worksheet = $workbook.Sheets[1]
    $worksheet.Name = "METADATOS"
    
    $columnas = $Datos[0].PSObject.Properties.Name
    
    # Cabeceras
    for ($col = 0; $col -lt $columnas.Count; $col++) {
        $worksheet.Cells.Item(1, $col + 1) = $columnas[$col]
    }
    
    # Datos
    for ($fila = 0; $fila -lt $Datos.Count; $fila++) {
        for ($col = 0; $col -lt $columnas.Count; $col++) {
            $worksheet.Cells.Item($fila + 2, $col + 1) = $Datos[$fila].$($columnas[$col])
        }
    }
    
    $worksheet.Columns.AutoFit() | Out-Null
    $workbook.SaveAs($RutaArchivo)
    $workbook.Close($false)
    $excel.Quit()
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) | Out-Null
}
```

## Descargar librerías .NET desde NuGet

Si necesitas una librería .NET (ej: iTextSharp para PDFs):

```bash
# Descargar paquete NuGet
curl -sL "https://www.nuget.org/api/v2/package/iTextSharp/5.5.13.4" -o itextsharp.nupkg

# Extraer DLL
python3 -c "
import zipfile
with zipfile.ZipFile('itextsharp.nupkg', 'r') as z:
    z.extractall('itextsharp')
    for f in z.namelist():
        if f.endswith('.dll'):
            print(f)
"
# Resultado: lib/net461/itextsharp.dll

# Copiar al proyecto
cp itextsharp/lib/net461/itextsharp.dll /ruta/proyecto/src/lib/
```

**Pitfall:** Los `.nupkg` son archivos ZIP. Python `zipfile` funciona para extraerlos si `unzip` no está disponible.

## Patrón iTextSharp para PDFs

```powershell
Add-Type -Path "$LibDir\itextsharp.dll"

# Leer metadatos de PDF
$reader = New-Object iTextSharp.text.pdf.PdfReader($RutaPdf)
$info = $reader.Info  # Hashtable con /Title, /Author, etc.
$reader.Close()

# Escribir metadatos en PDF
$tempFile = $RutaPdf + ".tmp"
$reader = New-Object iTextSharp.text.pdf.PdfReader($RutaPdf)
$stamper = New-Object iTextSharp.text.pdf.PdfStamper($reader, [System.IO.File]::Create($tempFile))
$infoDict = $stamper.Writer.Info
$infoDict["/Title"] = "Nuevo título"
$infoDict["/Author"] = "Autor"
$stamper.Close()
$reader.Close()

# Reemplazar original
Remove-Item $RutaPdf -Force
Rename-Item $tempFile $RutaPdf
```

## Pitfalls

1. **Execution Policy Bypass:** Siempre usar `powershell.exe -ExecutionPolicy Bypass -File` en el .bat. Sin esto, PowerShell bloquea scripts por defecto.

2. **COM Excel zombie:** Siempre hacer `ReleaseComObject` después de usar Excel COM. Sino, `EXCEL.EXE` queda en memoria.

3. **iTextSharp 5.x vs 7.x:** iTextSharp 5.5.x es la versión con namespace `iTextSharp.text.pdf`. iTextSharp 7.x cambió a `iText.Kernel.Pdf`. Usar 5.5.x para simplificar.

4. **Ruta relativa:** `%~dp0` en .bat = directorio del .bat. `$MyInvocation.MyCommand.Path` en PowerShell = ruta del script. Nunca usar rutas absolutas hardcodeadas.

5. **GUI PowerShell:** Windows Forms en PowerShell es limitado pero suficiente para herramientas simples. No intentar hacer frameworks complejos.

6. **CSV fallback:** Si Excel no está instalado, el COM approach falla. Ofrecer CSV como alternativa. El usuario puede exportar Excel a CSV.

7. **pyinstaller .exe sin admin:** Si el usuario puede ejecutar .exe pero no instalar Python, PyInstaller sigue siendo la primera opción. PowerShell es solo cuando el .exe está bloqueado por políticas.

