@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.
- package/LICENSE +21 -0
- package/README.md +53 -3
- package/README.zh-CN.md +53 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +882 -0
- package/lib/index.js +1053 -0
- package/lib/typert.host.d.ts +5 -0
- package/lib/typert.host.js +295 -0
- package/lib/typert.remote-client.d.ts +15 -0
- package/lib/typert.remote-client.js +231 -0
- package/lib/types/cli-path/index.d.ts +12 -0
- package/lib/types/cli-path/inspect-active.d.ts +33 -0
- package/lib/types/cli-path/macos.d.ts +21 -0
- package/lib/types/cli-path/paths.d.ts +46 -0
- package/lib/types/cli-path/resolve-expected.d.ts +24 -0
- package/lib/types/cli-path/status.d.ts +32 -0
- package/lib/types/cli-path/windows.d.ts +26 -0
- package/lib/types/client/EnvSettingsSection.d.ts +20 -0
- package/lib/types/client/format-remote-error.d.ts +10 -0
- package/lib/types/client/index.d.ts +17 -0
- package/lib/types/client/locales.d.ts +67 -0
- package/lib/types/client/mount.d.ts +11 -0
- package/lib/types/env-file.d.ts +40 -0
- package/lib/types/index.d.ts +60 -0
- package/lib/types/names.d.ts +16 -0
- package/lib/types/os-user-env/index.d.ts +13 -0
- package/lib/types/os-user-env/macos.d.ts +15 -0
- package/lib/types/os-user-env/windows.d.ts +37 -0
- package/lib/types/types.d.ts +44 -0
- package/locale/en.json +4 -0
- package/locale/zh.json +4 -0
- package/package.json +87 -4
- package/scripts/command-path.ps1 +150 -0
|
@@ -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
|
+
}
|