lightman-agent 1.0.12 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,673 +1,529 @@
1
- # LIGHTMAN Agent - Complete Windows Installer
2
- # Uses NSSM for rock-solid Windows Service. Shell replacement for kiosk.
3
- # Cleans up any previous installation automatically before installing.
4
- #
5
- # Run as Administrator:
6
- # powershell -ExecutionPolicy Bypass -File install-windows.ps1 -Slug "F-AV01" -Server "http://192.168.10.100:3401"
7
- #
8
- # Shell Replacement mode (RECOMMENDED for kiosk machines):
9
- # powershell -ExecutionPolicy Bypass -File install-windows.ps1 -Slug "F-AV01" -Server "http://192.168.10.100:3401" -ShellReplace
10
- #Requires -RunAsAdministrator
11
-
12
- param(
13
- [Parameter(Mandatory=$true)] [string]$Slug,
14
- [Parameter(Mandatory=$true)] [string]$Server,
15
- [string]$Timezone = "Asia/Kolkata",
16
- [string]$Username = "",
17
- [switch]$ShellReplace = $false,
18
- [int]$PairingTimeoutSeconds = 900
19
- )
20
-
21
- $ErrorActionPreference = "Stop"
22
-
23
- $InstallDir = "C:\Program Files\Lightman\Agent"
24
- $LogDir = "C:\ProgramData\Lightman\logs"
25
- $ChromeData = "C:\ProgramData\Lightman\chrome-kiosk"
26
- $NssmDir = "C:\ProgramData\Lightman\nssm"
27
- $NssmExe = "$NssmDir\nssm.exe"
28
- $ServiceName = "LightmanAgent"
29
- $GuardianTask = "LIGHTMAN Guardian"
30
- $KioskTask = "LIGHTMAN Kiosk Browser"
31
- $AgentTask = "LIGHTMAN Agent"
32
- $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
33
- $AgentDir = Split-Path -Parent $ScriptDir
34
-
35
- if (-not $Username) { $Username = $env:USERNAME }
36
-
37
- function Get-ChromePath {
38
- $candidates = @(
39
- "C:\Program Files\Google\Chrome\Application\chrome.exe",
40
- "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
41
- (Join-Path $env:LOCALAPPDATA "Google\Chrome\Application\chrome.exe")
42
- )
43
-
44
- foreach ($candidate in $candidates) {
45
- if ($candidate -and (Test-Path $candidate)) {
46
- return $candidate
47
- }
48
- }
49
-
50
- return $null
51
- }
52
-
53
- function Install-ChromeFromMsi {
54
- $is64Bit = [Environment]::Is64BitOperatingSystem
55
- $downloadUrl = if ($is64Bit) {
56
- "https://dl.google.com/dl/chrome/install/googlechromestandaloneenterprise64.msi"
57
- } else {
58
- "https://dl.google.com/dl/chrome/install/googlechromestandaloneenterprise.msi"
59
- }
60
- $installerName = if ($is64Bit) { "googlechromestandaloneenterprise64.msi" } else { "googlechromestandaloneenterprise.msi" }
61
- $installerPath = Join-Path $env:TEMP $installerName
62
-
63
- Write-Host " Chrome not found. Downloading official Google Chrome MSI..." -ForegroundColor Yellow
64
- [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
65
- Invoke-WebRequest -Uri $downloadUrl -OutFile $installerPath -UseBasicParsing -TimeoutSec 180
66
-
67
- if (-not (Test-Path $installerPath)) {
68
- throw "Chrome MSI was not downloaded."
69
- }
70
-
71
- $msiArgs = "/i `"$installerPath`" /qn /norestart"
72
- Start-Process msiexec.exe -ArgumentList $msiArgs -Wait -NoNewWindow
73
- Remove-Item $installerPath -Force -ErrorAction SilentlyContinue
74
- }
75
-
76
- function Install-ChromeFromWinget {
77
- if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
78
- throw "winget is not available."
79
- }
80
-
81
- Write-Host " MSI install failed. Trying winget for Google Chrome..." -ForegroundColor Yellow
82
- & winget install --id Google.Chrome --exact --silent --accept-package-agreements --accept-source-agreements 2>&1 | Out-Host
83
- if ($LASTEXITCODE -ne 0) {
84
- throw "winget install failed with exit code $LASTEXITCODE."
85
- }
86
- }
87
-
88
- function Ensure-ChromeInstalled {
89
- $existingChrome = Get-ChromePath
90
- if ($existingChrome) {
91
- Write-Host " Found Chrome: $existingChrome" -ForegroundColor Green
92
- return $existingChrome
93
- }
94
-
95
- try {
96
- Install-ChromeFromMsi
97
- } catch {
98
- Write-Host " Chrome MSI install failed: $($_.Exception.Message)" -ForegroundColor DarkYellow
99
- Install-ChromeFromWinget
100
- }
101
-
102
- $installedChrome = Get-ChromePath
103
- if (-not $installedChrome) {
104
- throw "Chrome install did not produce chrome.exe."
105
- }
106
-
107
- Write-Host " Chrome installed: $installedChrome" -ForegroundColor Green
108
- return $installedChrome
109
- }
110
-
111
- Write-Host ""
112
- Write-Host "=============================================" -ForegroundColor Cyan
113
- Write-Host " LIGHTMAN Agent - Complete Windows Installer" -ForegroundColor Cyan
114
- Write-Host "=============================================" -ForegroundColor Cyan
115
- Write-Host " Device slug : $Slug"
116
- Write-Host " Server URL : $Server"
117
- Write-Host " Username : $Username"
118
- Write-Host " Mode : $(if ($ShellReplace) { 'Shell Replacement' } else { 'Standard' })"
119
- Write-Host ""
120
-
121
- # ============================================================
122
- # PHASE 0: NUKE EVERYTHING FROM PREVIOUS INSTALLS
123
- # ============================================================
124
- Write-Host "--- Phase 0: Cleaning previous installation ---" -ForegroundColor Cyan
125
- $ErrorActionPreference = "Continue"
126
-
127
- # Stop and remove NSSM service
128
- Write-Host "[0a] Removing old services..." -ForegroundColor Yellow
129
- if (Test-Path $NssmExe) {
130
- & $NssmExe stop $ServiceName 2>$null
131
- & $NssmExe remove $ServiceName confirm 2>$null
132
- }
133
- foreach ($sn in @($ServiceName, "lightmanagent.exe", "LightmanAgent.exe")) {
134
- sc.exe stop $sn 2>$null; sc.exe delete $sn 2>$null
135
- }
136
- $oldSvc = Get-Service -DisplayName "LIGHTMAN*" -ErrorAction SilentlyContinue
137
- if ($oldSvc) { Stop-Service -Name $oldSvc.Name -Force -ErrorAction SilentlyContinue; sc.exe delete $oldSvc.Name 2>$null }
138
-
139
- # Remove scheduled tasks (from previous task-scheduler-based installs)
140
- Write-Host "[0b] Removing old scheduled tasks..." -ForegroundColor Yellow
141
- foreach ($tn in @($AgentTask, $KioskTask, $GuardianTask)) {
142
- $t = Get-ScheduledTask -TaskName $tn -ErrorAction SilentlyContinue
143
- if ($t) { Stop-ScheduledTask -TaskName $tn -ErrorAction SilentlyContinue; Unregister-ScheduledTask -TaskName $tn -Confirm:$false -ErrorAction SilentlyContinue }
144
- }
145
-
146
- # Kill processes
147
- Write-Host "[0c] Killing node.exe and kiosk browser..." -ForegroundColor Yellow
148
- # IMPORTANT:
149
- # If install-windows.ps1 is launched from the npm CLI wrapper (node.exe),
150
- # killing all node.exe would terminate the installer mid-run.
151
- $parentPid = $null
152
- try {
153
- $parentPid = (Get-CimInstance Win32_Process -Filter "ProcessId=$PID" -ErrorAction SilentlyContinue).ParentProcessId
154
- } catch { }
155
- Get-Process -Name "node" -ErrorAction SilentlyContinue | ForEach-Object {
156
- if ($parentPid -and $_.Id -eq $parentPid) {
157
- Write-Host " Keeping installer parent node.exe (PID $($_.Id))" -ForegroundColor DarkGray
158
- } else {
159
- Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
160
- }
161
- }
162
- Get-Process -Name "chrome" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
163
- Get-Process -Name "msedge" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
164
- Start-Sleep -Seconds 2
165
-
166
- # Remove old files (keep NSSM and logs)
167
- Write-Host "[0d] Removing old agent files..." -ForegroundColor Yellow
168
- Remove-Item -Path $InstallDir -Recurse -Force -ErrorAction SilentlyContinue
169
- Remove-Item -Path "C:\ProgramData\Lightman\kiosk-url.txt" -Force -ErrorAction SilentlyContinue
170
-
171
- # Remove firewall rule
172
- Remove-NetFirewallRule -DisplayName "LIGHTMAN Agent WebSocket" -ErrorAction SilentlyContinue
173
-
174
- $ErrorActionPreference = "Stop"
175
- Start-Sleep -Seconds 2
176
- Write-Host " Clean slate" -ForegroundColor Green
177
- Write-Host ""
178
-
179
- # ============================================================
180
- # PART 1: BUILD & INSTALL
181
- # ============================================================
182
-
183
- # --- 1. Node.js ---
184
- Write-Host "[1/19] Checking Node.js..." -ForegroundColor Yellow
185
- try {
186
- $nodeVersion = (node -v) -replace 'v', ''
187
- if ([int]($nodeVersion.Split('.')[0]) -lt 20) { throw "old" }
188
- Write-Host " Found Node.js v$nodeVersion"
189
- } catch {
190
- Write-Host " Installing Node.js v20.18.0..." -ForegroundColor Yellow
191
- $installer = "$env:TEMP\node-setup.msi"
192
- Invoke-WebRequest -Uri "https://nodejs.org/dist/v20.18.0/node-v20.18.0-x64.msi" -OutFile $installer -UseBasicParsing
193
- Start-Process msiexec.exe -ArgumentList "/i `"$installer`" /qn /norestart" -Wait -NoNewWindow
194
- Remove-Item $installer -Force -ErrorAction SilentlyContinue
195
- $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
196
- if (-not (Get-Command node -ErrorAction SilentlyContinue)) { Write-Host " FATAL: Node.js install failed!" -ForegroundColor Red; exit 1 }
197
- Write-Host " Node.js installed" -ForegroundColor Green
198
- }
199
-
200
- # --- 2. Build ---
201
- Write-Host "[2/19] Building agent..." -ForegroundColor Yellow
202
- Push-Location $AgentDir
203
- $ErrorActionPreference = "Continue"
204
- & npm install 2>&1 | Out-Host
205
- & npm run build 2>&1 | Out-Host
206
- $ErrorActionPreference = "Stop"
207
- if (-not (Test-Path "$AgentDir\dist\index.js")) { Write-Host " FATAL: Build failed!" -ForegroundColor Red; exit 1 }
208
- Pop-Location
209
- Write-Host " Build successful"
210
-
211
- # --- 3. Directories ---
212
- Write-Host "[3/19] Creating directories..." -ForegroundColor Yellow
213
- foreach ($d in @($InstallDir, $LogDir, $ChromeData, $NssmDir)) { New-Item -ItemType Directory -Force -Path $d | Out-Null }
214
-
215
- # --- 4. Copy files ---
216
- Write-Host "[4/19] Copying agent files..." -ForegroundColor Yellow
217
- Copy-Item "$AgentDir\dist" "$InstallDir\dist" -Recurse -Force
218
- Copy-Item "$AgentDir\package.json" "$InstallDir\package.json" -Force
219
- if (Test-Path "$AgentDir\package-lock.json") { Copy-Item "$AgentDir\package-lock.json" "$InstallDir\package-lock.json" -Force }
220
- Copy-Item "$AgentDir\agent.config.template.json" "$InstallDir\agent.config.template.json" -Force
221
- if (Test-Path "$AgentDir\public") { Copy-Item "$AgentDir\public" "$InstallDir\public" -Recurse -Force }
222
- if (Test-Path "$AgentDir\scripts") { Copy-Item "$AgentDir\scripts" "$InstallDir\scripts" -Recurse -Force }
223
-
224
- # --- 5. Install deps ---
225
- Write-Host "[5/19] Installing dependencies..." -ForegroundColor Yellow
226
- Push-Location $InstallDir
227
- $ErrorActionPreference = "Continue"
228
- & npm ci --omit=dev --ignore-scripts 2>&1 | Out-Host
229
- if ($LASTEXITCODE -ne 0) { & npm install --omit=dev --ignore-scripts 2>&1 | Out-Host }
230
- $ErrorActionPreference = "Stop"
231
- Pop-Location
232
-
233
- # --- 5b. Chrome ---
234
- Write-Host "[5b/19] Ensuring Google Chrome is installed..." -ForegroundColor Yellow
235
- try {
236
- $chromePath = Ensure-ChromeInstalled
237
- } catch {
238
- Write-Host " FATAL: $($_.Exception.Message)" -ForegroundColor Red
239
- Write-Host " Install Google Chrome manually, then re-run the installer." -ForegroundColor Yellow
240
- exit 1
241
- }
242
-
243
- # --- 6. Generate config ---
244
- Write-Host "[6/19] Generating config..." -ForegroundColor Yellow
245
- if ($ShellReplace) {
246
- & "$ScriptDir\setup.ps1" -Slug $Slug -Server $Server -Timezone $Timezone -InstallDir $InstallDir -ShellMode
247
- } else {
248
- & "$ScriptDir\setup.ps1" -Slug $Slug -Server $Server -Timezone $Timezone -InstallDir $InstallDir
249
- }
250
-
251
- # --- 7. Fix BOM ---
252
- Write-Host "[7/19] Fixing config encoding..." -ForegroundColor Yellow
253
- $configPath = Join-Path $InstallDir "agent.config.json"
254
- if (-not (Test-Path $configPath)) { Write-Host " FATAL: config not created!" -ForegroundColor Red; exit 1 }
255
- $raw = [System.IO.File]::ReadAllText($configPath)
256
- [System.IO.File]::WriteAllText($configPath, $raw.TrimStart([char]0xFEFF), [System.Text.UTF8Encoding]::new($false))
257
-
258
- # --- 8. Verify config ---
259
- Write-Host "[8/19] Verifying config..." -ForegroundColor Yellow
260
- Push-Location $InstallDir
261
- $ErrorActionPreference = "Continue"
262
- $jsonCheck = & node -e "try{const c=JSON.parse(require('fs').readFileSync('agent.config.json','utf8'));console.log('OK slug='+c.deviceSlug+' shellMode='+(c.kiosk&&c.kiosk.shellMode||false))}catch(e){console.log('FAIL: '+e.message);process.exit(1)}" 2>&1
263
- $ErrorActionPreference = "Stop"
264
- if ($LASTEXITCODE -ne 0) { Write-Host " FATAL: invalid config: $jsonCheck" -ForegroundColor Red; Pop-Location; exit 1 }
265
- Pop-Location
266
- Write-Host " $jsonCheck"
267
-
268
- # --- 9. Download NSSM ---
269
- Write-Host "[9/19] Setting up NSSM..." -ForegroundColor Yellow
270
- if (-not (Test-Path $NssmExe)) {
271
- # Check bundled copy first (fastest, no internet needed)
272
- $bundled = Join-Path $AgentDir "nssm\nssm.exe"
273
- if (Test-Path $bundled) {
274
- Copy-Item $bundled $NssmExe -Force
275
- Write-Host " Using bundled NSSM"
276
- } else {
277
- # Download from multiple sources
278
- $nssmZip = "$env:TEMP\nssm.zip"
279
- $downloaded = $false
280
- $urls = @(
281
- "https://nssm.cc/release/nssm-2.24.zip",
282
- "https://nssm.cc/ci/nssm-2.24-101-g897c7ad.zip"
283
- )
284
- foreach ($url in $urls) {
285
- if ($downloaded) { break }
286
- Write-Host " Downloading from $url ..."
287
- try {
288
- [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
289
- Invoke-WebRequest -Uri $url -OutFile $nssmZip -UseBasicParsing -TimeoutSec 60
290
- if ((Test-Path $nssmZip) -and (Get-Item $nssmZip).Length -gt 10000) {
291
- $downloaded = $true
292
- }
293
- } catch {
294
- Write-Host " Failed: $_" -ForegroundColor DarkYellow
295
- }
296
- }
297
- if ($downloaded) {
298
- Expand-Archive -Path $nssmZip -DestinationPath "$env:TEMP\nssm-extract" -Force
299
- # Find nssm.exe in extracted folder (handles different zip structures)
300
- $found = Get-ChildItem "$env:TEMP\nssm-extract" -Recurse -Filter "nssm.exe" | Where-Object { $_.DirectoryName -like "*win64*" } | Select-Object -First 1
301
- if (-not $found) { $found = Get-ChildItem "$env:TEMP\nssm-extract" -Recurse -Filter "nssm.exe" | Select-Object -First 1 }
302
- if ($found) { Copy-Item $found.FullName $NssmExe -Force }
303
- Remove-Item $nssmZip -Force -ErrorAction SilentlyContinue
304
- Remove-Item "$env:TEMP\nssm-extract" -Recurse -Force -ErrorAction SilentlyContinue
305
- }
306
- }
307
- }
308
- if (-not (Test-Path $NssmExe)) {
309
- Write-Host ""
310
- Write-Host " NSSM download failed. Manual fix:" -ForegroundColor Red
311
- Write-Host " 1. Download nssm-2.24.zip from https://nssm.cc/release/nssm-2.24.zip" -ForegroundColor Yellow
312
- Write-Host " 2. Extract win64\nssm.exe to: $NssmExe" -ForegroundColor Yellow
313
- Write-Host " 3. Re-run this script" -ForegroundColor Yellow
314
- Write-Host ""
315
- Write-Host " OR bundle it in the repo:" -ForegroundColor Yellow
316
- Write-Host " Copy nssm.exe to: $AgentDir\nssm\nssm.exe" -ForegroundColor Yellow
317
- exit 1
318
- }
319
- Write-Host " NSSM ready: $NssmExe"
320
-
321
- # --- 10. Install Windows Service via NSSM ---
322
- Write-Host "[10/19] Installing Windows Service..." -ForegroundColor Yellow
323
-
324
- # Clean slate
325
- $ErrorActionPreference = "Continue"
326
- & $NssmExe stop $ServiceName 2>$null
327
- & $NssmExe remove $ServiceName confirm 2>$null
328
- sc.exe delete $ServiceName 2>$null
329
- Start-Sleep -Seconds 2
330
- $ErrorActionPreference = "Stop"
331
-
332
- $nodePath = (Get-Command node).Source
333
-
334
- # Install
335
- & $NssmExe install $ServiceName $nodePath "dist\index.js"
336
- if ($LASTEXITCODE -ne 0) { Write-Host " FATAL: NSSM install failed!" -ForegroundColor Red; exit 1 }
337
-
338
- # Configure
339
- & $NssmExe set $ServiceName AppDirectory $InstallDir
340
- & $NssmExe set $ServiceName DisplayName "LIGHTMAN Agent"
341
- & $NssmExe set $ServiceName Description "LIGHTMAN kiosk agent - display management and monitoring"
342
- & $NssmExe set $ServiceName Start SERVICE_AUTO_START
343
- & $NssmExe set $ServiceName AppStdout "$LogDir\service-stdout.log"
344
- & $NssmExe set $ServiceName AppStderr "$LogDir\service-stderr.log"
345
- & $NssmExe set $ServiceName AppStdoutCreationDisposition 4
346
- & $NssmExe set $ServiceName AppStderrCreationDisposition 4
347
- & $NssmExe set $ServiceName AppRotateFiles 1
348
- & $NssmExe set $ServiceName AppRotateBytes 5242880
349
- & $NssmExe set $ServiceName AppRestartDelay 10000
350
- & $NssmExe set $ServiceName AppExit Default Restart
351
-
352
- # Verify service was created
353
- Start-Sleep -Seconds 2
354
- $svcCheck = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
355
- if (-not $svcCheck) {
356
- $svcCheck = Get-Service -DisplayName "LIGHTMAN*" -ErrorAction SilentlyContinue | Select-Object -First 1
357
- }
358
- if (-not $svcCheck) {
359
- Write-Host " FATAL: Service was not created!" -ForegroundColor Red
360
- exit 1
361
- }
362
- Write-Host " Service installed: $($svcCheck.Name)" -ForegroundColor Green
363
-
364
- # Recovery policy
365
- sc.exe failure $svcCheck.Name reset= 86400 actions= restart/5000/restart/10000/restart/30000 2>$null
366
-
367
- # --- 11. Start service ---
368
- Write-Host "[11/19] Starting service..." -ForegroundColor Yellow
369
- Start-Service -Name $svcCheck.Name -ErrorAction SilentlyContinue
370
- Start-Sleep -Seconds 5
371
- $svcCheck.Refresh()
372
-
373
- if ($svcCheck.Status -eq 'Running') {
374
- Write-Host " Service is RUNNING" -ForegroundColor Green
375
- } else {
376
- Write-Host " Service status: $($svcCheck.Status) - check $LogDir" -ForegroundColor Yellow
377
- Start-Sleep -Seconds 3
378
- Start-Service -Name $svcCheck.Name -ErrorAction SilentlyContinue
379
- }
380
-
381
- # Wait for port 3403
382
- $portUp = $false
383
- for ($i = 0; $i -lt 10; $i++) {
384
- $ErrorActionPreference = "Continue"
385
- $n = netstat -an 2>$null | findstr ":3403.*LISTENING" 2>$null
386
- $ErrorActionPreference = "Stop"
387
- if ($n) { $portUp = $true; break }
388
- Start-Sleep -Seconds 2
389
- }
390
- if ($portUp) { Write-Host " Port 3403 LISTENING" -ForegroundColor Green }
391
- else { Write-Host " Port 3403 not yet up (may take a moment)" -ForegroundColor Yellow }
392
-
393
- # Wait until provisioning is complete (auto-provision or manual pairing)
394
- Write-Host "[11b/19] Waiting for device provisioning/pairing..." -ForegroundColor Yellow
395
- $identityPath = Join-Path $InstallDir ".lightman-identity.json"
396
- $deadline = if ($PairingTimeoutSeconds -gt 0) { (Get-Date).AddSeconds($PairingTimeoutSeconds) } else { $null }
397
- $paired = $false
398
- $lastHint = ""
399
-
400
- while (-not $paired) {
401
- if (Test-Path $identityPath) {
402
- try {
403
- $identity = Get-Content $identityPath -Raw | ConvertFrom-Json
404
- if ($identity.deviceId -and $identity.apiKey) {
405
- $paired = $true
406
- break
407
- }
408
- } catch {
409
- # File may be mid-write, retry
410
- }
411
- }
412
-
413
- $logPath = Join-Path $LogDir "service-stdout.log"
414
- if (Test-Path $logPath) {
415
- try {
416
- $hint = Get-Content $logPath -Tail 40 | Where-Object {
417
- $_ -match "Pairing required|Waiting for admin to approve pairing|Auto-provisioned|Pairing complete"
418
- } | Select-Object -Last 1
419
- if ($hint -and $hint -ne $lastHint) {
420
- Write-Host " Agent: $hint" -ForegroundColor DarkGray
421
- $lastHint = $hint
422
- }
423
- } catch { }
424
- }
425
-
426
- if ($deadline -and (Get-Date) -ge $deadline) {
427
- Write-Host " FATAL: Pairing timed out after $PairingTimeoutSeconds seconds." -ForegroundColor Red
428
- Write-Host " Check server pairing UI, then re-run installer (or increase -PairingTimeoutSeconds)." -ForegroundColor Yellow
429
- exit 1
430
- }
431
-
432
- Start-Sleep -Seconds 5
433
- }
434
- Write-Host " Provisioning/pairing complete" -ForegroundColor Green
435
-
436
- # --- 12. Firewall ---
437
- Write-Host "[12/19] Configuring firewall..." -ForegroundColor Yellow
438
- $ErrorActionPreference = "Continue"
439
- if (-not (Get-NetFirewallRule -DisplayName "LIGHTMAN Agent WebSocket" -ErrorAction SilentlyContinue)) {
440
- New-NetFirewallRule -DisplayName "LIGHTMAN Agent WebSocket" -Direction Outbound -Action Allow -Protocol TCP -RemotePort 3001 -Description "LIGHTMAN Agent" | Out-Null
441
- Write-Host " Created"
442
- } else { Write-Host " Already exists" }
443
-
444
- # ============================================================
445
- # PART 2: KIOSK CONFIGURATION
446
- # ============================================================
447
- $ErrorActionPreference = "Continue"
448
- Write-Host ""
449
- Write-Host "--- Configuring Kiosk Mode ---" -ForegroundColor Cyan
450
- Write-Host ""
451
-
452
- # --- 13. Auto-login ---
453
- Write-Host "[13/19] Enabling auto-login..." -ForegroundColor Yellow
454
- $RegPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
455
- $targetUser = Get-LocalUser -Name $Username -ErrorAction SilentlyContinue
456
- $isMsAccount = $targetUser -and $targetUser.PrincipalSource -eq 'MicrosoftAccount'
457
-
458
- if ($isMsAccount) {
459
- $KioskUser = "kiosk"
460
- $existingKiosk = Get-LocalUser -Name $KioskUser -ErrorAction SilentlyContinue
461
- if (-not $existingKiosk) { net user $KioskUser "" /add 2>$null; net localgroup Administrators $KioskUser /add 2>$null }
462
- else { net user $KioskUser "" 2>$null }
463
- $HidePath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList"
464
- if (-not (Test-Path $HidePath)) { New-Item -Path $HidePath -Force | Out-Null }
465
- Set-ItemProperty -Path $HidePath -Name $Username -Value 0
466
- $Username = $KioskUser
467
- Write-Host " Created kiosk account, auto-login: $Username" -ForegroundColor Green
468
- } else {
469
- net user $Username "" 2>$null
470
- }
471
-
472
- $PwdLess = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\PasswordLess\Device"
473
- if (Test-Path $PwdLess) { Set-ItemProperty -Path $PwdLess -Name "DevicePasswordLessBuildVersion" -Value 0 }
474
-
475
- $Passport = "HKLM:\SOFTWARE\Policies\Microsoft\PassportForWork"
476
- if (-not (Test-Path $Passport)) { New-Item -Path $Passport -Force | Out-Null }
477
- Set-ItemProperty -Path $Passport -Name "Enabled" -Value 0
478
-
479
- Set-ItemProperty -Path $RegPath -Name "AutoAdminLogon" -Value "1"
480
- Set-ItemProperty -Path $RegPath -Name "DefaultUserName" -Value $Username
481
- Set-ItemProperty -Path $RegPath -Name "DefaultPassword" -Value ""
482
- Set-ItemProperty -Path $RegPath -Name "DefaultDomainName" -Value ""
483
- Set-ItemProperty -Path $RegPath -Name "DisableCAD" -Value 1
484
- Set-ItemProperty -Path $RegPath -Name "AutoRestartShell" -Value 1
485
- Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "DisableAutomaticRestartSignOn" -Value 0
486
- $OOBE = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\OOBE"
487
- if (-not (Test-Path $OOBE)) { New-Item -Path $OOBE -Force | Out-Null }
488
- Set-ItemProperty -Path $OOBE -Name "DisablePrivacyExperience" -Value 1
489
- Write-Host " Auto-login enabled for: $Username"
490
-
491
- # --- 14. Lock screen ---
492
- Write-Host "[14/19] Removing lock screen..." -ForegroundColor Yellow
493
- $LP = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization"
494
- if (-not (Test-Path $LP)) { New-Item -Path $LP -Force | Out-Null }
495
- Set-ItemProperty -Path $LP -Name "NoLockScreen" -Value 1
496
-
497
- $SD = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI\SessionData"
498
- if (Test-Path $SD) { Set-ItemProperty -Path $SD -Name "AllowLockScreen" -Value 0 -ErrorAction SilentlyContinue }
499
-
500
- $CC = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent"
501
- if (-not (Test-Path $CC)) { New-Item -Path $CC -Force | Out-Null }
502
- Set-ItemProperty -Path $CC -Name "DisableWindowsConsumerFeatures" -Value 1
503
- Set-ItemProperty -Path $CC -Name "DisableCloudOptimizedContent" -Value 1
504
- $CCU = "HKCU:\SOFTWARE\Policies\Microsoft\Windows\CloudContent"
505
- if (-not (Test-Path $CCU)) { New-Item -Path $CCU -Force | Out-Null }
506
- Set-ItemProperty -Path $CCU -Name "DisableWindowsSpotlightFeatures" -Value 1
507
- Set-ItemProperty -Path $CCU -Name "DisableTailoredExperiencesWithDiagnosticData" -Value 1
508
-
509
- Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "EnableFirstLogonAnimation" -Value 0
510
- $SP = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"
511
- Set-ItemProperty -Path $SP -Name "DisableLockWorkstation" -Value 1
512
- Set-ItemProperty -Path $SP -Name "HideFastUserSwitching" -Value 1
513
-
514
- $DL = "HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
515
- if (-not (Test-Path $DL)) { New-Item -Path $DL -Force | Out-Null }
516
- Set-ItemProperty -Path $DL -Name "EnableGoodbye" -Value 0
517
-
518
- $PS = "HKLM:\SOFTWARE\Policies\Microsoft\Power\PowerSettings\0e796bdb-100d-47d6-a2d5-f7d2daa51f51"
519
- if (-not (Test-Path $PS)) { New-Item -Path $PS -Force | Out-Null }
520
- Set-ItemProperty -Path $PS -Name "ACSettingIndex" -Value 0
521
- Set-ItemProperty -Path $PS -Name "DCSettingIndex" -Value 0
522
- powercfg /SETACVALUEINDEX SCHEME_CURRENT SUB_NONE CONSOLELOCK 0 2>&1 | Out-Null
523
- powercfg /SETDCVALUEINDEX SCHEME_CURRENT SUB_NONE CONSOLELOCK 0 2>&1 | Out-Null
524
- powercfg /SETACTIVE SCHEME_CURRENT 2>&1 | Out-Null
525
-
526
- Set-ItemProperty -Path "HKCU:\Control Panel\Desktop" -Name "ScreenSaverIsSecure" -Value "0"
527
- Set-ItemProperty -Path "HKCU:\Control Panel\Desktop" -Name "ScreenSaveActive" -Value "0"
528
- Set-ItemProperty -Path $SP -Name "InactivityTimeoutSecs" -Value 0 -ErrorAction SilentlyContinue
529
- try { Disable-ScheduledTask -TaskName "\Microsoft\Windows\Shell\CreateObjectTask" -ErrorAction SilentlyContinue | Out-Null } catch { }
530
- Write-Host " Lock screen fully disabled"
531
-
532
- # --- 15. Sleep ---
533
- Write-Host "[15/19] Disabling sleep..." -ForegroundColor Yellow
534
- powercfg /change monitor-timeout-ac 0 2>&1 | Out-Null
535
- powercfg /change standby-timeout-ac 0 2>&1 | Out-Null
536
- powercfg /change hibernate-timeout-ac 0 2>&1 | Out-Null
537
-
538
- # --- 16. Harden ---
539
- Write-Host "[16/19] Hardening Windows..." -ForegroundColor Yellow
540
- $WU = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU"
541
- if (-not (Test-Path $WU)) { New-Item -Path $WU -Force | Out-Null }
542
- Set-ItemProperty -Path $WU -Name "NoAutoRebootWithLoggedOnUsers" -Value 1
543
- Set-ItemProperty -Path $WU -Name "AUOptions" -Value 2
544
- $WUM = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"
545
- if (-not (Test-Path $WUM)) { New-Item -Path $WUM -Force | Out-Null }
546
- Set-ItemProperty -Path $WUM -Name "SetAutoRestartNotificationDisable" -Value 1
547
- Set-ItemProperty -Path $WUM -Name "SetActiveHours" -Value 1
548
- Set-ItemProperty -Path $WUM -Name "ActiveHoursStart" -Value 0
549
- Set-ItemProperty -Path $WUM -Name "ActiveHoursEnd" -Value 23
550
-
551
- $NP = "HKCU:\SOFTWARE\Policies\Microsoft\Windows\Explorer"
552
- if (-not (Test-Path $NP)) { New-Item -Path $NP -Force | Out-Null }
553
- Set-ItemProperty -Path $NP -Name "DisableNotificationCenter" -Value 1
554
- $TP = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\PushNotifications"
555
- if (-not (Test-Path $TP)) { New-Item -Path $TP -Force | Out-Null }
556
- Set-ItemProperty -Path $TP -Name "ToastEnabled" -Value 0
557
-
558
- $WER = "HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting"
559
- if (-not (Test-Path $WER)) { New-Item -Path $WER -Force | Out-Null }
560
- Set-ItemProperty -Path $WER -Name "DontShowUI" -Value 1
561
- Set-ItemProperty -Path $WER -Name "Disabled" -Value 1
562
- Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Windows" -Name "ErrorMode" -Value 2 -ErrorAction SilentlyContinue
563
-
564
- $SR = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search"
565
- if (-not (Test-Path $SR)) { New-Item -Path $SR -Force | Out-Null }
566
- Set-ItemProperty -Path $SR -Name "AllowCortana" -Value 0
567
- Write-Host " Done"
568
-
569
- # --- 17. Kiosk Chrome ---
570
- if ($ShellReplace) {
571
- Write-Host "[17/19] SHELL REPLACEMENT..." -ForegroundColor Magenta
572
-
573
- # Copy shell BAT (reads slug from agent.config.json - single source of truth)
574
- $shellSource = Join-Path $ScriptDir "lightman-shell.bat"
575
- $shellTarget = Join-Path $InstallDir "lightman-shell.bat"
576
- if (Test-Path $shellSource) { Copy-Item $shellSource $shellTarget -Force }
577
-
578
- # No sidecar file needed - shell BAT reads directly from agent.config.json
579
-
580
- # Replace shell
581
- $ShellReg = "HKCU:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon"
582
- if (-not (Test-Path $ShellReg)) { New-Item -Path $ShellReg -Force | Out-Null }
583
- Set-ItemProperty -Path $ShellReg -Name "Shell" -Value """$shellTarget"""
584
- $HKLMShell = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
585
- $orig = (Get-ItemProperty -Path $HKLMShell -Name "Shell" -ErrorAction SilentlyContinue).Shell
586
- if ($orig -and $orig -notlike "*lightman*") { Set-ItemProperty -Path $HKLMShell -Name "Shell_Original" -Value $orig }
587
- Set-ItemProperty -Path $HKLMShell -Name "Shell" -Value """$shellTarget"""
588
-
589
- Write-Host " Shell replaced -> lightman-shell.bat" -ForegroundColor Green
590
- Write-Host " Recovery: scripts\restore-desktop.ps1" -ForegroundColor Yellow
591
-
592
- # Remove kiosk task if exists
593
- $kt = Get-ScheduledTask -TaskName $KioskTask -ErrorAction SilentlyContinue
594
- if ($kt) { Unregister-ScheduledTask -TaskName $KioskTask -Confirm:$false }
595
- } else {
596
- Write-Host "[17/19] Standard mode - kiosk browser task..." -ForegroundColor Yellow
597
- $vbs = Join-Path $ScriptDir "launch-kiosk.vbs"
598
- $vbsT = Join-Path $InstallDir "launch-kiosk.vbs"
599
- if (Test-Path $vbs) { Copy-Item $vbs $vbsT -Force }
600
- $kt = Get-ScheduledTask -TaskName $KioskTask -ErrorAction SilentlyContinue
601
- if ($kt) { Unregister-ScheduledTask -TaskName $KioskTask -Confirm:$false }
602
- $kA = New-ScheduledTaskAction -Execute "wscript.exe" -Argument """$vbsT""" -WorkingDirectory $InstallDir
603
- $kT1 = New-ScheduledTaskTrigger -AtLogOn -User $Username
604
- $kT2 = New-ScheduledTaskTrigger -AtStartup
605
- $kS = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1)
606
- Register-ScheduledTask -TaskName $KioskTask -Action $kA -Trigger @($kT1,$kT2) -Settings $kS -RunLevel Highest -Description "Chrome kiosk at logon/startup" -Force | Out-Null
607
- Write-Host " Kiosk browser task registered"
608
- }
609
-
610
- # --- 18. Guardian ---
611
- Write-Host "[18/19] Registering Guardian..." -ForegroundColor Yellow
612
- $gSrc = Join-Path $ScriptDir "guardian.ps1"
613
- $gDst = Join-Path $InstallDir "guardian.ps1"
614
- if (Test-Path $gSrc) { Copy-Item $gSrc $gDst -Force }
615
- $gt = Get-ScheduledTask -TaskName $GuardianTask -ErrorAction SilentlyContinue
616
- if ($gt) { Unregister-ScheduledTask -TaskName $GuardianTask -Confirm:$false }
617
- $gA = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -WindowStyle Hidden -File ""$gDst""" -WorkingDirectory $InstallDir
618
- $gT = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 5) -RepetitionDuration (New-TimeSpan -Days 365)
619
- $gS = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 2)
620
- $gP = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
621
- Register-ScheduledTask -TaskName $GuardianTask -Action $gA -Trigger $gT -Settings $gS -Principal $gP -Description "LIGHTMAN health check every 5 min" -Force | Out-Null
622
-
623
- foreach ($task in @("\Microsoft\Windows\UpdateOrchestrator\Reboot","\Microsoft\Windows\UpdateOrchestrator\Schedule Retry Scan","\Microsoft\Windows\WindowsUpdate\Scheduled Start")) {
624
- try { Disable-ScheduledTask -TaskName $task -ErrorAction SilentlyContinue | Out-Null } catch { }
625
- }
626
- Write-Host " Guardian registered"
627
-
628
- # --- 19. Final verification ---
629
- Write-Host "[19/19] Verification..." -ForegroundColor Yellow
630
- Start-Sleep -Seconds 3
631
-
632
- $finalSvc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
633
- if (-not $finalSvc) { $finalSvc = Get-Service -DisplayName "LIGHTMAN*" -ErrorAction SilentlyContinue | Select-Object -First 1 }
634
- $svcStatus = if ($finalSvc) { "$($finalSvc.Status)" } else { "NOT FOUND" }
635
-
636
- $cfgOk = $false
637
- try {
638
- Push-Location $InstallDir
639
- $cfgResult = & node -e "const c=JSON.parse(require('fs').readFileSync('agent.config.json','utf8'));console.log(JSON.stringify({slug:c.deviceSlug,shell:c.kiosk&&c.kiosk.shellMode||false,browser:c.kiosk&&c.kiosk.browserPath||''}))" 2>&1
640
- $cfgData = $cfgResult | ConvertFrom-Json
641
- Pop-Location
642
- $cfgOk = $true
643
- } catch { Pop-Location }
644
-
645
- Write-Host ""
646
- Write-Host "=============================================" -ForegroundColor Green
647
- Write-Host " INSTALLATION COMPLETE" -ForegroundColor Green
648
- Write-Host "=============================================" -ForegroundColor Green
649
- Write-Host ""
650
- Write-Host " Slug : $Slug"
651
- Write-Host " Server : $Server"
652
- Write-Host " Install : $InstallDir"
653
- Write-Host " Logs : $LogDir"
654
- Write-Host " User : $Username"
655
- Write-Host ""
656
- Write-Host " Service : $svcStatus" -ForegroundColor $(if ($svcStatus -eq 'Running') { 'Green' } else { 'Red' })
657
- if ($cfgOk) {
658
- Write-Host " Config slug: $($cfgData.slug)" -ForegroundColor $(if ($cfgData.slug -eq $Slug) { 'Green' } else { 'Red' })
659
- Write-Host " Shell mode : $($cfgData.shell)" -ForegroundColor $(if ($cfgData.shell -eq $ShellReplace.IsPresent) { 'Green' } else { 'Red' })
660
- Write-Host " Browser : $($cfgData.browser)" -ForegroundColor Green
661
- }
662
- Write-Host ""
663
- Write-Host " Manage:" -ForegroundColor DarkGray
664
- Write-Host " $NssmExe stop $ServiceName" -ForegroundColor DarkGray
665
- Write-Host " $NssmExe start $ServiceName" -ForegroundColor DarkGray
666
- Write-Host " $NssmExe restart $ServiceName" -ForegroundColor DarkGray
667
- Write-Host ""
668
- Write-Host " BIOS (manual):" -ForegroundColor Red
669
- Write-Host " After Power Loss = Power On" -ForegroundColor Red
670
- Write-Host " Wake-on-LAN = Enabled" -ForegroundColor Red
671
- Write-Host ""
672
- Write-Host " REBOOT NOW: Restart-Computer" -ForegroundColor Yellow
673
- Write-Host ""
1
+ # LIGHTMAN Agent - Complete Windows Installer
2
+ # Uses NSSM for rock-solid Windows Service. Shell replacement for kiosk.
3
+ # Cleans up any previous installation automatically before installing.
4
+ #
5
+ # Run as Administrator:
6
+ # powershell -ExecutionPolicy Bypass -File install-windows.ps1 -Slug "F-AV01" -Server "http://192.168.1.180:3401"
7
+ #
8
+ # Shell Replacement mode (RECOMMENDED for kiosk machines):
9
+ # powershell -ExecutionPolicy Bypass -File install-windows.ps1 -Slug "F-AV01" -Server "http://..." -ShellReplace
10
+ #Requires -RunAsAdministrator
11
+
12
+ param(
13
+ [Parameter(Mandatory=$true)] [string]$Slug,
14
+ [Parameter(Mandatory=$true)] [string]$Server,
15
+ [string]$Timezone = "Asia/Kolkata",
16
+ [string]$Username = "",
17
+ [switch]$ShellReplace = $false
18
+ )
19
+
20
+ $ErrorActionPreference = "Stop"
21
+
22
+ $InstallDir = "C:\Program Files\Lightman\Agent"
23
+ $LogDir = "C:\ProgramData\Lightman\logs"
24
+ $ChromeData = "C:\ProgramData\Lightman\chrome-kiosk"
25
+ $NssmDir = "C:\ProgramData\Lightman\nssm"
26
+ $NssmExe = "$NssmDir\nssm.exe"
27
+ $ServiceName = "LightmanAgent"
28
+ $GuardianTask = "LIGHTMAN Guardian"
29
+ $KioskTask = "LIGHTMAN Kiosk Browser"
30
+ $AgentTask = "LIGHTMAN Agent"
31
+ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
32
+ $AgentDir = Split-Path -Parent $ScriptDir
33
+
34
+ if (-not $Username) { $Username = $env:USERNAME }
35
+
36
+ Write-Host ""
37
+ Write-Host "=============================================" -ForegroundColor Cyan
38
+ Write-Host " LIGHTMAN Agent - Complete Windows Installer" -ForegroundColor Cyan
39
+ Write-Host "=============================================" -ForegroundColor Cyan
40
+ Write-Host " Device slug : $Slug"
41
+ Write-Host " Server URL : $Server"
42
+ Write-Host " Username : $Username"
43
+ Write-Host " Mode : $(if ($ShellReplace) { 'Shell Replacement' } else { 'Standard' })"
44
+ Write-Host ""
45
+
46
+ # ============================================================
47
+ # PHASE 0: NUKE EVERYTHING FROM PREVIOUS INSTALLS
48
+ # ============================================================
49
+ Write-Host "--- Phase 0: Cleaning previous installation ---" -ForegroundColor Cyan
50
+ $ErrorActionPreference = "Continue"
51
+
52
+ # Stop and remove NSSM service
53
+ Write-Host "[0a] Removing old services..." -ForegroundColor Yellow
54
+ if (Test-Path $NssmExe) {
55
+ & $NssmExe stop $ServiceName 2>$null
56
+ & $NssmExe remove $ServiceName confirm 2>$null
57
+ }
58
+ foreach ($sn in @($ServiceName, "lightmanagent.exe", "LightmanAgent.exe")) {
59
+ sc.exe stop $sn 2>$null; sc.exe delete $sn 2>$null
60
+ }
61
+ $oldSvc = Get-Service -DisplayName "LIGHTMAN*" -ErrorAction SilentlyContinue
62
+ if ($oldSvc) { Stop-Service -Name $oldSvc.Name -Force -ErrorAction SilentlyContinue; sc.exe delete $oldSvc.Name 2>$null }
63
+
64
+ # Remove scheduled tasks (from previous task-scheduler-based installs)
65
+ Write-Host "[0b] Removing old scheduled tasks..." -ForegroundColor Yellow
66
+ foreach ($tn in @($AgentTask, $KioskTask, $GuardianTask)) {
67
+ $t = Get-ScheduledTask -TaskName $tn -ErrorAction SilentlyContinue
68
+ if ($t) { Stop-ScheduledTask -TaskName $tn -ErrorAction SilentlyContinue; Unregister-ScheduledTask -TaskName $tn -Confirm:$false -ErrorAction SilentlyContinue }
69
+ }
70
+
71
+ # Kill processes
72
+ Write-Host "[0c] Killing node.exe and Chrome..." -ForegroundColor Yellow
73
+ Get-Process -Name "node" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
74
+ Get-Process -Name "chrome" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
75
+ Start-Sleep -Seconds 2
76
+
77
+ # Remove old files (keep NSSM and logs)
78
+ Write-Host "[0d] Removing old agent files..." -ForegroundColor Yellow
79
+ Remove-Item -Path $InstallDir -Recurse -Force -ErrorAction SilentlyContinue
80
+ Remove-Item -Path "C:\ProgramData\Lightman\kiosk-url.txt" -Force -ErrorAction SilentlyContinue
81
+
82
+ # Remove firewall rule
83
+ Remove-NetFirewallRule -DisplayName "LIGHTMAN Agent WebSocket" -ErrorAction SilentlyContinue
84
+
85
+ $ErrorActionPreference = "Stop"
86
+ Start-Sleep -Seconds 2
87
+ Write-Host " Clean slate" -ForegroundColor Green
88
+ Write-Host ""
89
+
90
+ # ============================================================
91
+ # PART 1: BUILD & INSTALL
92
+ # ============================================================
93
+
94
+ # --- 1. Node.js ---
95
+ Write-Host "[1/19] Checking Node.js..." -ForegroundColor Yellow
96
+ try {
97
+ $nodeVersion = (node -v) -replace 'v', ''
98
+ if ([int]($nodeVersion.Split('.')[0]) -lt 20) { throw "old" }
99
+ Write-Host " Found Node.js v$nodeVersion"
100
+ } catch {
101
+ Write-Host " Installing Node.js v20.18.0..." -ForegroundColor Yellow
102
+ $installer = "$env:TEMP\node-setup.msi"
103
+ Invoke-WebRequest -Uri "https://nodejs.org/dist/v20.18.0/node-v20.18.0-x64.msi" -OutFile $installer -UseBasicParsing
104
+ Start-Process msiexec.exe -ArgumentList "/i `"$installer`" /qn /norestart" -Wait -NoNewWindow
105
+ Remove-Item $installer -Force -ErrorAction SilentlyContinue
106
+ $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
107
+ if (-not (Get-Command node -ErrorAction SilentlyContinue)) { Write-Host " FATAL: Node.js install failed!" -ForegroundColor Red; exit 1 }
108
+ Write-Host " Node.js installed" -ForegroundColor Green
109
+ }
110
+
111
+ # --- 2. Build ---
112
+ Write-Host "[2/19] Building agent..." -ForegroundColor Yellow
113
+ Push-Location $AgentDir
114
+ $ErrorActionPreference = "Continue"
115
+ & npm install 2>&1 | Out-Host
116
+ & npm run build 2>&1 | Out-Host
117
+ $ErrorActionPreference = "Stop"
118
+ if (-not (Test-Path "$AgentDir\dist\index.js")) { Write-Host " FATAL: Build failed!" -ForegroundColor Red; exit 1 }
119
+ Pop-Location
120
+ Write-Host " Build successful"
121
+
122
+ # --- 3. Directories ---
123
+ Write-Host "[3/19] Creating directories..." -ForegroundColor Yellow
124
+ foreach ($d in @($InstallDir, $LogDir, $ChromeData, $NssmDir)) { New-Item -ItemType Directory -Force -Path $d | Out-Null }
125
+
126
+ # --- 4. Copy files ---
127
+ Write-Host "[4/19] Copying agent files..." -ForegroundColor Yellow
128
+ Copy-Item "$AgentDir\dist" "$InstallDir\dist" -Recurse -Force
129
+ Copy-Item "$AgentDir\package.json" "$InstallDir\package.json" -Force
130
+ if (Test-Path "$AgentDir\package-lock.json") { Copy-Item "$AgentDir\package-lock.json" "$InstallDir\package-lock.json" -Force }
131
+ Copy-Item "$AgentDir\agent.config.template.json" "$InstallDir\agent.config.template.json" -Force
132
+ if (Test-Path "$AgentDir\public") { Copy-Item "$AgentDir\public" "$InstallDir\public" -Recurse -Force }
133
+
134
+ # --- 5. Install deps ---
135
+ Write-Host "[5/19] Installing dependencies..." -ForegroundColor Yellow
136
+ Push-Location $InstallDir
137
+ $ErrorActionPreference = "Continue"
138
+ & npm ci --omit=dev --ignore-scripts 2>&1 | Out-Host
139
+ if ($LASTEXITCODE -ne 0) { & npm install --omit=dev --ignore-scripts 2>&1 | Out-Host }
140
+ $ErrorActionPreference = "Stop"
141
+ Pop-Location
142
+
143
+ # --- 6. Generate config ---
144
+ Write-Host "[6/19] Generating config..." -ForegroundColor Yellow
145
+ if ($ShellReplace) {
146
+ & "$ScriptDir\setup.ps1" -Slug $Slug -Server $Server -Timezone $Timezone -InstallDir $InstallDir -ShellMode
147
+ } else {
148
+ & "$ScriptDir\setup.ps1" -Slug $Slug -Server $Server -Timezone $Timezone -InstallDir $InstallDir
149
+ }
150
+
151
+ # --- 7. Fix BOM ---
152
+ Write-Host "[7/19] Fixing config encoding..." -ForegroundColor Yellow
153
+ $configPath = Join-Path $InstallDir "agent.config.json"
154
+ if (-not (Test-Path $configPath)) { Write-Host " FATAL: config not created!" -ForegroundColor Red; exit 1 }
155
+ $raw = [System.IO.File]::ReadAllText($configPath)
156
+ [System.IO.File]::WriteAllText($configPath, $raw.TrimStart([char]0xFEFF), [System.Text.UTF8Encoding]::new($false))
157
+
158
+ # --- 8. Verify config ---
159
+ Write-Host "[8/19] Verifying config..." -ForegroundColor Yellow
160
+ Push-Location $InstallDir
161
+ $ErrorActionPreference = "Continue"
162
+ $jsonCheck = & node -e "try{const c=JSON.parse(require('fs').readFileSync('agent.config.json','utf8'));console.log('OK slug='+c.deviceSlug+' shellMode='+(c.kiosk&&c.kiosk.shellMode||false))}catch(e){console.log('FAIL: '+e.message);process.exit(1)}" 2>&1
163
+ $ErrorActionPreference = "Stop"
164
+ if ($LASTEXITCODE -ne 0) { Write-Host " FATAL: invalid config: $jsonCheck" -ForegroundColor Red; Pop-Location; exit 1 }
165
+ Pop-Location
166
+ Write-Host " $jsonCheck"
167
+
168
+ # --- 9. Download NSSM ---
169
+ Write-Host "[9/19] Setting up NSSM..." -ForegroundColor Yellow
170
+ if (-not (Test-Path $NssmExe)) {
171
+ # Check bundled copy first (fastest, no internet needed)
172
+ $bundled = Join-Path $AgentDir "nssm\nssm.exe"
173
+ if (Test-Path $bundled) {
174
+ Copy-Item $bundled $NssmExe -Force
175
+ Write-Host " Using bundled NSSM"
176
+ } else {
177
+ # Download from multiple sources
178
+ $nssmZip = "$env:TEMP\nssm.zip"
179
+ $downloaded = $false
180
+ $urls = @(
181
+ "https://nssm.cc/release/nssm-2.24.zip",
182
+ "https://nssm.cc/ci/nssm-2.24-101-g897c7ad.zip"
183
+ )
184
+ foreach ($url in $urls) {
185
+ if ($downloaded) { break }
186
+ Write-Host " Downloading from $url ..."
187
+ try {
188
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
189
+ Invoke-WebRequest -Uri $url -OutFile $nssmZip -UseBasicParsing -TimeoutSec 60
190
+ if ((Test-Path $nssmZip) -and (Get-Item $nssmZip).Length -gt 10000) {
191
+ $downloaded = $true
192
+ }
193
+ } catch {
194
+ Write-Host " Failed: $_" -ForegroundColor DarkYellow
195
+ }
196
+ }
197
+ if ($downloaded) {
198
+ Expand-Archive -Path $nssmZip -DestinationPath "$env:TEMP\nssm-extract" -Force
199
+ # Find nssm.exe in extracted folder (handles different zip structures)
200
+ $found = Get-ChildItem "$env:TEMP\nssm-extract" -Recurse -Filter "nssm.exe" | Where-Object { $_.DirectoryName -like "*win64*" } | Select-Object -First 1
201
+ if (-not $found) { $found = Get-ChildItem "$env:TEMP\nssm-extract" -Recurse -Filter "nssm.exe" | Select-Object -First 1 }
202
+ if ($found) { Copy-Item $found.FullName $NssmExe -Force }
203
+ Remove-Item $nssmZip -Force -ErrorAction SilentlyContinue
204
+ Remove-Item "$env:TEMP\nssm-extract" -Recurse -Force -ErrorAction SilentlyContinue
205
+ }
206
+ }
207
+ }
208
+ if (-not (Test-Path $NssmExe)) {
209
+ Write-Host ""
210
+ Write-Host " NSSM download failed. Manual fix:" -ForegroundColor Red
211
+ Write-Host " 1. Download nssm-2.24.zip from https://nssm.cc/release/nssm-2.24.zip" -ForegroundColor Yellow
212
+ Write-Host " 2. Extract win64\nssm.exe to: $NssmExe" -ForegroundColor Yellow
213
+ Write-Host " 3. Re-run this script" -ForegroundColor Yellow
214
+ Write-Host ""
215
+ Write-Host " OR bundle it in the repo:" -ForegroundColor Yellow
216
+ Write-Host " Copy nssm.exe to: $AgentDir\nssm\nssm.exe" -ForegroundColor Yellow
217
+ exit 1
218
+ }
219
+ Write-Host " NSSM ready: $NssmExe"
220
+
221
+ # --- 10. Install Windows Service via NSSM ---
222
+ Write-Host "[10/19] Installing Windows Service..." -ForegroundColor Yellow
223
+
224
+ # Clean slate
225
+ $ErrorActionPreference = "Continue"
226
+ & $NssmExe stop $ServiceName 2>$null
227
+ & $NssmExe remove $ServiceName confirm 2>$null
228
+ sc.exe delete $ServiceName 2>$null
229
+ Start-Sleep -Seconds 2
230
+ $ErrorActionPreference = "Stop"
231
+
232
+ $nodePath = (Get-Command node).Source
233
+
234
+ # Install
235
+ & $NssmExe install $ServiceName $nodePath "dist\index.js"
236
+ if ($LASTEXITCODE -ne 0) { Write-Host " FATAL: NSSM install failed!" -ForegroundColor Red; exit 1 }
237
+
238
+ # Configure
239
+ & $NssmExe set $ServiceName AppDirectory $InstallDir
240
+ & $NssmExe set $ServiceName DisplayName "LIGHTMAN Agent"
241
+ & $NssmExe set $ServiceName Description "LIGHTMAN kiosk agent - display management and monitoring"
242
+ & $NssmExe set $ServiceName Start SERVICE_AUTO_START
243
+ & $NssmExe set $ServiceName AppStdout "$LogDir\service-stdout.log"
244
+ & $NssmExe set $ServiceName AppStderr "$LogDir\service-stderr.log"
245
+ & $NssmExe set $ServiceName AppStdoutCreationDisposition 4
246
+ & $NssmExe set $ServiceName AppStderrCreationDisposition 4
247
+ & $NssmExe set $ServiceName AppRotateFiles 1
248
+ & $NssmExe set $ServiceName AppRotateBytes 5242880
249
+ & $NssmExe set $ServiceName AppRestartDelay 10000
250
+ & $NssmExe set $ServiceName AppExit Default Restart
251
+
252
+ # Verify service was created
253
+ Start-Sleep -Seconds 2
254
+ $svcCheck = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
255
+ if (-not $svcCheck) {
256
+ $svcCheck = Get-Service -DisplayName "LIGHTMAN*" -ErrorAction SilentlyContinue | Select-Object -First 1
257
+ }
258
+ if (-not $svcCheck) {
259
+ Write-Host " FATAL: Service was not created!" -ForegroundColor Red
260
+ exit 1
261
+ }
262
+ Write-Host " Service installed: $($svcCheck.Name)" -ForegroundColor Green
263
+
264
+ # Recovery policy
265
+ sc.exe failure $svcCheck.Name reset= 86400 actions= restart/5000/restart/10000/restart/30000 2>$null
266
+
267
+ # --- 11. Start service ---
268
+ Write-Host "[11/19] Starting service..." -ForegroundColor Yellow
269
+ Start-Service -Name $svcCheck.Name -ErrorAction SilentlyContinue
270
+ Start-Sleep -Seconds 5
271
+ $svcCheck.Refresh()
272
+
273
+ if ($svcCheck.Status -eq 'Running') {
274
+ Write-Host " Service is RUNNING" -ForegroundColor Green
275
+ } else {
276
+ Write-Host " Service status: $($svcCheck.Status) - check $LogDir" -ForegroundColor Yellow
277
+ Start-Sleep -Seconds 3
278
+ Start-Service -Name $svcCheck.Name -ErrorAction SilentlyContinue
279
+ }
280
+
281
+ # Wait for port 3403
282
+ $portUp = $false
283
+ for ($i = 0; $i -lt 10; $i++) {
284
+ $ErrorActionPreference = "Continue"
285
+ $n = netstat -an 2>$null | findstr ":3403.*LISTENING" 2>$null
286
+ $ErrorActionPreference = "Stop"
287
+ if ($n) { $portUp = $true; break }
288
+ Start-Sleep -Seconds 2
289
+ }
290
+ if ($portUp) { Write-Host " Port 3403 LISTENING" -ForegroundColor Green }
291
+ else { Write-Host " Port 3403 not yet up (may take a moment)" -ForegroundColor Yellow }
292
+
293
+ # --- 12. Firewall ---
294
+ Write-Host "[12/19] Configuring firewall..." -ForegroundColor Yellow
295
+ $ErrorActionPreference = "Continue"
296
+ if (-not (Get-NetFirewallRule -DisplayName "LIGHTMAN Agent WebSocket" -ErrorAction SilentlyContinue)) {
297
+ New-NetFirewallRule -DisplayName "LIGHTMAN Agent WebSocket" -Direction Outbound -Action Allow -Protocol TCP -RemotePort 3001 -Description "LIGHTMAN Agent" | Out-Null
298
+ Write-Host " Created"
299
+ } else { Write-Host " Already exists" }
300
+
301
+ # ============================================================
302
+ # PART 2: KIOSK CONFIGURATION
303
+ # ============================================================
304
+ $ErrorActionPreference = "Continue"
305
+ Write-Host ""
306
+ Write-Host "--- Configuring Kiosk Mode ---" -ForegroundColor Cyan
307
+ Write-Host ""
308
+
309
+ # --- 13. Auto-login ---
310
+ Write-Host "[13/19] Enabling auto-login..." -ForegroundColor Yellow
311
+ $RegPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
312
+ $targetUser = Get-LocalUser -Name $Username -ErrorAction SilentlyContinue
313
+ $isMsAccount = $targetUser -and $targetUser.PrincipalSource -eq 'MicrosoftAccount'
314
+
315
+ if ($isMsAccount) {
316
+ $KioskUser = "kiosk"
317
+ $existingKiosk = Get-LocalUser -Name $KioskUser -ErrorAction SilentlyContinue
318
+ if (-not $existingKiosk) { net user $KioskUser "" /add 2>$null; net localgroup Administrators $KioskUser /add 2>$null }
319
+ else { net user $KioskUser "" 2>$null }
320
+ $HidePath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList"
321
+ if (-not (Test-Path $HidePath)) { New-Item -Path $HidePath -Force | Out-Null }
322
+ Set-ItemProperty -Path $HidePath -Name $Username -Value 0
323
+ $Username = $KioskUser
324
+ Write-Host " Created kiosk account, auto-login: $Username" -ForegroundColor Green
325
+ } else {
326
+ net user $Username "" 2>$null
327
+ }
328
+
329
+ $PwdLess = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\PasswordLess\Device"
330
+ if (Test-Path $PwdLess) { Set-ItemProperty -Path $PwdLess -Name "DevicePasswordLessBuildVersion" -Value 0 }
331
+
332
+ $Passport = "HKLM:\SOFTWARE\Policies\Microsoft\PassportForWork"
333
+ if (-not (Test-Path $Passport)) { New-Item -Path $Passport -Force | Out-Null }
334
+ Set-ItemProperty -Path $Passport -Name "Enabled" -Value 0
335
+
336
+ Set-ItemProperty -Path $RegPath -Name "AutoAdminLogon" -Value "1"
337
+ Set-ItemProperty -Path $RegPath -Name "DefaultUserName" -Value $Username
338
+ Set-ItemProperty -Path $RegPath -Name "DefaultPassword" -Value ""
339
+ Set-ItemProperty -Path $RegPath -Name "DefaultDomainName" -Value ""
340
+ Set-ItemProperty -Path $RegPath -Name "DisableCAD" -Value 1
341
+ Set-ItemProperty -Path $RegPath -Name "AutoRestartShell" -Value 1
342
+ Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "DisableAutomaticRestartSignOn" -Value 0
343
+ $OOBE = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\OOBE"
344
+ if (-not (Test-Path $OOBE)) { New-Item -Path $OOBE -Force | Out-Null }
345
+ Set-ItemProperty -Path $OOBE -Name "DisablePrivacyExperience" -Value 1
346
+ Write-Host " Auto-login enabled for: $Username"
347
+
348
+ # --- 14. Lock screen ---
349
+ Write-Host "[14/19] Removing lock screen..." -ForegroundColor Yellow
350
+ $LP = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization"
351
+ if (-not (Test-Path $LP)) { New-Item -Path $LP -Force | Out-Null }
352
+ Set-ItemProperty -Path $LP -Name "NoLockScreen" -Value 1
353
+
354
+ $SD = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI\SessionData"
355
+ if (Test-Path $SD) { Set-ItemProperty -Path $SD -Name "AllowLockScreen" -Value 0 -ErrorAction SilentlyContinue }
356
+
357
+ $CC = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent"
358
+ if (-not (Test-Path $CC)) { New-Item -Path $CC -Force | Out-Null }
359
+ Set-ItemProperty -Path $CC -Name "DisableWindowsConsumerFeatures" -Value 1
360
+ Set-ItemProperty -Path $CC -Name "DisableCloudOptimizedContent" -Value 1
361
+ $CCU = "HKCU:\SOFTWARE\Policies\Microsoft\Windows\CloudContent"
362
+ if (-not (Test-Path $CCU)) { New-Item -Path $CCU -Force | Out-Null }
363
+ Set-ItemProperty -Path $CCU -Name "DisableWindowsSpotlightFeatures" -Value 1
364
+ Set-ItemProperty -Path $CCU -Name "DisableTailoredExperiencesWithDiagnosticData" -Value 1
365
+
366
+ Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "EnableFirstLogonAnimation" -Value 0
367
+ $SP = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"
368
+ Set-ItemProperty -Path $SP -Name "DisableLockWorkstation" -Value 1
369
+ Set-ItemProperty -Path $SP -Name "HideFastUserSwitching" -Value 1
370
+
371
+ $DL = "HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
372
+ if (-not (Test-Path $DL)) { New-Item -Path $DL -Force | Out-Null }
373
+ Set-ItemProperty -Path $DL -Name "EnableGoodbye" -Value 0
374
+
375
+ $PS = "HKLM:\SOFTWARE\Policies\Microsoft\Power\PowerSettings\0e796bdb-100d-47d6-a2d5-f7d2daa51f51"
376
+ if (-not (Test-Path $PS)) { New-Item -Path $PS -Force | Out-Null }
377
+ Set-ItemProperty -Path $PS -Name "ACSettingIndex" -Value 0
378
+ Set-ItemProperty -Path $PS -Name "DCSettingIndex" -Value 0
379
+ powercfg /SETACVALUEINDEX SCHEME_CURRENT SUB_NONE CONSOLELOCK 0 2>&1 | Out-Null
380
+ powercfg /SETDCVALUEINDEX SCHEME_CURRENT SUB_NONE CONSOLELOCK 0 2>&1 | Out-Null
381
+ powercfg /SETACTIVE SCHEME_CURRENT 2>&1 | Out-Null
382
+
383
+ Set-ItemProperty -Path "HKCU:\Control Panel\Desktop" -Name "ScreenSaverIsSecure" -Value "0"
384
+ Set-ItemProperty -Path "HKCU:\Control Panel\Desktop" -Name "ScreenSaveActive" -Value "0"
385
+ Set-ItemProperty -Path $SP -Name "InactivityTimeoutSecs" -Value 0 -ErrorAction SilentlyContinue
386
+ try { Disable-ScheduledTask -TaskName "\Microsoft\Windows\Shell\CreateObjectTask" -ErrorAction SilentlyContinue | Out-Null } catch { }
387
+ Write-Host " Lock screen fully disabled"
388
+
389
+ # --- 15. Sleep ---
390
+ Write-Host "[15/19] Disabling sleep..." -ForegroundColor Yellow
391
+ powercfg /change monitor-timeout-ac 0 2>&1 | Out-Null
392
+ powercfg /change standby-timeout-ac 0 2>&1 | Out-Null
393
+ powercfg /change hibernate-timeout-ac 0 2>&1 | Out-Null
394
+
395
+ # --- 16. Harden ---
396
+ Write-Host "[16/19] Hardening Windows..." -ForegroundColor Yellow
397
+ $WU = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU"
398
+ if (-not (Test-Path $WU)) { New-Item -Path $WU -Force | Out-Null }
399
+ Set-ItemProperty -Path $WU -Name "NoAutoRebootWithLoggedOnUsers" -Value 1
400
+ Set-ItemProperty -Path $WU -Name "AUOptions" -Value 2
401
+ $WUM = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"
402
+ if (-not (Test-Path $WUM)) { New-Item -Path $WUM -Force | Out-Null }
403
+ Set-ItemProperty -Path $WUM -Name "SetAutoRestartNotificationDisable" -Value 1
404
+ Set-ItemProperty -Path $WUM -Name "SetActiveHours" -Value 1
405
+ Set-ItemProperty -Path $WUM -Name "ActiveHoursStart" -Value 0
406
+ Set-ItemProperty -Path $WUM -Name "ActiveHoursEnd" -Value 23
407
+
408
+ $NP = "HKCU:\SOFTWARE\Policies\Microsoft\Windows\Explorer"
409
+ if (-not (Test-Path $NP)) { New-Item -Path $NP -Force | Out-Null }
410
+ Set-ItemProperty -Path $NP -Name "DisableNotificationCenter" -Value 1
411
+ $TP = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\PushNotifications"
412
+ if (-not (Test-Path $TP)) { New-Item -Path $TP -Force | Out-Null }
413
+ Set-ItemProperty -Path $TP -Name "ToastEnabled" -Value 0
414
+
415
+ $WER = "HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting"
416
+ if (-not (Test-Path $WER)) { New-Item -Path $WER -Force | Out-Null }
417
+ Set-ItemProperty -Path $WER -Name "DontShowUI" -Value 1
418
+ Set-ItemProperty -Path $WER -Name "Disabled" -Value 1
419
+ Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Windows" -Name "ErrorMode" -Value 2 -ErrorAction SilentlyContinue
420
+
421
+ $SR = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search"
422
+ if (-not (Test-Path $SR)) { New-Item -Path $SR -Force | Out-Null }
423
+ Set-ItemProperty -Path $SR -Name "AllowCortana" -Value 0
424
+ Write-Host " Done"
425
+
426
+ # --- 17. Kiosk Chrome ---
427
+ if ($ShellReplace) {
428
+ Write-Host "[17/19] SHELL REPLACEMENT..." -ForegroundColor Magenta
429
+
430
+ # Copy shell BAT (reads slug from agent.config.json - single source of truth)
431
+ $shellSource = Join-Path $ScriptDir "lightman-shell.bat"
432
+ $shellTarget = Join-Path $InstallDir "lightman-shell.bat"
433
+ if (Test-Path $shellSource) { Copy-Item $shellSource $shellTarget -Force }
434
+
435
+ # No sidecar file needed - shell BAT reads directly from agent.config.json
436
+
437
+ # Replace shell
438
+ $ShellReg = "HKCU:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon"
439
+ if (-not (Test-Path $ShellReg)) { New-Item -Path $ShellReg -Force | Out-Null }
440
+ Set-ItemProperty -Path $ShellReg -Name "Shell" -Value """$shellTarget"""
441
+ $HKLMShell = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
442
+ $orig = (Get-ItemProperty -Path $HKLMShell -Name "Shell" -ErrorAction SilentlyContinue).Shell
443
+ if ($orig -and $orig -notlike "*lightman*") { Set-ItemProperty -Path $HKLMShell -Name "Shell_Original" -Value $orig }
444
+ Set-ItemProperty -Path $HKLMShell -Name "Shell" -Value """$shellTarget"""
445
+
446
+ Write-Host " Shell replaced -> lightman-shell.bat" -ForegroundColor Green
447
+ Write-Host " Recovery: scripts\restore-desktop.ps1" -ForegroundColor Yellow
448
+
449
+ # Remove kiosk task if exists
450
+ $kt = Get-ScheduledTask -TaskName $KioskTask -ErrorAction SilentlyContinue
451
+ if ($kt) { Unregister-ScheduledTask -TaskName $KioskTask -Confirm:$false }
452
+ } else {
453
+ Write-Host "[17/19] Standard mode - kiosk browser task..." -ForegroundColor Yellow
454
+ $vbs = Join-Path $ScriptDir "launch-kiosk.vbs"
455
+ $vbsT = Join-Path $InstallDir "launch-kiosk.vbs"
456
+ if (Test-Path $vbs) { Copy-Item $vbs $vbsT -Force }
457
+ $kt = Get-ScheduledTask -TaskName $KioskTask -ErrorAction SilentlyContinue
458
+ if ($kt) { Unregister-ScheduledTask -TaskName $KioskTask -Confirm:$false }
459
+ $kA = New-ScheduledTaskAction -Execute "wscript.exe" -Argument """$vbsT""" -WorkingDirectory $InstallDir
460
+ $kT1 = New-ScheduledTaskTrigger -AtLogOn -User $Username
461
+ $kT2 = New-ScheduledTaskTrigger -AtStartup
462
+ $kS = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1)
463
+ Register-ScheduledTask -TaskName $KioskTask -Action $kA -Trigger @($kT1,$kT2) -Settings $kS -RunLevel Highest -Description "Chrome kiosk at logon/startup" -Force | Out-Null
464
+ Write-Host " Kiosk browser task registered"
465
+ }
466
+
467
+ # --- 18. Guardian ---
468
+ Write-Host "[18/19] Registering Guardian..." -ForegroundColor Yellow
469
+ $gSrc = Join-Path $ScriptDir "guardian.ps1"
470
+ $gDst = Join-Path $InstallDir "guardian.ps1"
471
+ if (Test-Path $gSrc) { Copy-Item $gSrc $gDst -Force }
472
+ $gt = Get-ScheduledTask -TaskName $GuardianTask -ErrorAction SilentlyContinue
473
+ if ($gt) { Unregister-ScheduledTask -TaskName $GuardianTask -Confirm:$false }
474
+ $gA = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -WindowStyle Hidden -File ""$gDst""" -WorkingDirectory $InstallDir
475
+ $gT = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 5) -RepetitionDuration (New-TimeSpan -Days 365)
476
+ $gS = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 2)
477
+ $gP = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
478
+ Register-ScheduledTask -TaskName $GuardianTask -Action $gA -Trigger $gT -Settings $gS -Principal $gP -Description "LIGHTMAN health check every 5 min" -Force | Out-Null
479
+
480
+ foreach ($task in @("\Microsoft\Windows\UpdateOrchestrator\Reboot","\Microsoft\Windows\UpdateOrchestrator\Schedule Retry Scan","\Microsoft\Windows\WindowsUpdate\Scheduled Start")) {
481
+ try { Disable-ScheduledTask -TaskName $task -ErrorAction SilentlyContinue | Out-Null } catch { }
482
+ }
483
+ Write-Host " Guardian registered"
484
+
485
+ # --- 19. Final verification ---
486
+ Write-Host "[19/19] Verification..." -ForegroundColor Yellow
487
+ Start-Sleep -Seconds 3
488
+
489
+ $finalSvc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
490
+ if (-not $finalSvc) { $finalSvc = Get-Service -DisplayName "LIGHTMAN*" -ErrorAction SilentlyContinue | Select-Object -First 1 }
491
+ $svcStatus = if ($finalSvc) { "$($finalSvc.Status)" } else { "NOT FOUND" }
492
+
493
+ $cfgOk = $false
494
+ try {
495
+ Push-Location $InstallDir
496
+ $cfgResult = & node -e "const c=JSON.parse(require('fs').readFileSync('agent.config.json','utf8'));console.log(JSON.stringify({slug:c.deviceSlug,shell:c.kiosk&&c.kiosk.shellMode||false}))" 2>&1
497
+ $cfgData = $cfgResult | ConvertFrom-Json
498
+ Pop-Location
499
+ $cfgOk = $true
500
+ } catch { Pop-Location }
501
+
502
+ Write-Host ""
503
+ Write-Host "=============================================" -ForegroundColor Green
504
+ Write-Host " INSTALLATION COMPLETE" -ForegroundColor Green
505
+ Write-Host "=============================================" -ForegroundColor Green
506
+ Write-Host ""
507
+ Write-Host " Slug : $Slug"
508
+ Write-Host " Server : $Server"
509
+ Write-Host " Install : $InstallDir"
510
+ Write-Host " Logs : $LogDir"
511
+ Write-Host " User : $Username"
512
+ Write-Host ""
513
+ Write-Host " Service : $svcStatus" -ForegroundColor $(if ($svcStatus -eq 'Running') { 'Green' } else { 'Red' })
514
+ if ($cfgOk) {
515
+ Write-Host " Config slug: $($cfgData.slug)" -ForegroundColor $(if ($cfgData.slug -eq $Slug) { 'Green' } else { 'Red' })
516
+ Write-Host " Shell mode : $($cfgData.shell)" -ForegroundColor $(if ($cfgData.shell -eq $ShellReplace.IsPresent) { 'Green' } else { 'Red' })
517
+ }
518
+ Write-Host ""
519
+ Write-Host " Manage:" -ForegroundColor DarkGray
520
+ Write-Host " $NssmExe stop $ServiceName" -ForegroundColor DarkGray
521
+ Write-Host " $NssmExe start $ServiceName" -ForegroundColor DarkGray
522
+ Write-Host " $NssmExe restart $ServiceName" -ForegroundColor DarkGray
523
+ Write-Host ""
524
+ Write-Host " BIOS (manual):" -ForegroundColor Red
525
+ Write-Host " After Power Loss = Power On" -ForegroundColor Red
526
+ Write-Host " Wake-on-LAN = Enabled" -ForegroundColor Red
527
+ Write-Host ""
528
+ Write-Host " REBOOT NOW: Restart-Computer" -ForegroundColor Yellow
529
+ Write-Host ""