Skip to content

Commit db05644

Browse files
committed
🚜 [refactor] Modularize private implementation and add robust helpers for ANSI, caching, metadata, errors and localization
✨ [feat] Add ANSI utilities for colored text handling - Get-ColorScriptAnsiSequence: map common color names to ANSI sequences - New-ColorScriptAnsiText: wrap text with ANSI sequences and honor -NoAnsiOutput - Remove-ColorScriptAnsiSequence: compiled/cached regex ($script:AnsiStripRegex) to strip ANSI sequences ✨ [feat] Add JSON/hashtable compatibility helpers - ConvertFrom-JsonToHashtable: cross-version ConvertFrom-Json wrapper (uses -AsHashtable on PS6+) - ConvertTo-HashtableInternal: normalize PSCustomObject / enumerable structures into plain hashtables/arrays (PS5 fallback) for deterministic merging and caching ✨ [feat] Add structured error creation & consistent throwing - New-ColorScriptErrorRecord: build rich ErrorRecord (Message, ErrorId, Category, TargetObject, RecommendedAction, Exception wrapping) - Invoke-ColorScriptError: throw or ThrowTerminatingError via PSCmdlet using the constructed ErrorRecord ✨ [feat] Add process execution and cache build/read helpers - Invoke-ColorScriptProcess: execute colorscripts; supports fast in-process execution when -ForCache and preserves isolated UTF-8 child-process path for display - Build-ScriptCache: produce cache file from script execution, preserve timestamps, return detailed result object - Get-CachedOutput: read cached output safely via delegates and validate freshness vs script last-write - Initialize-CacheDirectory: resolve cache location candidates (env/config/OS), create directories with failure callbacks and fall back to temp ✨ [feat] Add metadata and inventory management - Get-ColorScriptInventory: file discovery and inventory caching with timestamp checks and raw/record modes - Get-ColorScriptMetadataTableInternal / Get-ColorScriptMetadataTable: build metadata dictionary from Messages.psd1 and merge auto-categories; support fast JSON metadata cache (metadata.cache.json) - Get-ColorScriptEntry & Select-RecordsByName: expose normalized script entries, name/category/tag filtering and pattern matching - Add $script:DefaultAutoCategoryRules default rules to enable auto-categorization when metadata lacks rules 🚜 [refactor] Split monolithic PrivateFunctions.ps1 into many focused files - Removed huge PrivateFunctions.ps1 and moved each helper into its own file for clarity, testability and DI via delegates - Initialize-SystemDelegateState centralizes filesystem/console delegates used by many helpers (enables injection & mocking) ✨ [feat] Improve configuration & localization flows - Get-ColorScriptsConfigurationRoot / Initialize-Configuration / Save-ColorScriptConfiguration / Get-ConfigurationDataInternal: robust cross-OS config root resolution, config.json load/merge/save with default fallback - Resolve-LocalizedMessagesFile / Import-LocalizedMessagesFromFile / Initialize-ColorScriptsLocalization: localized message discovery via Import-LocalizedData with Import-PowerShellDataFile fallback, candidate root probing, and embedded fallback defaults ✨ [feat] Add general utilities and safe IO helpers - Resolve-CachePath, Resolve-PreferredDirectory, Resolve-PreferredDirectoryCandidate: portable path resolution and directory creation with callbacks - Invoke-ModuleSynchronized: Monitor-based synchronization helper - Invoke-ShouldProcess: central ShouldProcess evaluation hook - Invoke-WithUtf8Encoding: temporarily set console encoding to UTF-8 for blocks and restore safely - Invoke-FileWriteAllText, Get/Set FileLastWriteTime(utc): small wrappers for file I/O and timestamp preservation used by cache build ✨ [feat] Add validation, matching & output helpers - Test-ColorScriptNameValue / Test-ColorScriptPathValue: validation functions that throw ValidationMetadataException with localized messages (used in ValidateScript blocks) - New-NameMatcherSet: build wildcard/exact matcher objects used by Select-RecordsByName - Test-ColorScriptTextEmission: determine if cmdlets should emit text vs return values - Write-RenderedText & Write-ColorScriptInformation: consistent render path with NoAnsiOutput support and Write-Information tagging ('ColorScripts'); Write-RenderedText falls back to pipeline output on console IO errors 🔧 [build] Update BeastMode agent descriptor - .github/agents/BeastMode.agent.md: adjust tools list ordering and add 'memory' to agent toolset 🧹 [chore] Clean up and prepare for testing - Consolidated many helpers into individual files to improve unit testing granularity and dependency injection points - Deleted legacy combined PrivateFunctions.ps1 to avoid duplication and simplify CI/test targets Notes: - Primary goal: break down a large monolithic private implementation into composable, testable pieces and introduce robust utilities for ANSI handling, cross-version JSON -> hashtable conversion, deterministic metadata caching, standardized errors, safe cache IO and localization discovery. - Behavior preserved: where needed the child-process execution and output encoding behavior are retained for display scenarios; a fast in-process path was added specifically to speed cache building while keeping parity. Signed-off-by: Nick2bad4u <20943337+Nick2bad4u@users.noreply.github.com>
1 parent ffefde0 commit db05644

51 files changed

Lines changed: 2846 additions & 2849 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/agents/BeastMode.agent.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: BeastMode
33
description: Beast Mode 3.1 (Custom) - PowerShell Module Specialist
44
argument-hint: "💻 🤖 😈 Beast Mode agent ready for PowerShell excellence. 👿 🤖 💻"
55
model: GPT-5-Codex (Preview) (copilot)
6-
tools: ['edit/createFile', 'edit/createDirectory', 'edit/editFiles', 'search/fileSearch', 'search/textSearch', 'search/listDirectory', 'search/readFile', 'search/codebase', 'runCommands/getTerminalOutput', 'runCommands/terminalLastCommand', 'runCommands/runInTerminal', 'runTasks/runTask', 'runTasks/getTaskOutput', 'tavily/tavily-extract', 'tavily/tavily-map', 'tavily/tavily-search', 'vscode-mcp/execute_command', 'vscode-mcp/get_diagnostics', 'vscode-mcp/get_references', 'vscode-mcp/get_symbol_lsp_info', 'vscode-mcp/rename_symbol', 'runSubagent', 'usages', 'problems', 'changes', 'testFailure', 'fetch', 'ms-vscode.vscode-websearchforcopilot/websearch', 'todos', 'runTests']
6+
tools: ['edit/createFile', 'edit/createDirectory', 'edit/editFiles', 'search/fileSearch', 'search/textSearch', 'search/listDirectory', 'search/readFile', 'search/codebase', 'tavily/tavily-extract', 'tavily/tavily-map', 'tavily/tavily-search', 'vscode-mcp/execute_command', 'vscode-mcp/get_diagnostics', 'vscode-mcp/get_references', 'vscode-mcp/get_symbol_lsp_info', 'vscode-mcp/rename_symbol', 'runCommands/getTerminalOutput', 'runCommands/terminalLastCommand', 'runCommands/runInTerminal', 'runTasks/runTask', 'runTasks/getTaskOutput', 'runSubagent', 'usages', 'problems', 'changes', 'testFailure', 'fetch', 'memory', 'ms-vscode.vscode-websearchforcopilot/websearch', 'todos', 'runTests']
77
handoffs:
88
- label: Consistency Check
99
agent: BeastMode
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
function Build-ScriptCache {
2+
<#
3+
.SYNOPSIS
4+
Builds cache for a specific colorscript.
5+
6+
The full path to the colorscript file.
7+
#>
8+
[CmdletBinding()]
9+
[OutputType([pscustomobject])]
10+
param(
11+
[Parameter(Mandatory)]
12+
[string]$ScriptPath
13+
)
14+
15+
$scriptName = [System.IO.Path]::GetFileNameWithoutExtension($ScriptPath)
16+
$cacheFile = Join-Path $script:CacheDir "$scriptName.cache"
17+
18+
$result = [pscustomobject]@{
19+
ScriptName = $scriptName
20+
CacheFile = $cacheFile
21+
Success = $false
22+
ExitCode = $null
23+
StdOut = ''
24+
StdErr = ''
25+
}
26+
27+
if (-not [System.IO.File]::Exists($ScriptPath)) {
28+
$result.StdErr = $script:Messages.ScriptPathNotFound
29+
return $result
30+
}
31+
32+
$execution = Invoke-ColorScriptProcess -ScriptPath $ScriptPath -ForCache
33+
$result.ExitCode = $execution.ExitCode
34+
$result.StdOut = $execution.StdOut
35+
$result.StdErr = $execution.StdErr
36+
37+
if ($execution.Success) {
38+
try {
39+
Invoke-FileWriteAllText -Path $cacheFile -Content $execution.StdOut -Encoding $script:Utf8NoBomEncoding
40+
41+
try {
42+
$scriptLastWrite = Get-FileLastWriteTimeUtc -Path $ScriptPath
43+
Set-FileLastWriteTimeUtc -Path $cacheFile -Timestamp $scriptLastWrite
44+
}
45+
catch {
46+
$scriptLastWrite = Get-FileLastWriteTime -Path $ScriptPath
47+
Set-FileLastWriteTime -Path $cacheFile -Timestamp $scriptLastWrite
48+
}
49+
$result.Success = $true
50+
}
51+
catch {
52+
$result.StdErr = $_.Exception.Message
53+
}
54+
}
55+
elseif (-not $result.StdErr) {
56+
$result.StdErr = ($script:Messages.ScriptExitedWithCode -f $execution.ExitCode)
57+
}
58+
59+
return $result
60+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
function ConvertFrom-JsonToHashtable {
2+
<#
3+
.SYNOPSIS
4+
Converts JSON to a hashtable, compatible with PowerShell 5.1 and 7+
5+
.DESCRIPTION
6+
PowerShell 5.1 doesn't support -AsHashtable parameter on ConvertFrom-Json.
7+
This function provides a compatible conversion method for all PowerShell versions.
8+
#>
9+
param(
10+
[Parameter(Mandatory, ValueFromPipeline)]
11+
[AllowEmptyString()]
12+
[string]$InputObject
13+
)
14+
15+
process {
16+
if ([string]::IsNullOrWhiteSpace($InputObject)) {
17+
return $null
18+
}
19+
20+
# PowerShell 6.0+ supports -AsHashtable natively
21+
if ($PSVersionTable.PSVersion.Major -ge 6) {
22+
return ConvertFrom-Json -InputObject $InputObject -AsHashtable
23+
}
24+
25+
$obj = ConvertFrom-Json -InputObject $InputObject
26+
return ConvertTo-HashtableInternal $obj
27+
}
28+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
function ConvertTo-HashtableInternal {
2+
param([Parameter(ValueFromPipeline)]$InputObject)
3+
4+
process {
5+
if ($null -eq $InputObject) {
6+
return $null
7+
}
8+
9+
if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {
10+
$collection = @()
11+
foreach ($item in $InputObject) {
12+
$collection += ConvertTo-HashtableInternal $item
13+
}
14+
return $collection
15+
}
16+
17+
if ($InputObject -is [PSCustomObject]) {
18+
$hash = @{}
19+
foreach ($property in $InputObject.PSObject.Properties) {
20+
$hash[$property.Name] = ConvertTo-HashtableInternal $property.Value
21+
}
22+
return $hash
23+
}
24+
25+
return $InputObject
26+
}
27+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
function Copy-ColorScriptHashtable {
2+
param([System.Collections.IDictionary]$Source)
3+
4+
if (-not $Source) {
5+
return @{}
6+
}
7+
8+
$clone = @{}
9+
foreach ($key in $Source.Keys) {
10+
$value = $Source[$key]
11+
switch ($true) {
12+
{ $value -is [System.Collections.IDictionary] } {
13+
$clone[$key] = Copy-ColorScriptHashtable $value
14+
break
15+
}
16+
{ $value -is [System.Array] } {
17+
$clone[$key] = $value.Clone()
18+
break
19+
}
20+
{ $value -is [System.ICloneable] -and $value -isnot [string] } {
21+
$clone[$key] = $value.Clone()
22+
break
23+
}
24+
{ $value -is [System.Collections.IEnumerable] -and $value -isnot [string] } {
25+
$buffer = New-Object System.Collections.Generic.List[object]
26+
foreach ($item in $value) {
27+
$null = $buffer.Add($item)
28+
}
29+
$clone[$key] = $buffer.ToArray()
30+
break
31+
}
32+
default {
33+
$clone[$key] = $value
34+
}
35+
}
36+
}
37+
38+
return $clone
39+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
$script:DefaultAutoCategoryRules = @(
2+
[pscustomobject]@{
3+
Category = 'System'
4+
Tags = @('System', 'Utility')
5+
Patterns = @(
6+
'^(00default|alpha)$',
7+
'^ansi-palette$',
8+
'^awk-rgb-test$',
9+
'^colortest(-slim)?$',
10+
'^colorbars$',
11+
'^colorview$',
12+
'^colorwheel$',
13+
'^(A{6}|O{6})$',
14+
'^nerd-font-(glyphs|test)$',
15+
'^rgb-spectrum$',
16+
'^RGB-Wave(-Shifted)?$',
17+
'^spectrum(-flames)?$',
18+
'^terminal-benchmark$',
19+
'^text-styles$',
20+
'^unicode-showcase$',
21+
'^gradient-test$'
22+
)
23+
}
24+
[pscustomobject]@{
25+
Category = 'TerminalThemes'
26+
Tags = @('Terminal', 'Theme')
27+
Patterns = @('^terminal($|-.*)')
28+
}
29+
[pscustomobject]@{
30+
Category = 'Logos'
31+
Tags = @('Logo')
32+
Patterns = @('arch', 'debian', 'manjaro', 'kaisen', 'tux', 'xmonad', 'suckless', 'android', 'apple', 'windows', 'ubuntu', 'pinguco', 'crunchbang', 'amiga')
33+
}
34+
[pscustomobject]@{
35+
Category = 'Gaming'
36+
Tags = @('Gaming', 'PopCulture')
37+
Patterns = @('doom', 'pacman', 'space-invaders', 'tiefighter', 'rally-x', 'tanks', 'guns', 'pukeskull', 'rupees', 'unowns', 'jangofett', 'darthvader')
38+
}
39+
[pscustomobject]@{
40+
Category = 'ASCIIArt'
41+
Tags = @('ASCIIArt')
42+
Patterns = @('cats', 'crabs', 'crowns', 'elfman', 'faces', '^hearts[0-9]*$', 'kevin-woods', 'monster', '^mouseface', 'pinguco', '^thebat', 'thisisfine', '^welcome-', 'ghosts', 'bears', 'hedgehogs', '^tvs$', 'pukeskull')
43+
}
44+
[pscustomobject]@{
45+
Category = 'Physics'
46+
Tags = @('Physics')
47+
Patterns = @('boids', 'cyclone', 'domain', '\bdla\b', '\bdna\b', 'lightning', 'nbody', 'particle', 'perlin', 'plasma', 'sandpile', '\bsdf\b', 'solar-system', 'verlet', 'waveform', 'wavelet', 'wave-interference', 'wave-pattern', 'vector-streams', 'vortex', 'orbit', 'field', 'life', 'langton', 'electrostatic')
48+
}
49+
[pscustomobject]@{
50+
Category = 'Nature'
51+
Tags = @('Nature')
52+
Patterns = @('aurora', 'nebula', 'galaxy', 'forest', 'crystal', 'fern', 'dunes', 'twilight', 'starlit', 'cloud', 'horizon', 'cosmic', 'enchanted')
53+
}
54+
[pscustomobject]@{
55+
Category = 'Mathematical'
56+
Tags = @('Mathematical')
57+
Patterns = @('apollonian', 'barnsley', 'binary-tree', 'clifford', 'fourier', 'fractal', 'hilbert', 'koch', 'lissajous', 'mandelbrot', 'newton', 'penrose', 'pythagorean', 'quasicrystal', 'rossler', 'sierpinski', 'circle-packing', 'lorenz', 'julia', 'lsystem', 'voronoi', 'iso-cubes')
58+
}
59+
[pscustomobject]@{
60+
Category = 'Artistic'
61+
Tags = @('Artistic')
62+
Patterns = @('braid', 'chromatic', 'chrono', 'city', 'ember', 'kaleidoscope', 'mandala', 'mosaic', 'prismatic', 'midnight', 'illumina', 'inkblot', 'pixel', 'sunburst', 'fade', 'starlit', 'twilight', 'rainbow', 'matrix')
63+
}
64+
[pscustomobject]@{
65+
Category = 'Patterns'
66+
Tags = @('Pattern')
67+
Patterns = @('bars?', 'block', 'blok', 'grid', 'maze', 'spiral', 'wave', 'zigzag', 'tile', 'lattice', 'hex', 'ring', 'polygon', 'prism', 'tessell', 'iso', 'quasicrystal', 'rail', 'pane', 'truchet', 'pattern', 'panes', 'rails', 'circle', 'square', 'triangles', 'gradient', 'voronoi', 'radial', '^six$')
68+
}
69+
)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
function Get-CachedOutput {
2+
<#
3+
.SYNOPSIS
4+
Retrieves cached output for a colorscript if available and valid.
5+
#>
6+
[CmdletBinding()]
7+
[OutputType([pscustomobject])]
8+
param(
9+
[Parameter(Mandatory)]
10+
[string]$ScriptPath
11+
)
12+
13+
if ([string]::IsNullOrWhiteSpace($ScriptPath)) {
14+
return [pscustomobject]@{
15+
Available = $false
16+
CacheFile = $null
17+
Content = ''
18+
LastWriteTime = $null
19+
}
20+
}
21+
22+
if (-not $script:CacheDir) {
23+
try {
24+
Initialize-CacheDirectory
25+
}
26+
catch {
27+
Write-Verbose "Initialize-CacheDirectory failed: $($_.Exception.Message)"
28+
}
29+
}
30+
31+
if (-not $script:CacheDir) {
32+
return [pscustomobject]@{
33+
Available = $false
34+
CacheFile = $null
35+
Content = ''
36+
LastWriteTime = $null
37+
}
38+
}
39+
40+
$scriptName = [System.IO.Path]::GetFileNameWithoutExtension($ScriptPath)
41+
$cacheFile = Join-Path -Path $script:CacheDir -ChildPath ("{0}.cache" -f $scriptName)
42+
43+
try {
44+
$scriptFileExists = $false
45+
try {
46+
$scriptFileExists = & $script:FileExistsDelegate $ScriptPath
47+
}
48+
catch {
49+
Write-Verbose "Unable to verify script existence for ${ScriptPath}: $($_.Exception.Message)"
50+
return [pscustomobject]@{
51+
Available = $false
52+
CacheFile = $null
53+
Content = ''
54+
LastWriteTime = $null
55+
}
56+
}
57+
58+
if (-not $scriptFileExists) {
59+
return [pscustomobject]@{
60+
Available = $false
61+
CacheFile = $null
62+
Content = ''
63+
LastWriteTime = $null
64+
}
65+
}
66+
67+
if (-not (Test-Path -LiteralPath $cacheFile)) {
68+
return [pscustomobject]@{
69+
Available = $false
70+
CacheFile = $cacheFile
71+
Content = ''
72+
LastWriteTime = $null
73+
}
74+
}
75+
76+
$scriptLastWrite = & $script:FileGetLastWriteTimeUtcDelegate $ScriptPath
77+
$cacheLastWrite = & $script:FileGetLastWriteTimeUtcDelegate $cacheFile
78+
79+
if ($scriptLastWrite -gt $cacheLastWrite) {
80+
return [pscustomobject]@{
81+
Available = $false
82+
CacheFile = $cacheFile
83+
Content = ''
84+
LastWriteTime = $cacheLastWrite
85+
}
86+
}
87+
88+
$content = & $script:FileReadAllTextDelegate $cacheFile $script:Utf8NoBomEncoding
89+
90+
return [pscustomobject]@{
91+
Available = $true
92+
CacheFile = $cacheFile
93+
Content = $content
94+
LastWriteTime = $cacheLastWrite
95+
}
96+
}
97+
catch {
98+
Write-Verbose "Cache read error for $ScriptPath : $($_.Exception.Message)"
99+
return [pscustomobject]@{
100+
Available = $false
101+
CacheFile = $cacheFile
102+
Content = ''
103+
LastWriteTime = $null
104+
}
105+
}
106+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
function Get-ColorScriptAnsiSequence {
2+
param([string]$Color)
3+
4+
if ([string]::IsNullOrWhiteSpace($Color)) {
5+
return $null
6+
}
7+
8+
switch ($Color.ToLowerInvariant()) {
9+
'cyan' { return "${([char]27)}[36m" }
10+
'yellow' { return "${([char]27)}[33m" }
11+
'green' { return "${([char]27)}[32m" }
12+
'magenta' { return "${([char]27)}[35m" }
13+
'darkgray' { return "${([char]27)}[90m" }
14+
'red' { return "${([char]27)}[31m" }
15+
'blue' { return "${([char]27)}[34m" }
16+
default { return $null }
17+
}
18+
}

0 commit comments

Comments
 (0)