@woosau/dsh-custom-env 0.0.0-stage → 0.1.1

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,150 @@
1
+ <# Per-user PATH ownership for dsh-custom-env (not DeepSeekHarness Desktop). #>
2
+ function Invoke-DshCommandPath {
3
+ param([hashtable]$Request, [string]$EnvironmentKey, [string]$OwnerKey, [string]$MachinePath, [string]$MutexName)
4
+ $ErrorActionPreference = 'Stop'
5
+ function Fail([string]$Code, [string]$Message) {
6
+ $error = [InvalidOperationException]::new($Message)
7
+ $error.Data['code'] = $Code
8
+ throw $error
9
+ }
10
+ function Comparable([string]$Path) {
11
+ $expanded = [Environment]::ExpandEnvironmentVariables($Path.Trim().Trim('"'))
12
+ try {
13
+ if (-not [IO.Path]::IsPathRooted($expanded)) { return $expanded }
14
+ return [IO.Path]::GetFullPath($expanded).TrimEnd('\','/').ToUpperInvariant()
15
+ } catch {
16
+ # Invalid user PATH entries cannot identify the managed directory.
17
+ return $expanded
18
+ }
19
+ }
20
+ $directory = [string]$Request.directory
21
+ if (-not [IO.Path]::IsPathRooted($directory) -or $directory.Contains(';') -or $directory.Contains([char]0)) {
22
+ Fail 'EUNSUPPORTED' 'The command directory cannot be represented in PATH.'
23
+ }
24
+ $directory = [IO.Path]::GetFullPath($directory)
25
+ if ($Request.operation -notin @('inspect','install','remove')) { Fail 'EINVAL' 'Unknown command operation.' }
26
+ $mutex = [Threading.Mutex]::new($false, $MutexName)
27
+ $locked = $false
28
+ try {
29
+ try { $locked = $mutex.WaitOne(5000) } catch [Threading.AbandonedMutexException] { $locked = $true }
30
+ if (-not $locked) { Fail 'EBUSY' 'Another command-management operation is running.' }
31
+ $environment = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($EnvironmentKey)
32
+ $owner = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($OwnerKey)
33
+ try {
34
+ $raw = if ($environment) { $environment.GetValue('Path', $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) } else { $null }
35
+ if ($null -ne $raw -and $raw -isnot [string]) { Fail 'EUNSUPPORTED' 'The user PATH value is not a string.' }
36
+ $kind = if ($null -ne $raw) { $environment.GetValueKind('Path') } else { [Microsoft.Win32.RegistryValueKind]::ExpandString }
37
+ $owned = if ($owner) { $owner.GetValue('Directory', $null) } else { $null }
38
+ $wasAbsent = if ($owner) { $owner.GetValue('PathWasAbsent', 0) -eq 1 } else { $false }
39
+ $retained = if ($owner) { [int]$owner.GetValue('RetainedEntries', 0) } else { 0 }
40
+ } finally {
41
+ if ($environment) { $environment.Dispose() }
42
+ if ($owner) { $owner.Dispose() }
43
+ }
44
+ $snapshot = [ordered]@{ path=$raw; kind=[string]$kind; owned=$owned; absent=$wasAbsent; retained=$retained; directory=$directory; machine=$MachinePath }
45
+ $sha = [Security.Cryptography.SHA256]::Create()
46
+ try { $fingerprint = ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes(($snapshot | ConvertTo-Json -Compress))))).Replace('-','').ToLowerInvariant() }
47
+ finally { $sha.Dispose() }
48
+ if ($Request.operation -ne 'inspect' -and $Request.expected -ne $fingerprint) { Fail 'ESTALE' 'PATH changed after confirmation.' }
49
+ $parts = if ($null -eq $raw) { @() } else { @($raw.Split(';')) }
50
+ $current = Comparable $directory
51
+ $old = if ($owned) { Comparable ([string]$owned) } else { $null }
52
+ $kept = $parts
53
+ if ($old -and ($Request.operation -eq 'install' -or ($Request.operation -eq 'remove' -and $old -eq $current))) {
54
+ $ownedMatches = @($parts | Where-Object { $_ -ceq $owned }).Count
55
+ if ($ownedMatches -gt $retained) {
56
+ $index = [Array]::IndexOf($parts, $owned)
57
+ $kept = @(); for ($i = 0; $i -lt $parts.Count; $i++) { if ($i -ne $index) { $kept += $parts[$i] } }
58
+ }
59
+ }
60
+ if ($Request.operation -eq 'install') {
61
+ if (-not (Test-Path -LiteralPath (Join-Path $directory 'dsh.cmd') -PathType Leaf)) { Fail 'ENOENT' 'The installed launcher is unavailable.' }
62
+ $next = (@($directory) + $kept) -join ';'
63
+ $environment = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey($EnvironmentKey)
64
+ $owner = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey($OwnerKey)
65
+ try {
66
+ $environment.SetValue('Path', $next, $kind)
67
+ $owner.SetValue('Directory', $directory, [Microsoft.Win32.RegistryValueKind]::String)
68
+ $absent = ($null -eq $raw) -or ($owned -and $wasAbsent)
69
+ $owner.SetValue('PathWasAbsent', [int]$absent, [Microsoft.Win32.RegistryValueKind]::DWord)
70
+ $owner.SetValue('RetainedEntries', @($kept | Where-Object { $_ -ceq $directory }).Count, [Microsoft.Win32.RegistryValueKind]::DWord)
71
+ } finally { $environment.Dispose(); $owner.Dispose() }
72
+ $raw = $next
73
+ $owned = $directory
74
+ } elseif ($Request.operation -eq 'remove') {
75
+ if ($kept.Count -ne $parts.Count) {
76
+ $environment = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey($EnvironmentKey)
77
+ try {
78
+ if ($kept.Count -eq 0 -and $wasAbsent) { $environment.DeleteValue('Path', $false); $raw = $null }
79
+ else { $raw = $kept -join ';'; $environment.SetValue('Path', $raw, $kind) }
80
+ } finally { $environment.Dispose() }
81
+ }
82
+ $owner = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($OwnerKey, $true)
83
+ if ($owner -and $old -eq $current) {
84
+ try { $owner.DeleteValue('Directory', $false); $owner.DeleteValue('PathWasAbsent', $false); $owner.DeleteValue('RetainedEntries', $false) }
85
+ finally { $owner.Dispose() }
86
+ $owned = $null
87
+ } elseif ($owner) {
88
+ $owner.Dispose()
89
+ }
90
+ }
91
+ $active = $null
92
+ foreach ($part in (($MachinePath + ';' + [string]$raw).Split(';'))) {
93
+ $path = [Environment]::ExpandEnvironmentVariables($part.Trim().Trim('"'))
94
+ try { $rooted = [IO.Path]::IsPathRooted($path) } catch { continue }
95
+ if (-not $rooted) { continue }
96
+ $extensions = if ($env:PATHEXT) { @($env:PATHEXT.Split(';')) } else { @('.com','.exe','.bat','.cmd') }
97
+ foreach ($extension in (@('.ps1') + $extensions)) {
98
+ try {
99
+ $candidate = Join-Path $path ('dsh' + $extension)
100
+ if (Test-Path -LiteralPath $candidate -PathType Leaf) { $active = $candidate; break }
101
+ } catch {
102
+ # Invalid or unavailable PATH candidates do not prevent checking later entries.
103
+ continue
104
+ }
105
+ }
106
+ if ($active) { break }
107
+ }
108
+ $onPath = @(([string]$raw).Split(';') | Where-Object { (Comparable $_) -eq $current }).Count -gt 0
109
+ return [ordered]@{ fingerprint=$fingerprint; directory=$directory; ownedDirectory=$owned; managed=([bool]$owned -and (Comparable ([string]$owned)) -eq $current); activeCommand=$active; available=($onPath -and (Test-Path -LiteralPath (Join-Path $directory 'dsh.cmd') -PathType Leaf)) }
110
+ } finally {
111
+ if ($locked) { $mutex.ReleaseMutex() }
112
+ $mutex.Dispose()
113
+ }
114
+ }
115
+
116
+ function Send-DshCommandEnvironmentChange {
117
+ try {
118
+ if (-not ('CustomEnvCommandEnvironment' -as [type])) {
119
+ Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public static class CustomEnvCommandEnvironment { [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern IntPtr SendMessageTimeout(IntPtr h, uint m, IntPtr w, string l, uint f, uint t, out IntPtr r); }'
120
+ }
121
+ $ignored = [IntPtr]::Zero
122
+ [void][CustomEnvCommandEnvironment]::SendMessageTimeout([IntPtr]0xffff, 0x1a, [IntPtr]::Zero, 'Environment', 2, 2000, [ref]$ignored)
123
+ } catch {
124
+ # The PATH write is committed. A failed notification only delays other processes observing it.
125
+ return
126
+ }
127
+ }
128
+
129
+ if ($MyInvocation.InvocationName -ne '.') {
130
+ $ErrorActionPreference = 'Stop'
131
+ [Console]::InputEncoding = [Text.UTF8Encoding]::new($false)
132
+ [Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
133
+ try {
134
+ $inputValue = [Console]::In.ReadToEnd() | ConvertFrom-Json
135
+ $request = @{ operation=$inputValue.operation; directory=$inputValue.directory; expected=$inputValue.expected }
136
+ $machine = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey('SYSTEM\CurrentControlSet\Control\Session Manager\Environment')
137
+ try { $machinePath = [string]$machine.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) }
138
+ finally { $machine.Dispose() }
139
+ $sid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
140
+ $state = Invoke-DshCommandPath -Request $request -EnvironmentKey 'Environment' -OwnerKey 'Software\dsh-custom-env\Command' -MachinePath $machinePath -MutexName ('Global\dsh-custom-env.Command.' + $sid)
141
+ if ($request.operation -ne 'inspect') {
142
+ Send-DshCommandEnvironmentChange
143
+ }
144
+ @{ ok=$true; state=$state } | ConvertTo-Json -Compress -Depth 5
145
+ } catch {
146
+ $code = if ($_.Exception.Data['code']) { $_.Exception.Data['code'] } else { 'EIO' }
147
+ @{ ok=$false; code=$code; message=$_.Exception.Message } | ConvertTo-Json -Compress
148
+ exit 1
149
+ }
150
+ }