tr200 2.0.0

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.
@@ -0,0 +1,512 @@
1
+ <#!
2
+ .SYNOPSIS
3
+ TR-200 Machine Report for Windows (PowerShell implementation).
4
+
5
+ .DESCRIPTION
6
+ Windows-native implementation of the TR-200 Machine Report originally written
7
+ as a cross-platform bash script. This script collects system information
8
+ using Windows APIs (CIM/WMI, performance counters, network cmdlets) and
9
+ renders a Unicode box-drawing report very similar to the Unix version.
10
+
11
+ It is designed to be:
12
+ * Dot-sourced from a PowerShell profile so that the `report` command
13
+ is available in every interactive session.
14
+ * Executed directly as a script (e.g. via a batch shim or `-File`) to
15
+ immediately show the report.
16
+
17
+ .NOTES
18
+ Copyright 2026, ES Development LLC (https://emmetts.dev)
19
+ Based on original work by U.S. Graphics, LLC (BSD-3-Clause)
20
+ Tested : Windows PowerShell 5.1 and PowerShell 7+
21
+ #>
22
+
23
+ #region Encoding and box-drawing configuration
24
+
25
+ # Ensure UTF-8 output for proper box-drawing characters on Windows PowerShell 5.1
26
+ try {
27
+ if ($PSVersionTable.PSEdition -eq 'Desktop' -and $env:OS -like 'Windows*') {
28
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
29
+ }
30
+ } catch {
31
+ # Non-fatal; fallback to whatever the console supports
32
+ }
33
+
34
+ # Box-drawing characters and bar fill characters
35
+ $script:TR200Chars = [pscustomobject]@{
36
+ TopLeft = [char]0x250C # ┌
37
+ TopRight = [char]0x2510 # ┐
38
+ BottomLeft = [char]0x2514 # └
39
+ BottomRight = [char]0x2518 # ┘
40
+ Horizontal = [char]0x2500 # ─
41
+ Vertical = [char]0x2502 # │
42
+ TDown = [char]0x252C # ┬
43
+ TUp = [char]0x2534 # ┴
44
+ TRight = [char]0x251C # ├
45
+ TLeft = [char]0x2524 # ┤
46
+ Cross = [char]0x253C # ┼
47
+ BarFilled = [char]0x2588 # █
48
+ BarEmpty = [char]0x2591 # ░
49
+ }
50
+
51
+ #endregion Encoding and box-drawing configuration
52
+
53
+ #region Utility helpers
54
+
55
+ function New-TR200BarGraph {
56
+ [CmdletBinding()]
57
+ param(
58
+ [double]$Used,
59
+ [double]$Total,
60
+ [int] $Width
61
+ )
62
+
63
+ if ($Total -le 0) {
64
+ return ($TR200Chars.BarEmpty * [math]::Max($Width, 1))
65
+ }
66
+
67
+ $percent = [math]::Max([math]::Min(($Used / $Total) * 100.0, 100.0), 0.0)
68
+ $filledBars = [int]([math]::Round(($percent / 100.0) * $Width))
69
+ if ($filledBars -gt $Width) { $filledBars = $Width }
70
+
71
+ $filled = $TR200Chars.BarFilled * $filledBars
72
+ $empty = $TR200Chars.BarEmpty * ([math]::Max($Width,0) - $filledBars)
73
+ return "$filled$empty"
74
+ }
75
+
76
+ function Get-TR200UptimeString {
77
+ [CmdletBinding()]
78
+ param()
79
+
80
+ try {
81
+ $uptimeSpan = $null
82
+
83
+ if (Get-Command Get-Uptime -ErrorAction SilentlyContinue) {
84
+ $uptimeResult = Get-Uptime
85
+ if ($uptimeResult -is [TimeSpan]) {
86
+ $uptimeSpan = $uptimeResult
87
+ } elseif ($uptimeResult -and $uptimeResult.PSObject.Properties['Uptime']) {
88
+ # PowerShell 7+: Get-Uptime returns an object with an Uptime TimeSpan property
89
+ $uptimeSpan = $uptimeResult.Uptime
90
+ }
91
+ }
92
+
93
+ if (-not $uptimeSpan) {
94
+ $os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop
95
+ $bootTime = $os.LastBootUpTime
96
+ $uptimeSpan = (Get-Date) - $bootTime
97
+ }
98
+
99
+ $days = [int]$uptimeSpan.Days
100
+ $hours = [int]$uptimeSpan.Hours
101
+ $mins = [int]$uptimeSpan.Minutes
102
+
103
+ $parts = @()
104
+ if ($days -gt 0) { $parts += "${days}d" }
105
+ if ($hours -gt 0) { $parts += "${hours}h" }
106
+ if ($mins -gt 0 -or $parts.Count -eq 0) { $parts += "${mins}m" }
107
+ return ($parts -join ' ')
108
+ } catch {
109
+ return 'Unknown'
110
+ }
111
+ }
112
+
113
+ #endregion Utility helpers
114
+
115
+ #region Data collection
116
+
117
+ function Get-TR200Report {
118
+ [CmdletBinding()]
119
+ [OutputType([pscustomobject])]
120
+ param()
121
+
122
+ # Initialize variables with safe defaults
123
+ $osName = 'Unknown OS'
124
+ $osKernel = 'Unknown Kernel'
125
+ $hostname = $env:COMPUTERNAME
126
+ $machineIP = 'No IP found'
127
+ $clientIP = 'Not connected'
128
+ $dnsServers = @()
129
+ $currentUser = [Environment]::UserName
130
+
131
+ $cpuModel = 'Unknown CPU'
132
+ $cpuCores = 0
133
+ $cpuSockets = '-'
134
+ $cpuHypervisor = 'Unknown'
135
+ $cpuFreqGHz = ''
136
+ $cpuUsagePercent = $null
137
+
138
+ $memTotalGiB = 0.0
139
+ $memUsedGiB = 0.0
140
+ $memPercent = 0.0
141
+
142
+ $diskTotalGiB = 0.0
143
+ $diskUsedGiB = 0.0
144
+ $diskPercent = 0.0
145
+
146
+ $lastLoginTime = 'Login tracking unavailable'
147
+
148
+ $uptimeString = Get-TR200UptimeString
149
+
150
+ try {
151
+ $os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop
152
+ $osName = "{0} {1}" -f $os.Caption.Trim(), $os.Version
153
+ $osKernel = "Windows {0}" -f $os.Version
154
+ } catch { }
155
+
156
+ try {
157
+ $cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop
158
+ if ($cs.Name) { $hostname = $cs.Name }
159
+
160
+ # Hypervisor detection
161
+ if ($cs.HypervisorPresent -eq $true) {
162
+ $model = $cs.Model
163
+ switch -Regex ($model) {
164
+ 'Virtual Machine' { $cpuHypervisor = 'Hyper-V'; break }
165
+ 'VMware' { $cpuHypervisor = 'VMware'; break }
166
+ 'VirtualBox' { $cpuHypervisor = 'VirtualBox'; break }
167
+ default { $cpuHypervisor = 'Virtualized' }
168
+ }
169
+ } else {
170
+ $cpuHypervisor = 'Bare Metal'
171
+ }
172
+ } catch { }
173
+
174
+ try {
175
+ $cpu = Get-CimInstance -ClassName Win32_Processor -ErrorAction Stop | Select-Object -First 1
176
+ if ($cpu) {
177
+ $cpuModel = $cpu.Name.Trim()
178
+ $cpuCores = $cpu.NumberOfLogicalProcessors
179
+ $cpuSockets = $cpu.SocketDesignation
180
+ if ($cpu.MaxClockSpeed -gt 0) {
181
+ $cpuFreqGHz = [math]::Round($cpu.MaxClockSpeed / 1000.0, 2)
182
+ }
183
+ }
184
+ } catch { }
185
+
186
+ try {
187
+ # Memory in KB from Win32_OperatingSystem
188
+ $osMem = $os
189
+ if (-not $osMem) {
190
+ $osMem = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop
191
+ }
192
+
193
+ $totalKB = [double]$osMem.TotalVisibleMemorySize
194
+ $freeKB = [double]$osMem.FreePhysicalMemory
195
+ $usedKB = $totalKB - $freeKB
196
+
197
+ if ($totalKB -gt 0) {
198
+ $memTotalGiB = [math]::Round($totalKB / (1024.0 * 1024.0), 2)
199
+ $memUsedGiB = [math]::Round($usedKB / (1024.0 * 1024.0), 2)
200
+ $memPercent = [math]::Round(($usedKB / $totalKB) * 100.0, 2)
201
+ }
202
+ } catch { }
203
+
204
+ try {
205
+ # System drive (usually C:)
206
+ $systemDrive = $env:SystemDrive
207
+ if (-not $systemDrive) { $systemDrive = 'C:' }
208
+ $disk = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DeviceID='$systemDrive'" -ErrorAction Stop
209
+ if ($disk.Size -gt 0) {
210
+ $diskTotalGiB = [math]::Round($disk.Size / 1GB, 2)
211
+ $diskUsedGiB = [math]::Round(($disk.Size - $disk.FreeSpace) / 1GB, 2)
212
+ $diskPercent = [math]::Round((($disk.Size - $disk.FreeSpace) / $disk.Size) * 100.0, 2)
213
+ }
214
+ } catch { }
215
+
216
+ try {
217
+ # Machine IP (IPv4 preferred)
218
+ if (Get-Command Get-NetIPAddress -ErrorAction SilentlyContinue) {
219
+ $ip = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
220
+ Where-Object { $_.IPAddress -notlike '127.*' -and $_.IPAddress -notlike '169.254.*' } |
221
+ Select-Object -First 1 -ExpandProperty IPAddress
222
+ if (-not $ip) {
223
+ $ip = Get-NetIPAddress -AddressFamily IPv6 -ErrorAction SilentlyContinue |
224
+ Where-Object { $_.IPAddress -notlike 'fe80::*' } |
225
+ Select-Object -First 1 -ExpandProperty IPAddress
226
+ }
227
+ if ($ip) { $machineIP = $ip }
228
+ } else {
229
+ # Fallback to WMI-based network info
230
+ $nics = Get-CimInstance Win32_NetworkAdapterConfiguration -Filter "IPEnabled=true" -ErrorAction SilentlyContinue
231
+ if ($nics) {
232
+ $candidate = $nics | Where-Object { $_.IPAddress } | Select-Object -First 1
233
+ if ($candidate -and $candidate.IPAddress) {
234
+ $machineIP = $candidate.IPAddress | Where-Object { $_ -notlike '127.*' } | Select-Object -First 1
235
+ }
236
+ }
237
+ }
238
+ } catch { }
239
+
240
+ try {
241
+ # DNS servers
242
+ if (Get-Command Get-DnsClientServerAddress -ErrorAction SilentlyContinue) {
243
+ $dnsServers = Get-DnsClientServerAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
244
+ Where-Object { $_.ServerAddresses } |
245
+ ForEach-Object { $_.ServerAddresses } |
246
+ Select-Object -First 5
247
+ } elseif ($nics) {
248
+ $dnsServers = $nics | Where-Object { $_.DNSServerSearchOrder } |
249
+ ForEach-Object { $_.DNSServerSearchOrder } |
250
+ Select-Object -First 5
251
+ }
252
+ } catch { }
253
+
254
+ try {
255
+ # Approximate client IP for SSH/remote sessions using environment variables
256
+ if ($env:SSH_CLIENT -or $env:SSH_CONNECTION) {
257
+ # SSH_CLIENT format: "client_ip client_port server_port"
258
+ $raw = $env:SSH_CLIENT
259
+ if (-not $raw) { $raw = $env:SSH_CONNECTION }
260
+ if ($raw) {
261
+ $clientIP = $raw.Split(' ')[0]
262
+ }
263
+ } else {
264
+ $clientIP = 'Not connected'
265
+ }
266
+ } catch { }
267
+
268
+ try {
269
+ # CPU usage: instantaneous sample of % Processor Time
270
+ if (Get-Command Get-Counter -ErrorAction SilentlyContinue) {
271
+ $counter = Get-Counter '\Processor(_Total)\% Processor Time' -ErrorAction Stop
272
+ $cpuUsagePercent = [math]::Round($counter.CounterSamples[0].CookedValue, 2)
273
+ }
274
+ } catch { }
275
+
276
+ [pscustomobject]@{
277
+ ReportTitle = 'SHAUGHNESSY V DEVELOPMENT INC.'
278
+ ReportSubtitle = 'TR-200 MACHINE REPORT'
279
+
280
+ OSName = $osName
281
+ OSKernel = $osKernel
282
+
283
+ Hostname = $hostname
284
+ MachineIP = $machineIP
285
+ ClientIP = $clientIP
286
+ DNSServers = $dnsServers
287
+ CurrentUser = $currentUser
288
+
289
+ CPUModel = $cpuModel
290
+ CPUCores = $cpuCores
291
+ CPUSockets = $cpuSockets
292
+ CPUHypervisor = $cpuHypervisor
293
+ CPUFreqGHz = $cpuFreqGHz
294
+ CPUUsagePercent = $cpuUsagePercent
295
+
296
+ MemTotalGiB = $memTotalGiB
297
+ MemUsedGiB = $memUsedGiB
298
+ MemPercent = $memPercent
299
+
300
+ DiskTotalGiB = $diskTotalGiB
301
+ DiskUsedGiB = $diskUsedGiB
302
+ DiskPercent = $diskPercent
303
+
304
+ LastLoginTime = $lastLoginTime
305
+ Uptime = $uptimeString
306
+ }
307
+ }
308
+
309
+ #endregion Data collection
310
+
311
+ #region Rendering
312
+
313
+ function Show-TR200Report {
314
+ [CmdletBinding()]
315
+ param()
316
+
317
+ $data = Get-TR200Report
318
+
319
+ # Build list of label/value pairs in order
320
+ $rows = @()
321
+ $rows += [pscustomobject]@{ Label = 'OS'; Value = $data.OSName }
322
+ $rows += [pscustomobject]@{ Label = 'KERNEL'; Value = $data.OSKernel }
323
+ $rows += 'DIVIDER'
324
+ $rows += [pscustomobject]@{ Label = 'HOSTNAME'; Value = $data.Hostname }
325
+ $rows += [pscustomobject]@{ Label = 'MACHINE IP';Value = $data.MachineIP }
326
+ $rows += [pscustomobject]@{ Label = 'CLIENT IP';Value = $data.ClientIP }
327
+
328
+ if ($data.DNSServers -and $data.DNSServers.Count -gt 0) {
329
+ $i = 1
330
+ foreach ($dns in $data.DNSServers) {
331
+ $rows += [pscustomobject]@{ Label = "DNS IP $i"; Value = $dns }
332
+ $i++
333
+ }
334
+ }
335
+
336
+ $rows += [pscustomobject]@{ Label = 'USER'; Value = $data.CurrentUser }
337
+ $rows += 'DIVIDER'
338
+
339
+ $rows += [pscustomobject]@{ Label = 'PROCESSOR'; Value = $data.CPUModel }
340
+ $rows += [pscustomobject]@{ Label = 'CORES'; Value = ("{0} vCPU(s) / {1} Socket(s)" -f $data.CPUCores, $data.CPUSockets) }
341
+ $rows += [pscustomobject]@{ Label = 'HYPERVISOR';Value = $data.CPUHypervisor }
342
+ if ($data.CPUFreqGHz) {
343
+ $rows += [pscustomobject]@{ Label = 'CPU FREQ'; Value = ("{0} GHz" -f $data.CPUFreqGHz) }
344
+ }
345
+
346
+ # CPU load-style bar graphs (using instantaneous CPU percentage)
347
+ $rows += [pscustomobject]@{ Label = 'LOAD 1m'; Value = '$CPU_LOAD_1M$' }
348
+ $rows += [pscustomobject]@{ Label = 'LOAD 5m'; Value = '$CPU_LOAD_5M$' }
349
+ $rows += [pscustomobject]@{ Label = 'LOAD 15m'; Value = '$CPU_LOAD_15M$' }
350
+
351
+ $rows += 'DIVIDER'
352
+
353
+ $rows += [pscustomobject]@{ Label = 'VOLUME'; Value = ("{0}/{1} GB [{2}%]" -f $data.DiskUsedGiB, $data.DiskTotalGiB, $data.DiskPercent) }
354
+ $rows += [pscustomobject]@{ Label = 'DISK USAGE';Value = '$DISK_USAGE$' }
355
+
356
+ $rows += 'DIVIDER'
357
+
358
+ $rows += [pscustomobject]@{ Label = 'MEMORY'; Value = ("{0}/{1} GiB [{2}%]" -f $data.MemUsedGiB, $data.MemTotalGiB, $data.MemPercent) }
359
+ $rows += [pscustomobject]@{ Label = 'USAGE'; Value = '$MEM_USAGE$' }
360
+
361
+ $rows += 'DIVIDER'
362
+
363
+ $rows += [pscustomobject]@{ Label = 'LAST LOGIN';Value = $data.LastLoginTime }
364
+ $rows += [pscustomobject]@{ Label = 'UPTIME'; Value = $data.Uptime }
365
+
366
+ # Compute column widths
367
+ $labelStrings = $rows | Where-Object { $_ -isnot [string] } | ForEach-Object { $_.Label }
368
+ $valueStrings = $rows | Where-Object { $_ -isnot [string] } | ForEach-Object { $_.Value }
369
+
370
+ $minLabel = 5
371
+ $maxLabel = 13
372
+ $minData = 20
373
+ $maxData = 32
374
+
375
+ $labelWidth = $labelStrings | ForEach-Object { $_.Length } | Measure-Object -Maximum | Select-Object -ExpandProperty Maximum
376
+ if (-not $labelWidth) { $labelWidth = $minLabel }
377
+ $labelWidth = [math]::Max($minLabel, [math]::Min($labelWidth, $maxLabel))
378
+
379
+ $dataWidth = $valueStrings | ForEach-Object { ($_.ToString()).Length } | Measure-Object -Maximum | Select-Object -ExpandProperty Maximum
380
+ if (-not $dataWidth) { $dataWidth = $minData }
381
+ $dataWidth = [math]::Max($minData, [math]::Min($dataWidth, $maxData))
382
+
383
+ # Bar graph width based on data column width
384
+ $barWidth = $dataWidth
385
+
386
+ # Now that we know bar width, generate bar strings and substitute placeholders
387
+ $cpuUsed = if ($data.CPUUsagePercent -ne $null) { $data.CPUUsagePercent } else { 0 }
388
+ $cpuBar = New-TR200BarGraph -Used $cpuUsed -Total 100 -Width $barWidth
389
+
390
+ $diskBar = New-TR200BarGraph -Used $data.DiskUsedGiB -Total $data.DiskTotalGiB -Width $barWidth
391
+ $memBar = New-TR200BarGraph -Used $data.MemUsedGiB -Total $data.MemTotalGiB -Width $barWidth
392
+
393
+ $rows = $rows | ForEach-Object {
394
+ if ($_ -is [string]) { return $_ }
395
+ $value = $_.Value
396
+ switch ($value) {
397
+ '$CPU_LOAD_1M$' { $value = $cpuBar }
398
+ '$CPU_LOAD_5M$' { $value = $cpuBar }
399
+ '$CPU_LOAD_15M$' { $value = $cpuBar }
400
+ '$DISK_USAGE$' { $value = $diskBar }
401
+ '$MEM_USAGE$' { $value = $memBar }
402
+ }
403
+ [pscustomobject]@{ Label = $_.Label; Value = $value }
404
+ }
405
+
406
+ # Total inner width of table (excluding outer borders)
407
+ $innerWidth = 2 + $labelWidth + 3 + $dataWidth + 2 # "│ <label> │ <value> │"
408
+
409
+ # Helper to write the top header and borders
410
+ function Write-TR200TopHeader {
411
+ param()
412
+ $top = $TR200Chars.TopLeft + ($TR200Chars.Horizontal * ($innerWidth)) + $TR200Chars.TopRight
413
+ Write-Host $top
414
+ $mid = $TR200Chars.TRight + ($TR200Chars.TDown * ($innerWidth)) + $TR200Chars.TLeft
415
+ Write-Host $mid
416
+ }
417
+
418
+ function Write-TR200Divider {
419
+ param([string]$Position)
420
+
421
+ $left = $TR200Chars.TRight
422
+ $right = $TR200Chars.TLeft
423
+ $mid = if ($Position -eq 'Bottom') { $TR200Chars.TUp } else { $TR200Chars.Cross }
424
+
425
+ $line = $left
426
+ for ($i = 0; $i -lt $innerWidth; $i++) {
427
+ if ($i -eq ($labelWidth + 2)) {
428
+ $line += $mid
429
+ } else {
430
+ $line += $TR200Chars.Horizontal
431
+ }
432
+ }
433
+ $line += $right
434
+ Write-Host $line
435
+ }
436
+
437
+ function Write-TR200Footer {
438
+ param()
439
+ $bottom = $TR200Chars.BottomLeft + ($TR200Chars.Horizontal * ($innerWidth)) + $TR200Chars.BottomRight
440
+ Write-Host $bottom
441
+ }
442
+
443
+ function Write-TR200CenteredLine {
444
+ param([string]$Text)
445
+ $totalWidth = $innerWidth
446
+ $text = $Text
447
+ if ($text.Length -gt $totalWidth) {
448
+ $text = $text.Substring(0, $totalWidth)
449
+ }
450
+ $padding = $totalWidth - $text.Length
451
+ $leftPad = [int]([math]::Floor($padding / 2.0))
452
+ $rightPad = $padding - $leftPad
453
+ Write-Host ("{0}{1}{2}{3}{4}" -f $TR200Chars.Vertical, ' ' * $leftPad, $text, ' ' * $rightPad, $TR200Chars.Vertical)
454
+ }
455
+
456
+ function Write-TR200Row {
457
+ param(
458
+ [string]$Label,
459
+ [string]$Value
460
+ )
461
+
462
+ # Trim/truncate label
463
+ $lbl = $Label
464
+ if ($lbl.Length -gt $labelWidth) {
465
+ $lbl = $lbl.Substring(0, [math]::Max($labelWidth - 3, 1)) + '...'
466
+ } else {
467
+ $lbl = $lbl.PadRight($labelWidth)
468
+ }
469
+
470
+ # Trim/truncate value
471
+ $val = $Value
472
+ if ($null -eq $val) { $val = '' }
473
+ if ($val.Length -gt $dataWidth) {
474
+ $val = $val.Substring(0, [math]::Max($dataWidth - 3, 1)) + '...'
475
+ } else {
476
+ $val = $val.PadRight($dataWidth)
477
+ }
478
+
479
+ Write-Host ("{0} {1} {2} {3} {4}" -f $TR200Chars.Vertical, $lbl, $TR200Chars.Vertical, $val, $TR200Chars.Vertical)
480
+ }
481
+
482
+ # Render table
483
+ Write-TR200TopHeader
484
+ Write-TR200CenteredLine -Text $data.ReportTitle
485
+ Write-TR200CenteredLine -Text $data.ReportSubtitle
486
+ Write-TR200Divider -Position 'Top'
487
+
488
+ foreach ($row in $rows) {
489
+ if ($row -is [string]) {
490
+ if ($row -eq 'DIVIDER') {
491
+ Write-TR200Divider -Position 'Middle'
492
+ }
493
+ } else {
494
+ Write-TR200Row -Label $row.Label -Value ($row.Value.ToString())
495
+ }
496
+ }
497
+
498
+ Write-TR200Divider -Position 'Bottom'
499
+ Write-TR200Footer
500
+ }
501
+
502
+ #endregion Rendering
503
+
504
+ # If the script is executed directly (not dot-sourced), show the report immediately
505
+ try {
506
+ if ($MyInvocation.InvocationName -ne '.') {
507
+ # Only auto-run when invoked as a script, not when dot-sourced from a profile
508
+ Show-TR200Report
509
+ }
510
+ } catch {
511
+ Write-Error $_
512
+ }
package/bin/tr200.js ADDED
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * TR-200 Machine Report - CLI Wrapper
5
+ *
6
+ * Cross-platform Node.js wrapper that detects the OS and runs
7
+ * the appropriate script (bash on Unix, PowerShell on Windows).
8
+ *
9
+ * Copyright 2026, ES Development LLC (https://emmetts.dev)
10
+ * BSD 3-Clause License
11
+ */
12
+
13
+ const { spawn } = require('child_process');
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+
17
+ const isWindows = process.platform === 'win32';
18
+
19
+ // Locate the script files relative to this wrapper
20
+ const packageRoot = path.resolve(__dirname, '..');
21
+ const bashScript = path.join(packageRoot, 'machine_report.sh');
22
+ const psScript = path.join(packageRoot, 'WINDOWS', 'TR-200-MachineReport.ps1');
23
+
24
+ function runReport() {
25
+ let command, args, scriptPath;
26
+
27
+ if (isWindows) {
28
+ scriptPath = psScript;
29
+
30
+ if (!fs.existsSync(scriptPath)) {
31
+ console.error(`Error: PowerShell script not found at ${scriptPath}`);
32
+ process.exit(1);
33
+ }
34
+
35
+ // Try pwsh (PowerShell 7+) first, fall back to powershell (5.1)
36
+ command = 'pwsh';
37
+ args = ['-ExecutionPolicy', 'Bypass', '-NoProfile', '-File', scriptPath];
38
+
39
+ const child = spawn(command, args, {
40
+ stdio: 'inherit',
41
+ shell: false
42
+ });
43
+
44
+ child.on('error', (err) => {
45
+ // If pwsh not found, try Windows PowerShell
46
+ if (err.code === 'ENOENT') {
47
+ const fallback = spawn('powershell', args, {
48
+ stdio: 'inherit',
49
+ shell: false
50
+ });
51
+
52
+ fallback.on('error', (fallbackErr) => {
53
+ console.error('Error: PowerShell not found. Please ensure PowerShell is installed.');
54
+ process.exit(1);
55
+ });
56
+
57
+ fallback.on('close', (code) => {
58
+ process.exit(code || 0);
59
+ });
60
+ } else {
61
+ console.error(`Error running report: ${err.message}`);
62
+ process.exit(1);
63
+ }
64
+ });
65
+
66
+ child.on('close', (code) => {
67
+ process.exit(code || 0);
68
+ });
69
+
70
+ } else {
71
+ // Unix (Linux, macOS, BSD)
72
+ scriptPath = bashScript;
73
+
74
+ if (!fs.existsSync(scriptPath)) {
75
+ console.error(`Error: Bash script not found at ${scriptPath}`);
76
+ process.exit(1);
77
+ }
78
+
79
+ command = 'bash';
80
+ args = [scriptPath];
81
+
82
+ const child = spawn(command, args, {
83
+ stdio: 'inherit',
84
+ shell: false
85
+ });
86
+
87
+ child.on('error', (err) => {
88
+ if (err.code === 'ENOENT') {
89
+ console.error('Error: Bash not found. Please ensure bash is installed.');
90
+ } else {
91
+ console.error(`Error running report: ${err.message}`);
92
+ }
93
+ process.exit(1);
94
+ });
95
+
96
+ child.on('close', (code) => {
97
+ process.exit(code || 0);
98
+ });
99
+ }
100
+ }
101
+
102
+ // Handle help flag
103
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
104
+ console.log(`
105
+ TR-200 Machine Report v2.0.0
106
+
107
+ Usage: tr200 [options]
108
+ report [options]
109
+
110
+ Displays system information in a formatted table with Unicode box-drawing.
111
+
112
+ Options:
113
+ -h, --help Show this help message
114
+ -v, --version Show version number
115
+
116
+ More info: https://github.com/RealEmmettS/usgc-machine-report
117
+ `);
118
+ process.exit(0);
119
+ }
120
+
121
+ // Handle version flag
122
+ if (process.argv.includes('--version') || process.argv.includes('-v')) {
123
+ console.log('2.0.0');
124
+ process.exit(0);
125
+ }
126
+
127
+ // Run the report
128
+ runReport();