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.
- package/LICENSE +29 -0
- package/README.md +529 -0
- package/WINDOWS/TR-200-MachineReport.ps1 +512 -0
- package/bin/tr200.js +128 -0
- package/machine_report.sh +683 -0
- package/package.json +47 -0
|
@@ -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();
|