shrinker-ai 0.3.3 → 0.4.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.
@@ -1,113 +1,161 @@
1
- param(
2
- [switch]$Local,
3
- [string]$PackageName = "shrinker-ai",
4
- [string]$Registry = "https://registry.npmjs.org",
5
- [string]$Version,
6
- [switch]$SkipNpmInstall,
7
- [switch]$SkipBuild,
8
- [switch]$SkipLink,
9
- [switch]$EnableProfileRouting,
10
- [switch]$SkipProfile,
11
- [switch]$SkipAgentRules,
12
- [switch]$CopilotOnly,
13
- [switch]$ClaudeOnly,
14
- [string]$ProfilePath = $PROFILE,
15
- [int]$StepOffset = 0
16
- )
17
-
18
- $ErrorActionPreference = "Stop"
19
- $InstallStep = $StepOffset
20
-
21
- function Write-InstallStep {
22
- param([string]$Emoji, [string]$Message)
23
- $script:InstallStep++
24
- Write-Host "$($script:InstallStep). $Emoji $Message"
25
- }
26
-
27
- if ($EnableProfileRouting -and $SkipProfile) { throw "Use either -EnableProfileRouting or -SkipProfile, not both." }
28
- if ($CopilotOnly -and $ClaudeOnly) { throw "Use either -CopilotOnly or -ClaudeOnly, not both." }
29
-
30
- function Set-AgentRules {
31
- param([string]$Body)
32
- $start = "<!-- shrinker agent rules start -->"
33
- $end = "<!-- shrinker agent rules end -->"
34
- foreach ($target in @((Join-Path $HOME ".copilot\copilot-instructions.md"), (Join-Path $HOME ".claude\CLAUDE.md"))) {
35
- if ($ClaudeOnly -and $target.EndsWith(".copilot-instructions.md")) { continue }
36
- if ($CopilotOnly -and $target.EndsWith("CLAUDE.md")) { continue }
37
- New-Item -ItemType Directory -Path (Split-Path $target) -Force | Out-Null
38
- $content = if (Test-Path $target) { Get-Content $target -Raw } else { "" }
39
- $block = "$start`n$Body`n$end"
40
- if ($null -ne $content -and $content.Contains($start) -and $content.Contains($end)) {
41
- $content = [regex]::Replace($content, [regex]::Escape($start) + ".*?" + [regex]::Escape($end), $block, 'Singleline')
42
- Set-Content $target $content
43
- } elseif ($content) { Add-Content $target "`n$block`n" }
44
- else { Set-Content $target "$block`n" }
45
- Write-InstallStep "📄" "Installed managed rules in: $target"
46
- }
47
- }
48
-
49
- function Add-ProfileIntegration {
50
- param([string]$ProfileFile, [string]$IntegrationFile)
51
- if (-not (Test-Path $ProfileFile)) { New-Item -ItemType File -Path $ProfileFile -Force | Out-Null }
52
- $start = "# >>> shrinker integration >>>"
53
- if ((Get-Content $ProfileFile -Raw).Contains($start)) { return }
54
- Add-Content $ProfileFile "`n$start`n. `"$IntegrationFile`"`n# <<< shrinker integration <<<`n"
55
- Write-InstallStep "🔧" "Added shrinker integration block to profile: $ProfileFile"
56
- }
57
-
58
- if (-not $Local) {
59
- $packageSpec = if ($Version) { "$PackageName@$Version" } else { $PackageName }
60
- Write-InstallStep "📦" "Installing $packageSpec from $Registry..."
61
- & npm install --silent --global $packageSpec "--registry=$Registry"
62
- if ($LASTEXITCODE -ne 0) { throw "npm package installation failed." }
63
- Write-InstallStep "🔎" "Locating the installed package..."
64
- $globalRoot = (& npm root --global).Trim()
65
- if (-not $globalRoot) { throw "Could not determine the global npm package directory." }
66
- $packageRoot = Join-Path $globalRoot ($PackageName -replace '/', '\')
67
- $entrypoint = Join-Path $packageRoot "integrations\windows\install.ps1"
68
- if (-not (Test-Path $entrypoint)) { throw "Installed package installer not found: $entrypoint" }
69
- Write-InstallStep "⚙️" "Configuring the installed package..."
70
- & pwsh -ExecutionPolicy Bypass -File $entrypoint -Local -SkipNpmInstall -SkipBuild -SkipLink -EnableProfileRouting:$EnableProfileRouting -SkipProfile:$SkipProfile -SkipAgentRules:$SkipAgentRules -CopilotOnly:$CopilotOnly -ClaudeOnly:$ClaudeOnly -ProfilePath $ProfilePath -StepOffset $InstallStep
71
- if ($LASTEXITCODE -ne 0) { throw "Repository installer failed with exit code $LASTEXITCODE" }
72
- return
73
- }
74
-
75
- $scriptDir = $PSScriptRoot
76
- $repoRoot = (Resolve-Path (Join-Path $scriptDir "..\..")).Path
77
- $templatePath = Join-Path $repoRoot "templates\agent-rules.md"
78
- $integrationPath = (Resolve-Path (Join-Path $scriptDir "shrinker-profile.ps1")).Path
79
- if (-not $SkipAgentRules) {
80
- if (-not (Test-Path $templatePath)) { throw "Agent rules template not found: $templatePath" }
81
- $rulesBody = Get-Content $templatePath -Raw
82
- }
83
-
84
- $versionText = & node -v 2>$null
85
- if (-not $versionText) { throw "Node.js was not found on PATH. Install Node.js 22.13+ first." }
86
- $version = [version]($versionText.TrimStart('v'))
87
- if ($version -lt [version]'22.13.0') { throw "Node.js 22.13+ is required. Found $versionText" }
88
-
89
- Push-Location $repoRoot
90
- try {
91
- Write-InstallStep "📦" "Installing shrinker from: $repoRoot"
92
- if (-not $SkipNpmInstall) {
93
- Write-InstallStep "📥" "Installing dependencies..."
94
- & npm install --silent
95
- if ($LASTEXITCODE -ne 0) { throw "npm install failed." }
96
- }
97
- if (-not $SkipBuild) {
98
- Write-InstallStep "🏗️" "Building shrinker..."
99
- & npm run build --silent
100
- if ($LASTEXITCODE -ne 0) { throw "npm run build failed." }
101
- }
102
- if (-not $SkipLink) {
103
- Write-InstallStep "🔗" "Linking shrinker globally..."
104
- & npm link --silent
105
- if ($LASTEXITCODE -ne 0) { throw "npm link failed." }
106
- }
107
- } finally { Pop-Location }
108
-
109
- if (-not $SkipProfile -and $EnableProfileRouting) { Add-ProfileIntegration $ProfilePath $integrationPath }
110
- if (-not $SkipAgentRules) { Set-AgentRules $rulesBody }
111
- Write-InstallStep "✅" "Install complete."
112
- Write-Host " "
113
- Write-Host "💡 Try: shrinker help"
1
+ param(
2
+ [switch]$Local,
3
+ [string]$PackageName = "shrinker-ai",
4
+ [string]$Registry = "https://registry.npmjs.org",
5
+ [string]$Version,
6
+ [switch]$SkipNpmInstall,
7
+ [switch]$SkipBuild,
8
+ [switch]$SkipLink,
9
+ [switch]$EnableProfileRouting,
10
+ [switch]$SkipProfile,
11
+ [switch]$SkipAgentRules,
12
+ [switch]$CopilotOnly,
13
+ [switch]$ClaudeOnly,
14
+ [switch]$EnableUncoveredTracking,
15
+ [switch]$DisableUncoveredTracking,
16
+ [string]$ProfilePath = $PROFILE,
17
+ [string]$ConfigPath = $(if ($env:SHRINKER_CONFIG_PATH) { $env:SHRINKER_CONFIG_PATH } else { Join-Path $HOME ".shrinker/config" }),
18
+ [int]$StepOffset = 0
19
+ )
20
+
21
+ $ErrorActionPreference = "Stop"
22
+ $InstallStep = $StepOffset
23
+
24
+ function Write-InstallStep {
25
+ param([string]$Emoji, [string]$Message)
26
+ $script:InstallStep++
27
+ Write-Host "$($script:InstallStep). $Emoji $Message"
28
+ }
29
+
30
+ if ($EnableProfileRouting -and $SkipProfile) { throw "Use either -EnableProfileRouting or -SkipProfile, not both." }
31
+ if ($CopilotOnly -and $ClaudeOnly) { throw "Use either -CopilotOnly or -ClaudeOnly, not both." }
32
+ if ($EnableUncoveredTracking -and $DisableUncoveredTracking) { throw "Use either -EnableUncoveredTracking or -DisableUncoveredTracking, not both." }
33
+
34
+ # Prompt only on a real console; non-interactive installs keep tracking on and leave the profile alone.
35
+ $IsInteractive = -not [System.Console]::IsInputRedirected -and $Host.UI.RawUI -ne $null
36
+
37
+ function Read-YesNo {
38
+ param([string]$Question, [bool]$Default)
39
+ $hint = if ($Default) { "[Y/n]" } else { "[y/N]" }
40
+ $reply = (Read-Host " $Question $hint").Trim().ToLowerInvariant()
41
+ switch ($reply) {
42
+ { $_ -in @("y", "yes") } { return $true }
43
+ { $_ -in @("n", "no") } { return $false }
44
+ default { return $Default }
45
+ }
46
+ }
47
+
48
+ if ($EnableUncoveredTracking) { $TrackUncovered = $true }
49
+ elseif ($DisableUncoveredTracking) { $TrackUncovered = $false }
50
+ elseif ($IsInteractive) {
51
+ Write-InstallStep "❓" "Track uncovered commands? Records which unfiltered commands cost the most tokens."
52
+ $TrackUncovered = Read-YesNo "Enable uncovered-command tracking?" $true
53
+ }
54
+ else { $TrackUncovered = $true }
55
+
56
+ if ($IsInteractive -and -not $SkipProfile -and -not $EnableProfileRouting) {
57
+ Write-InstallStep "❓" "Route wrapped commands through shrinker automatically? Adds a line to $ProfilePath."
58
+ $EnableProfileRouting = Read-YesNo "Enable automatic shell routing?" $false
59
+ }
60
+
61
+ function Set-ConfigValue {
62
+ param([string]$Key, [string]$Value)
63
+ $directory = Split-Path -Parent $ConfigPath
64
+ if ($directory -and -not (Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Force -Path $directory | Out-Null }
65
+ $lines = @()
66
+ if (Test-Path -LiteralPath $ConfigPath) {
67
+ $lines = @(Get-Content -LiteralPath $ConfigPath | Where-Object { $_ -notmatch "^\s*$Key\s*=" })
68
+ }
69
+ $lines += "$Key=$Value"
70
+ Set-Content -LiteralPath $ConfigPath -Value $lines -Encoding utf8
71
+ Write-InstallStep "⚙️" "Set $Key=$Value in: $ConfigPath"
72
+ }
73
+
74
+ function Set-AgentRules {
75
+ param([string]$Body)
76
+ $start = "<!-- shrinker agent rules start -->"
77
+ $end = "<!-- shrinker agent rules end -->"
78
+ foreach ($target in @((Join-Path $HOME ".copilot\copilot-instructions.md"), (Join-Path $HOME ".claude\CLAUDE.md"))) {
79
+ if ($ClaudeOnly -and $target.EndsWith(".copilot-instructions.md")) { continue }
80
+ if ($CopilotOnly -and $target.EndsWith("CLAUDE.md")) { continue }
81
+ New-Item -ItemType Directory -Path (Split-Path $target) -Force | Out-Null
82
+ $content = if (Test-Path $target) { Get-Content $target -Raw } else { "" }
83
+ $block = "$start`n$Body`n$end"
84
+ if ($null -ne $content -and $content.Contains($start) -and $content.Contains($end)) {
85
+ $content = [regex]::Replace($content, [regex]::Escape($start) + ".*?" + [regex]::Escape($end), $block, 'Singleline')
86
+ Set-Content $target $content
87
+ } elseif ($content) { Add-Content $target "`n$block`n" }
88
+ else { Set-Content $target "$block`n" }
89
+ Write-InstallStep "📄" "Installed managed rules in: $target"
90
+ }
91
+ }
92
+
93
+ function Add-ProfileIntegration {
94
+ param([string]$ProfileFile, [string]$IntegrationFile)
95
+ if (-not (Test-Path $ProfileFile)) { New-Item -ItemType File -Path $ProfileFile -Force | Out-Null }
96
+ $start = "# >>> shrinker integration >>>"
97
+ if ((Get-Content $ProfileFile -Raw).Contains($start)) { return }
98
+ Add-Content $ProfileFile "`n$start`n. `"$IntegrationFile`"`n# <<< shrinker integration <<<`n"
99
+ Write-InstallStep "🔧" "Added shrinker integration block to profile: $ProfileFile"
100
+ }
101
+
102
+ if (-not $Local) {
103
+ $packageSpec = if ($Version) { "$PackageName@$Version" } else { $PackageName }
104
+ Write-InstallStep "📦" "Installing $packageSpec from $Registry..."
105
+ & npm install --silent --global $packageSpec "--registry=$Registry"
106
+ if ($LASTEXITCODE -ne 0) { throw "npm package installation failed." }
107
+ Write-InstallStep "🔎" "Locating the installed package..."
108
+ $globalRoot = (& npm root --global).Trim()
109
+ if (-not $globalRoot) { throw "Could not determine the global npm package directory." }
110
+ $packageRoot = Join-Path $globalRoot ($PackageName -replace '/', '\')
111
+ $entrypoint = Join-Path $packageRoot "integrations\windows\install.ps1"
112
+ if (-not (Test-Path $entrypoint)) { throw "Installed package installer not found: $entrypoint" }
113
+ Write-InstallStep "⚙️" "Configuring the installed package..."
114
+ & pwsh -ExecutionPolicy Bypass -File $entrypoint -Local -SkipNpmInstall -SkipBuild -SkipLink -EnableProfileRouting:$EnableProfileRouting -SkipProfile:$SkipProfile -SkipAgentRules:$SkipAgentRules -CopilotOnly:$CopilotOnly -ClaudeOnly:$ClaudeOnly -EnableUncoveredTracking:$TrackUncovered -DisableUncoveredTracking:(-not $TrackUncovered) -ProfilePath $ProfilePath -ConfigPath $ConfigPath -StepOffset $InstallStep
115
+ if ($LASTEXITCODE -ne 0) { throw "Repository installer failed with exit code $LASTEXITCODE" }
116
+ return
117
+ }
118
+
119
+ $scriptDir = $PSScriptRoot
120
+ $repoRoot = (Resolve-Path (Join-Path $scriptDir "..\..")).Path
121
+ $templatePath = Join-Path $repoRoot "templates\agent-rules.md"
122
+ $integrationPath = (Resolve-Path (Join-Path $scriptDir "shrinker-profile.ps1")).Path
123
+ if (-not $SkipAgentRules) {
124
+ if (-not (Test-Path $templatePath)) { throw "Agent rules template not found: $templatePath" }
125
+ $rulesBody = Get-Content $templatePath -Raw
126
+ }
127
+
128
+ $versionText = & node -v 2>$null
129
+ if (-not $versionText) { throw "Node.js was not found on PATH. Install Node.js 22.13+ first." }
130
+ $version = [version]($versionText.TrimStart('v'))
131
+ if ($version -lt [version]'22.13.0') { throw "Node.js 22.13+ is required. Found $versionText" }
132
+
133
+ Push-Location $repoRoot
134
+ try {
135
+ Write-InstallStep "📦" "Installing shrinker from: $repoRoot"
136
+ if (-not $SkipNpmInstall) {
137
+ Write-InstallStep "📥" "Installing dependencies..."
138
+ & npm install --silent
139
+ if ($LASTEXITCODE -ne 0) { throw "npm install failed." }
140
+ }
141
+ if (-not $SkipBuild) {
142
+ Write-InstallStep "🏗️" "Building shrinker..."
143
+ $previousQuiet = $env:SHRINKER_BUILD_QUIET
144
+ $env:SHRINKER_BUILD_QUIET = "1"
145
+ try { & npm run build --silent }
146
+ finally { $env:SHRINKER_BUILD_QUIET = $previousQuiet }
147
+ if ($LASTEXITCODE -ne 0) { throw "npm run build failed." }
148
+ }
149
+ if (-not $SkipLink) {
150
+ Write-InstallStep "🔗" "Linking shrinker globally..."
151
+ & npm link --silent
152
+ if ($LASTEXITCODE -ne 0) { throw "npm link failed." }
153
+ }
154
+ } finally { Pop-Location }
155
+
156
+ Set-ConfigValue "SHRINKER_TRACK_UNCOVERED" $(if ($TrackUncovered) { "1" } else { "0" })
157
+ if (-not $SkipProfile -and $EnableProfileRouting) { Add-ProfileIntegration $ProfilePath $integrationPath }
158
+ if (-not $SkipAgentRules) { Set-AgentRules $rulesBody }
159
+ Write-InstallStep "✅" "Install complete."
160
+ Write-Host " "
161
+ Write-Host "💡 Try: shrinker help"
@@ -1,136 +1,206 @@
1
- $global:ShrinkPowerShellRules = @{
2
- git = @("status", "diff", "log", "show", "reflog", "branch", "tag", "stash")
3
- npm = @("test", "t", "install", "i", "ci", "ls", "list")
4
- docker = @("ps", "logs", "images", "compose")
5
- kubectl = @("get", "describe", "logs")
6
- gh = @("pr", "issue", "run")
7
- rg = @("*")
8
- find = @("*")
9
- tail = @("*")
10
- cat = @("*")
11
- ls = @("*")
12
- dir = @("*")
13
- }
14
-
15
- $global:ShrinkOptionValueFlags = @{
16
- git = @("-C", "-c", "--git-dir", "--work-tree", "--namespace")
17
- npm = @("--prefix", "--cache", "--registry", "--workspace", "--userconfig", "-w", "-C")
18
- docker = @("-H", "--host", "--context", "--config")
19
- kubectl = @("-n", "--namespace", "-o", "--output", "--context", "--kubeconfig", "--cluster", "--user")
20
- gh = @("-R", "--repo")
21
- }
22
-
23
- $global:ShrinkNativePaths = @{}
24
- foreach ($name in @("git", "npm", "docker", "kubectl", "gh", "rg", "find", "tail")) {
25
- $command = Get-Command $name -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
26
- if ($command) {
27
- $global:ShrinkNativePaths[$name] = $command.Source
28
- }
29
- }
30
-
31
- function Get-ShrinkSubcommand {
32
- param(
33
- [string]$CommandName,
34
- [object[]]$Arguments
35
- )
36
-
37
- $valueFlags = @($global:ShrinkOptionValueFlags[$CommandName])
38
- for ($i = 0; $i -lt $Arguments.Count; $i += 1) {
39
- $part = [string]$Arguments[$i]
40
- if (-not $part) { continue }
41
- if ($valueFlags -contains $part) {
42
- $i += 1
43
- continue
44
- }
45
- if ($part.StartsWith("-")) { continue }
46
- return $part.ToLowerInvariant()
47
- }
48
-
49
- return ""
50
- }
51
-
52
- function Use-ShrinkRouting {
53
- param(
54
- [string]$CommandName,
55
- [object[]]$Arguments
56
- )
57
-
58
- $allowlist = @($global:ShrinkPowerShellRules[$CommandName])
59
- if ($allowlist.Count -eq 0) {
60
- return $false
61
- }
62
- if ($allowlist -contains "*") {
63
- return $true
64
- }
65
-
66
- $subcommand = Get-ShrinkSubcommand -CommandName $CommandName -Arguments $Arguments
67
- return $allowlist -contains $subcommand
68
- }
69
-
70
- function Get-ShrinkCommandPath {
71
- $candidates = Get-Command shrinker -All -ErrorAction SilentlyContinue
72
- if (-not $candidates) {
73
- return $null
74
- }
75
-
76
- foreach ($candidate in $candidates) {
77
- $source = [string]$candidate.Source
78
- if ($source -and $source.ToLowerInvariant().EndsWith(".cmd")) {
79
- return $source
80
- }
81
- }
82
- foreach ($candidate in $candidates) {
83
- $source = [string]$candidate.Source
84
- if ($source -and $source.ToLowerInvariant().EndsWith(".exe")) {
85
- return $source
86
- }
87
- }
88
- foreach ($candidate in $candidates) {
89
- $source = [string]$candidate.Source
90
- if ($source -and $source.ToLowerInvariant().EndsWith(".ps1")) {
91
- return $source
92
- }
93
- }
94
-
95
- return [string]($candidates | Select-Object -First 1).Source
96
- }
97
-
98
- function Invoke-ShrinkOrNative {
99
- param(
100
- [string]$CommandName,
101
- [object[]]$Arguments
102
- )
103
-
104
- $routeToShrink = Use-ShrinkRouting -CommandName $CommandName -Arguments $Arguments
105
- $shrinkCommand = Get-ShrinkCommandPath
106
-
107
- if ($routeToShrink -and $shrinkCommand) {
108
- & $shrinkCommand $CommandName @Arguments
109
- return
110
- }
111
-
112
- $nativePath = $global:ShrinkNativePaths[$CommandName]
113
- if ($nativePath) {
114
- & $nativePath @Arguments
115
- return
116
- }
117
-
118
- switch ($CommandName) {
119
- "cat" { Microsoft.PowerShell.Management\Get-Content @Arguments; return }
120
- "ls" { Microsoft.PowerShell.Management\Get-ChildItem @Arguments; return }
121
- "dir" { Microsoft.PowerShell.Management\Get-ChildItem @Arguments; return }
122
- default { throw "No native fallback found for '$CommandName'." }
123
- }
124
- }
125
-
126
- function global:git { Invoke-ShrinkOrNative -CommandName "git" -Arguments $args }
127
- function global:npm { Invoke-ShrinkOrNative -CommandName "npm" -Arguments $args }
128
- function global:docker { Invoke-ShrinkOrNative -CommandName "docker" -Arguments $args }
129
- function global:kubectl { Invoke-ShrinkOrNative -CommandName "kubectl" -Arguments $args }
130
- function global:gh { Invoke-ShrinkOrNative -CommandName "gh" -Arguments $args }
131
- function global:rg { Invoke-ShrinkOrNative -CommandName "rg" -Arguments $args }
132
- function global:find { Invoke-ShrinkOrNative -CommandName "find" -Arguments $args }
133
- function global:tail { Invoke-ShrinkOrNative -CommandName "tail" -Arguments $args }
134
- function global:cat { Invoke-ShrinkOrNative -CommandName "cat" -Arguments $args }
135
- function global:ls { Invoke-ShrinkOrNative -CommandName "ls" -Arguments $args }
136
- function global:dir { Invoke-ShrinkOrNative -CommandName "dir" -Arguments $args }
1
+ $global:ShrinkPowerShellRules = @{
2
+ git = @("status", "diff", "log", "show", "reflog", "branch", "tag", "stash")
3
+ npm = @("test", "t", "install", "i", "ci", "ls", "list")
4
+ docker = @("ps", "logs", "images", "compose")
5
+ kubectl = @("get", "describe", "logs")
6
+ gh = @("pr", "issue", "run")
7
+ rg = @("*")
8
+ find = @("*")
9
+ tail = @("*")
10
+ cat = @("*")
11
+ ls = @("*")
12
+ dir = @("*")
13
+ }
14
+
15
+ $global:ShrinkOptionValueFlags = @{
16
+ git = @("-C", "-c", "--git-dir", "--work-tree", "--namespace")
17
+ npm = @("--prefix", "--cache", "--registry", "--workspace", "--userconfig", "-w", "-C")
18
+ docker = @("-H", "--host", "--context", "--config")
19
+ kubectl = @("-n", "--namespace", "-o", "--output", "--context", "--kubeconfig", "--cluster", "--user")
20
+ gh = @("-R", "--repo")
21
+ }
22
+
23
+ $global:ShrinkNativePaths = @{}
24
+ foreach ($name in @("git", "npm", "docker", "kubectl", "gh", "rg", "find", "tail")) {
25
+ $command = Get-Command $name -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
26
+ if ($command) {
27
+ $global:ShrinkNativePaths[$name] = $command.Source
28
+ }
29
+ }
30
+
31
+ function Get-ShrinkSubcommand {
32
+ param(
33
+ [string]$CommandName,
34
+ [object[]]$Arguments
35
+ )
36
+
37
+ $valueFlags = @($global:ShrinkOptionValueFlags[$CommandName])
38
+ for ($i = 0; $i -lt $Arguments.Count; $i += 1) {
39
+ $part = [string]$Arguments[$i]
40
+ if (-not $part) { continue }
41
+ if ($valueFlags -contains $part) {
42
+ $i += 1
43
+ continue
44
+ }
45
+ if ($part.StartsWith("-")) { continue }
46
+ return $part.ToLowerInvariant()
47
+ }
48
+
49
+ return ""
50
+ }
51
+
52
+ function Use-ShrinkRouting {
53
+ param(
54
+ [string]$CommandName,
55
+ [object[]]$Arguments
56
+ )
57
+
58
+ $allowlist = @($global:ShrinkPowerShellRules[$CommandName])
59
+ if ($allowlist.Count -eq 0) {
60
+ return $false
61
+ }
62
+ if ($allowlist -contains "*") {
63
+ return $true
64
+ }
65
+
66
+ $subcommand = Get-ShrinkSubcommand -CommandName $CommandName -Arguments $Arguments
67
+ return $allowlist -contains $subcommand
68
+ }
69
+
70
+ function Get-ShrinkCommandPath {
71
+ $candidates = Get-Command shrinker -All -ErrorAction SilentlyContinue
72
+ if (-not $candidates) {
73
+ return $null
74
+ }
75
+
76
+ foreach ($candidate in $candidates) {
77
+ $source = [string]$candidate.Source
78
+ if ($source -and $source.ToLowerInvariant().EndsWith(".cmd")) {
79
+ return $source
80
+ }
81
+ }
82
+ foreach ($candidate in $candidates) {
83
+ $source = [string]$candidate.Source
84
+ if ($source -and $source.ToLowerInvariant().EndsWith(".exe")) {
85
+ return $source
86
+ }
87
+ }
88
+ foreach ($candidate in $candidates) {
89
+ $source = [string]$candidate.Source
90
+ if ($source -and $source.ToLowerInvariant().EndsWith(".ps1")) {
91
+ return $source
92
+ }
93
+ }
94
+
95
+ return [string]($candidates | Select-Object -First 1).Source
96
+ }
97
+
98
+ # Resolved once at load so wrapped commands never pay for reading the config file.
99
+ $script:ShrinkTrackUncoveredDefault = ""
100
+ $script:ShrinkConfigPath = if ($env:SHRINKER_CONFIG_PATH) { $env:SHRINKER_CONFIG_PATH } else { Join-Path $HOME ".shrinker/config" }
101
+ if (Test-Path -LiteralPath $script:ShrinkConfigPath) {
102
+ foreach ($line in Get-Content -LiteralPath $script:ShrinkConfigPath) {
103
+ $stripped = ($line -split "#", 2)[0].Trim()
104
+ if ($stripped -match '^\s*SHRINKER_TRACK_UNCOVERED\s*=\s*(.*)$') {
105
+ $script:ShrinkTrackUncoveredDefault = $Matches[1].Trim()
106
+ }
107
+ }
108
+ }
109
+
110
+ function Test-ShrinkTrackingEnabled {
111
+ $value = [string]$env:SHRINKER_TRACK_UNCOVERED
112
+ if (-not $value) { $value = $script:ShrinkTrackUncoveredDefault }
113
+ if (-not $value) { return $false }
114
+ return @("1", "true", "yes") -contains $value.Trim().ToLowerInvariant()
115
+ }
116
+
117
+ function Write-ShrinkUncovered {
118
+ param(
119
+ [string]$CommandName,
120
+ [object[]]$Arguments,
121
+ [int]$Bytes,
122
+ [int]$ExitCode
123
+ )
124
+
125
+ if (-not (Test-ShrinkTrackingEnabled)) { return }
126
+ $shrinkCommand = Get-ShrinkCommandPath
127
+ if (-not $shrinkCommand) { return }
128
+
129
+ $trackArgs = @("track", "--executable", $CommandName, "--bytes", $Bytes, "--exit-code", $ExitCode)
130
+ $subcommand = Get-ShrinkSubcommand -CommandName $CommandName -Arguments $Arguments
131
+ if ($subcommand) {
132
+ $trackArgs += @("--subcommand", $subcommand)
133
+ }
134
+
135
+ try {
136
+ Start-Process -FilePath $shrinkCommand -ArgumentList $trackArgs -WindowStyle Hidden -ErrorAction SilentlyContinue | Out-Null
137
+ } catch {}
138
+ }
139
+
140
+ function Invoke-ShrinkNativeTracked {
141
+ param(
142
+ [scriptblock]$Invoke,
143
+ [string]$CommandName,
144
+ [object[]]$Arguments
145
+ )
146
+
147
+ if (-not (Test-ShrinkTrackingEnabled)) {
148
+ & $Invoke
149
+ return
150
+ }
151
+
152
+ # Only measure output volume when stdout is redirected (the agent case). Interactive
153
+ # consoles stream the native output untouched so paging and colours still work.
154
+ if (-not [Console]::IsOutputRedirected) {
155
+ & $Invoke
156
+ Write-ShrinkUncovered -CommandName $CommandName -Arguments $Arguments -Bytes 0 -ExitCode ([int]$LASTEXITCODE)
157
+ return
158
+ }
159
+
160
+ $output = & $Invoke | Out-String -Stream
161
+ $exitCode = [int]$LASTEXITCODE
162
+ $text = ($output -join [Environment]::NewLine)
163
+ $bytes = [System.Text.Encoding]::UTF8.GetByteCount($text)
164
+ $output | ForEach-Object { $_ }
165
+ Write-ShrinkUncovered -CommandName $CommandName -Arguments $Arguments -Bytes $bytes -ExitCode $exitCode
166
+ }
167
+
168
+ function Invoke-ShrinkOrNative {
169
+ param(
170
+ [string]$CommandName,
171
+ [object[]]$Arguments
172
+ )
173
+
174
+ $routeToShrink = Use-ShrinkRouting -CommandName $CommandName -Arguments $Arguments
175
+ $shrinkCommand = Get-ShrinkCommandPath
176
+
177
+ if ($routeToShrink -and $shrinkCommand) {
178
+ & $shrinkCommand $CommandName @Arguments
179
+ return
180
+ }
181
+
182
+ $nativePath = $global:ShrinkNativePaths[$CommandName]
183
+ if ($nativePath) {
184
+ Invoke-ShrinkNativeTracked -CommandName $CommandName -Arguments $Arguments -Invoke { & $nativePath @Arguments }
185
+ return
186
+ }
187
+
188
+ switch ($CommandName) {
189
+ "cat" { Invoke-ShrinkNativeTracked -CommandName $CommandName -Arguments $Arguments -Invoke { Microsoft.PowerShell.Management\Get-Content @Arguments }; return }
190
+ "ls" { Invoke-ShrinkNativeTracked -CommandName $CommandName -Arguments $Arguments -Invoke { Microsoft.PowerShell.Management\Get-ChildItem @Arguments }; return }
191
+ "dir" { Invoke-ShrinkNativeTracked -CommandName $CommandName -Arguments $Arguments -Invoke { Microsoft.PowerShell.Management\Get-ChildItem @Arguments }; return }
192
+ default { throw "No native fallback found for '$CommandName'." }
193
+ }
194
+ }
195
+
196
+ function global:git { Invoke-ShrinkOrNative -CommandName "git" -Arguments $args }
197
+ function global:npm { Invoke-ShrinkOrNative -CommandName "npm" -Arguments $args }
198
+ function global:docker { Invoke-ShrinkOrNative -CommandName "docker" -Arguments $args }
199
+ function global:kubectl { Invoke-ShrinkOrNative -CommandName "kubectl" -Arguments $args }
200
+ function global:gh { Invoke-ShrinkOrNative -CommandName "gh" -Arguments $args }
201
+ function global:rg { Invoke-ShrinkOrNative -CommandName "rg" -Arguments $args }
202
+ function global:find { Invoke-ShrinkOrNative -CommandName "find" -Arguments $args }
203
+ function global:tail { Invoke-ShrinkOrNative -CommandName "tail" -Arguments $args }
204
+ function global:cat { Invoke-ShrinkOrNative -CommandName "cat" -Arguments $args }
205
+ function global:ls { Invoke-ShrinkOrNative -CommandName "ls" -Arguments $args }
206
+ function global:dir { Invoke-ShrinkOrNative -CommandName "dir" -Arguments $args }