diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 4fb152e..2f70949 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -76,12 +76,11 @@ Plans:
3. Generated install script relaunches in 64-bit PowerShell when Intune's 32-bit process triggers it (WOW64 guard)
4. Generated uninstall script removes printer, driver, and port cleanly
5. Generated detection script returns exit 0 when the printer is installed and exit 1 when it is not
-**Plans**: TBD
+**Plans**: 2 plans
Plans:
-- [ ] 04-01: Jinja2 PS install script template (pnputil two-step, SYSTEM vs user detection, WOW64 guard, idempotency)
-- [ ] 04-02: Uninstall and detection script templates
-- [ ] 04-03: Script generation API endpoint (accepts printer config, returns script content)
+- [ ] 04-01-PLAN.md — Install script generator with TDD (Jinja2 template, WOW64 guard, UAC elevation, pnputil two-step, duplex mapping, idempotency)
+- [ ] 04-02-PLAN.md — Uninstall + detection templates, script download API endpoints, router registration
### Phase 5: Package Export
**Goal**: Technicians can download a complete, ready-to-deploy package for either Intune or NinjaRMM in one click
@@ -109,5 +108,5 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5
| 1. Foundation | 3/3 | Complete | 2026-04-10 |
| 2. Driver Management | 1/2 | In Progress| |
| 3. Printer Configuration | 2/2 | Complete | 2026-04-10 |
-| 4. Script Generation | 0/3 | Not started | - |
+| 4. Script Generation | 0/2 | Not started | - |
| 5. Package Export | 0/3 | Not started | - |
diff --git a/.planning/phases/04-script-generation/04-01-PLAN.md b/.planning/phases/04-script-generation/04-01-PLAN.md
new file mode 100644
index 0000000..9145562
--- /dev/null
+++ b/.planning/phases/04-script-generation/04-01-PLAN.md
@@ -0,0 +1,191 @@
+---
+phase: 04-script-generation
+plan: 01
+type: tdd
+wave: 1
+depends_on: []
+files_modified:
+ - imptune/generators/script_generator.py
+ - imptune/templates/scripts/install.ps1.j2
+ - tests/test_script_generator.py
+autonomous: true
+requirements:
+ - SCRPT-01
+ - SCRPT-04
+ - SCRPT-05
+
+must_haves:
+ truths:
+ - "render_install() produces a complete PowerShell script containing pnputil /add-driver, Add-PrinterPort, Add-PrinterDriver, Add-Printer, Set-PrintConfiguration"
+ - "Generated install script contains WOW64 relaunch guard as the first executable block"
+ - "Generated install script contains SYSTEM vs user detection with UAC self-elevation"
+ - "Set-PrintConfiguration receives translated duplex values (TwoSidedLongEdge, TwoSidedShortEdge)"
+ - "All add operations are wrapped in idempotency checks (Get-PrinterPort, Get-Printer)"
+ artifacts:
+ - path: "imptune/generators/script_generator.py"
+ provides: "Jinja2 Environment + render_install function with duplex_map"
+ exports: ["render_install"]
+ - path: "imptune/templates/scripts/install.ps1.j2"
+ provides: "PowerShell install template with WOW64, UAC, pnputil, idempotency"
+ min_lines: 30
+ - path: "tests/test_script_generator.py"
+ provides: "Unit tests for SCRPT-01, SCRPT-04, SCRPT-05"
+ min_lines: 40
+ key_links:
+ - from: "imptune/generators/script_generator.py"
+ to: "imptune/templates/scripts/install.ps1.j2"
+ via: "Jinja2 FileSystemLoader"
+ pattern: "_env\\.get_template.*install"
+ - from: "imptune/generators/script_generator.py"
+ to: "imptune/db/models.py"
+ via: "Printer model fields used as template vars"
+ pattern: "printer\\.name|printer\\.ip_address|printer\\.port_name"
+---
+
+
+Create the script generator module and install.ps1 Jinja2 template with full correctness guards.
+
+Purpose: The install script is the most complex of the three scripts (WOW64, UAC, pnputil two-step, idempotency, duplex mapping). Building it first with TDD ensures all edge cases are covered before the simpler templates.
+
+Output: `script_generator.py` with `render_install()`, `install.ps1.j2` template, and passing unit tests.
+
+
+
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/04-script-generation/04-RESEARCH.md
+
+
+
+
+From imptune/db/models.py:
+```python
+class Printer(BaseModel):
+ name = CharField()
+ ip_address = CharField()
+ port_name = CharField()
+ client = ForeignKeyField(Client, null=True, backref="printers")
+ driver = ForeignKeyField(Driver, null=True, backref="printers")
+ duplex_mode = CharField(default="OneSided") # "OneSided" | "LongEdge" | "ShortEdge"
+ color_mode = BooleanField(default=True)
+ paper_size = CharField(default="A4") # "A4" | "Letter" | "Legal"
+ collate = BooleanField(default=True)
+
+class Driver(BaseModel):
+ sha256 = CharField(unique=True, index=True)
+ original_filename = CharField()
+ driver_desc = CharField(null=True) # JSON list: '["HP Universal Printing PCL 6"]'
+ inf_filename = CharField(null=True) # e.g. "hpcu270u.inf"
+```
+
+From imptune/generators/intunewin_builder.py (pattern reference):
+```python
+# Existing generator module pattern — script_generator.py follows same structure
+# Module-level setup, public render functions
+```
+
+
+
+
+
+
+ Install script generator with TDD
+ imptune/generators/script_generator.py, imptune/templates/scripts/install.ps1.j2, tests/test_script_generator.py
+
+ - render_install(printer_name, ip_address, port_name, driver_name, inf_filename, duplex_mode, color_mode, paper_size, collate) returns a string containing valid PowerShell
+ - Output contains WOW64 guard: `$env:PROCESSOR_ARCHITECTURE -eq "x86"` and `SysNative` relaunch as the FIRST executable block
+ - Output contains SYSTEM/admin detection: `WindowsIdentity::GetCurrent()`, `IsSystem`, `IsInRole(Administrator)`, `Start-Process -Verb Runas`
+ - Output contains pnputil two-step: `pnputil.exe /add-driver "$PSScriptRoot\drivers\{inf_filename}" /install` then `Add-PrinterDriver -Name "{driver_name}"`
+ - Output contains idempotent port creation: `Get-PrinterPort` check before `Add-PrinterPort`
+ - Output contains idempotent printer creation: `Get-Printer` check before `Add-Printer`
+ - Output contains `Set-PrintConfiguration` with translated duplex: "LongEdge" -> "TwoSidedLongEdge", "ShortEdge" -> "TwoSidedShortEdge", "OneSided" -> "OneSided"
+ - Output contains correct boolean rendering: color_mode=True -> `$true`, color_mode=False -> `$false`
+ - Output contains paper size and collate values
+ - Script uses proper quoting for printer name, port name, driver name (double-quoted in PS)
+
+
+ **RED phase — write tests first in tests/test_script_generator.py:**
+
+ 1. Create `imptune/templates/scripts/` directory (empty, needed for Jinja2 loader)
+ 2. Write tests that import `render_install` from `imptune.generators.script_generator` and assert on rendered output:
+ - `test_render_install_contains_pnputil`: assert `pnputil.exe /add-driver` and `Add-PrinterDriver` in output
+ - `test_render_install_print_config`: assert `Set-PrintConfiguration` with `-DuplexingMode TwoSidedLongEdge` when duplex_mode="LongEdge"
+ - `test_render_install_wow64_guard`: assert `PROCESSOR_ARCHITECTURE` and `SysNative` in output
+ - `test_render_install_uac_guard`: assert `IsSystem` and `Start-Process` and `-Verb Runas` in output
+ - `test_render_install_idempotency`: assert `Get-PrinterPort` and `Get-Printer` checks before add operations
+ - `test_render_install_booleans`: assert `$true` / `$false` for color and collate
+ 3. Run tests — all MUST fail (RED)
+
+ **GREEN phase — implement:**
+
+ 4. Create `imptune/generators/script_generator.py`:
+ - Module-level Jinja2 Environment with `FileSystemLoader` pointing to `imptune/templates/scripts/`
+ - `trim_blocks=True`, `lstrip_blocks=True`, `keep_trailing_newline=True`
+ - `_duplex_map` dict: `{"OneSided": "OneSided", "LongEdge": "TwoSidedLongEdge", "ShortEdge": "TwoSidedShortEdge"}`
+ - `render_install(printer_name, ip_address, port_name, driver_name, inf_filename, duplex_mode, color_mode, paper_size, collate) -> str`
+ - Translates duplex_mode via `_duplex_map`
+ - Converts color_mode/collate bools to `"true"` / `"false"` (lowercase, template adds `$` prefix)
+ - Calls `_env.get_template("install.ps1.j2").render(...)` with all variables
+ - Use plain string parameters (not ORM objects) so the function is testable without DB
+
+ 5. Create `imptune/templates/scripts/install.ps1.j2`:
+ Template structure (in this exact order):
+ ```
+ # Header comment: Generated by ImpTune, printer name, install command hint
+ # WOW64 Guard (FIRST executable block)
+ if ($env:PROCESSOR_ARCHITECTURE -eq "x86" -and $env:PROCESSOR_ARCHITEW6432) { ... relaunch 64-bit ... exit }
+ # SYSTEM/Admin check + UAC elevation
+ $id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
+ $isSystem = $id.IsSystem
+ $isAdmin = ... IsInRole(Administrator)
+ if (-not $isSystem -and -not $isAdmin) { Start-Process -Verb Runas ... exit }
+ # pnputil driver staging
+ pnputil.exe /add-driver "$PSScriptRoot\drivers\{{ inf_filename }}" /install
+ # Add-PrinterDriver
+ Add-PrinterDriver -Name "{{ driver_name }}"
+ # Idempotent port creation
+ if (-not (Get-PrinterPort -Name "{{ port_name }}" -ErrorAction SilentlyContinue)) { Add-PrinterPort ... }
+ # Idempotent printer creation
+ if (-not (Get-Printer -Name "{{ printer_name }}" -ErrorAction SilentlyContinue)) { Add-Printer ... }
+ # Set-PrintConfiguration
+ Set-PrintConfiguration -PrinterName "{{ printer_name }}" -DuplexingMode {{ duplex_mode }} -Color ${{ color }} -PaperSize {{ paper_size }} -Collate ${{ collate }}
+ ```
+
+ 6. Run tests — all MUST pass (GREEN)
+
+ **Important notes:**
+ - Use plain string args for render_install, NOT Printer ORM object — keeps tests DB-free
+ - The `_duplex_map` must translate BEFORE passing to template (template receives already-mapped value)
+ - Boolean values: pass as lowercase string `"true"` / `"false"` so template renders `$true` / `$false` with `${{ color }}`
+ - Template must use `{{ }}` for all variable interpolation — no `{% set %}` for simple values
+ - All PowerShell string parameters (printer_name, port_name, driver_name) must be double-quoted in the template
+
+
+
+
+
+
+```bash
+python -m pytest tests/test_script_generator.py -x -q
+```
+All 6+ tests pass. Rendered install script contains all required blocks in correct order.
+
+
+
+- render_install() produces complete PowerShell install script
+- WOW64 guard appears before any other logic
+- UAC self-elevation skips when SYSTEM
+- Duplex mode values correctly translated (LongEdge -> TwoSidedLongEdge)
+- All add operations wrapped in idempotency checks
+- All unit tests pass
+
+
+
diff --git a/.planning/phases/04-script-generation/04-02-PLAN.md b/.planning/phases/04-script-generation/04-02-PLAN.md
new file mode 100644
index 0000000..5356237
--- /dev/null
+++ b/.planning/phases/04-script-generation/04-02-PLAN.md
@@ -0,0 +1,247 @@
+---
+phase: 04-script-generation
+plan: 02
+type: execute
+wave: 2
+depends_on: ["04-01"]
+files_modified:
+ - imptune/generators/script_generator.py
+ - imptune/templates/scripts/uninstall.ps1.j2
+ - imptune/templates/scripts/detect.ps1.j2
+ - imptune/api/scripts.py
+ - imptune/main.py
+ - tests/test_script_generator.py
+autonomous: true
+requirements:
+ - SCRPT-02
+ - SCRPT-03
+
+must_haves:
+ truths:
+ - "render_uninstall() produces script with Remove-Printer, Remove-PrinterDriver, Remove-PrinterPort in correct order"
+ - "render_detect() produces script that exits 0 with Write-Output when printer found, exits 1 when absent"
+ - "GET /printers/{id}/scripts/install returns 200 with PowerShell content and attachment header"
+ - "GET /printers/{id}/scripts/uninstall returns 200 with PowerShell content"
+ - "GET /printers/{id}/scripts/detect returns 200 with PowerShell content"
+ - "GET /printers/{id}/scripts/{type} returns 404 for nonexistent printer"
+ - "GET /printers/{id}/scripts/{type} returns 422 when driver or inf_filename is missing"
+ artifacts:
+ - path: "imptune/templates/scripts/uninstall.ps1.j2"
+ provides: "PowerShell uninstall template"
+ contains: "Remove-Printer"
+ - path: "imptune/templates/scripts/detect.ps1.j2"
+ provides: "PowerShell detection template"
+ contains: "Write-Output"
+ - path: "imptune/api/scripts.py"
+ provides: "Script download endpoints"
+ exports: ["router"]
+ - path: "imptune/generators/script_generator.py"
+ provides: "render_uninstall and render_detect functions added"
+ exports: ["render_install", "render_uninstall", "render_detect"]
+ key_links:
+ - from: "imptune/api/scripts.py"
+ to: "imptune/generators/script_generator.py"
+ via: "import render_install, render_uninstall, render_detect"
+ pattern: "from imptune\\.generators\\.script_generator import"
+ - from: "imptune/api/scripts.py"
+ to: "imptune/db/models.py"
+ via: "Printer.get_or_none query with Driver join"
+ pattern: "Printer\\.get_or_none"
+ - from: "imptune/main.py"
+ to: "imptune/api/scripts.py"
+ via: "app.include_router(scripts.router)"
+ pattern: "include_router.*scripts"
+---
+
+
+Add uninstall and detection templates, then wire all three scripts to downloadable API endpoints.
+
+Purpose: Completes the script generation phase by adding the two simpler templates and exposing all scripts via GET endpoints that the printer detail page (Phase 3) can link to.
+
+Output: `uninstall.ps1.j2`, `detect.ps1.j2`, `scripts.py` router, updated `main.py`, passing integration tests.
+
+
+
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/04-script-generation/04-RESEARCH.md
+@.planning/phases/04-script-generation/04-01-SUMMARY.md
+
+
+
+
+From imptune/generators/script_generator.py (created in 04-01):
+```python
+# Jinja2 Environment already configured with FileSystemLoader for templates/scripts/
+# _duplex_map already defined
+def render_install(printer_name, ip_address, port_name, driver_name, inf_filename,
+ duplex_mode, color_mode, paper_size, collate) -> str: ...
+# Plan 02 adds: render_uninstall(), render_detect()
+```
+
+From imptune/db/models.py:
+```python
+class Printer(BaseModel):
+ name = CharField()
+ ip_address = CharField()
+ port_name = CharField()
+ driver = ForeignKeyField(Driver, null=True, backref="printers")
+ duplex_mode = CharField(default="OneSided")
+ color_mode = BooleanField(default=True)
+ paper_size = CharField(default="A4")
+ collate = BooleanField(default=True)
+
+class Driver(BaseModel):
+ driver_desc = CharField(null=True) # JSON list
+ inf_filename = CharField(null=True)
+```
+
+From imptune/main.py (router registration pattern):
+```python
+from imptune.api import clients, drivers, health, pages, printers
+app.include_router(health.router)
+app.include_router(pages.router)
+app.include_router(drivers.router)
+app.include_router(printers.router)
+app.include_router(clients.router)
+```
+
+From tests/conftest.py:
+```python
+@pytest.fixture
+def client(tmp_data_dir):
+ from imptune.main import app
+ with TestClient(app) as c:
+ yield c
+
+@pytest.fixture
+def tmp_data_dir(tmp_path, monkeypatch): ...
+```
+
+
+
+
+
+
+ Task 1: Uninstall and detection templates + render functions
+ imptune/generators/script_generator.py, imptune/templates/scripts/uninstall.ps1.j2, imptune/templates/scripts/detect.ps1.j2, tests/test_script_generator.py
+
+ - render_uninstall(printer_name, driver_name, port_name) returns PS script with Remove-Printer BEFORE Remove-PrinterDriver BEFORE Remove-PrinterPort, all with -ErrorAction SilentlyContinue
+ - render_detect(printer_name) returns PS script with Get-Printer check, Write-Output + exit 0 when found, exit 1 when absent
+
+
+ **Tests first (add to existing test_script_generator.py):**
+
+ - `test_render_uninstall`: call render_uninstall("Test Printer", "HP Driver", "IP_10.0.0.1"), assert output contains `Remove-Printer -Name "Test Printer"`, `Remove-PrinterDriver -Name "HP Driver"`, `Remove-PrinterPort -Name "IP_10.0.0.1"`, and `-ErrorAction SilentlyContinue` on all three. Assert Remove-Printer appears BEFORE Remove-PrinterDriver (order matters — driver removal fails if printer still references it).
+ - `test_render_detect`: call render_detect("Test Printer"), assert output contains `Get-Printer -Name "Test Printer"`, `Write-Output`, `exit 0`, `exit 1`.
+
+ Run tests — both MUST fail.
+
+ **Implement:**
+
+ Add `render_uninstall(printer_name, driver_name, port_name) -> str` to `script_generator.py`:
+ - Gets `uninstall.ps1.j2` template, renders with the three names.
+
+ Add `render_detect(printer_name) -> str` to `script_generator.py`:
+ - Gets `detect.ps1.j2` template, renders with printer_name.
+
+ Create `imptune/templates/scripts/uninstall.ps1.j2`:
+ ```
+ # Header: Generated by ImpTune — Uninstall script for {{ printer_name }}
+ Remove-Printer -Name "{{ printer_name }}" -ErrorAction SilentlyContinue
+ Remove-PrinterDriver -Name "{{ driver_name }}" -ErrorAction SilentlyContinue
+ Remove-PrinterPort -Name "{{ port_name }}" -ErrorAction SilentlyContinue
+ ```
+
+ Create `imptune/templates/scripts/detect.ps1.j2`:
+ ```
+ # Header: Generated by ImpTune — Detection script for {{ printer_name }}
+ $printer = Get-Printer -Name "{{ printer_name }}" -ErrorAction SilentlyContinue
+ if ($printer) {
+ Write-Output "Installed: {{ printer_name }}"
+ exit 0
+ } else {
+ exit 1
+ }
+ ```
+
+ Run tests — both MUST pass.
+
+
+ python -m pytest tests/test_script_generator.py::test_render_uninstall tests/test_script_generator.py::test_render_detect -x -q
+
+ render_uninstall and render_detect produce correct PowerShell scripts; removal order is correct; detection uses Write-Output + exit codes per Intune contract.
+
+
+
+ Task 2: Script download API endpoints and router registration
+ imptune/api/scripts.py, imptune/main.py, tests/test_script_generator.py
+
+ Create `imptune/api/scripts.py`:
+ - `router = APIRouter(prefix="/printers")`
+ - Three GET endpoints: `/{printer_id}/scripts/install`, `/{printer_id}/scripts/uninstall`, `/{printer_id}/scripts/detect`
+ - Each endpoint:
+ 1. `Printer.get_or_none(Printer.id == printer_id)` — return `PlainTextResponse("Printer not found", status_code=404)` if None
+ 2. Access `printer.driver` — return `PlainTextResponse("No driver assigned", status_code=422)` if driver is None
+ 3. Check `driver.inf_filename` — return `PlainTextResponse("Driver has no INF file", status_code=422)` if None/empty
+ 4. Parse `driver_name = json.loads(driver.driver_desc)[0]` — return 422 if driver_desc is empty/null
+ 5. Call the appropriate render function with plain values extracted from ORM objects
+ 6. Return `PlainTextResponse(content=rendered, headers={"Content-Disposition": 'attachment; filename="{type}.ps1"'})`
+ - For install endpoint: extract all printer fields + driver fields, call `render_install(printer.name, printer.ip_address, printer.port_name, driver_name, driver.inf_filename, printer.duplex_mode, printer.color_mode, printer.paper_size, printer.collate)`
+ - For uninstall: call `render_uninstall(printer.name, driver_name, printer.port_name)`
+ - For detect: call `render_detect(printer.name)`
+
+ Update `imptune/main.py`:
+ - Add `scripts` to import: `from imptune.api import clients, drivers, health, pages, printers, scripts`
+ - Add `app.include_router(scripts.router)` after existing router registrations
+
+ Add integration tests to `tests/test_script_generator.py`:
+ - `test_install_endpoint`: create Driver + Printer via ORM in test, GET `/printers/{id}/scripts/install`, assert 200 + content contains `pnputil`
+ - `test_uninstall_endpoint`: same setup, GET `/printers/{id}/scripts/uninstall`, assert 200 + `Remove-Printer`
+ - `test_detect_endpoint`: same setup, GET `/printers/{id}/scripts/detect`, assert 200 + `Write-Output`
+ - `test_script_endpoint_missing_printer`: GET `/printers/9999/scripts/install`, assert 404
+ - `test_script_endpoint_no_driver`: create Printer without driver FK, GET install, assert 422
+
+ For integration tests, use the `client` fixture from conftest.py. Create test data via ORM:
+ ```python
+ from imptune.db.models import Driver, Printer
+ driver = Driver.create(sha256="abc123", original_filename="test.zip", size_bytes=100,
+ driver_desc='["Test Driver"]', inf_filename="test.inf")
+ printer = Printer.create(name="Test Printer", ip_address="10.0.0.1", port_name="IP_10.0.0.1",
+ driver=driver, duplex_mode="LongEdge", color_mode=True,
+ paper_size="A4", collate=True)
+ ```
+
+
+ python -m pytest tests/test_script_generator.py -x -q
+
+ All script endpoints return 200 with correct PS content; 404 for missing printer; 422 for missing driver/INF; router registered in main.py; full test suite passes.
+
+
+
+
+
+```bash
+python -m pytest tests/ -x -q
+```
+Full test suite passes (existing + new script tests). No regressions.
+
+
+
+- render_uninstall produces script with correct removal order
+- render_detect follows Intune detection contract (Write-Output + exit 0/1)
+- All three script types downloadable via GET /printers/{id}/scripts/{type}
+- Error handling: 404 for missing printer, 422 for missing driver/INF
+- Scripts router registered in main.py
+- Full test suite green
+
+
+