win-nice 0.1.0 → 0.2.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/CHANGELOG.md +141 -0
- package/README.md +309 -41
- package/bin/abovenormal +11 -11
- package/bin/abovenormal.bat +2 -2
- package/bin/abovenormal.ps1 +38 -10
- package/bin/admin +11 -11
- package/bin/admin.bat +2 -2
- package/bin/admin.ps1 +52 -13
- package/bin/belownormal +11 -11
- package/bin/belownormal.bat +2 -2
- package/bin/belownormal.ps1 +38 -10
- package/bin/{cap → capc} +11 -11
- package/bin/{cap.bat → capc.bat} +3 -3
- package/bin/capc.ps1 +432 -0
- package/bin/{pint → capm} +11 -11
- package/bin/capm.bat +12 -0
- package/bin/capm.ps1 +506 -0
- package/bin/capn +11 -0
- package/bin/capn.bat +8 -0
- package/bin/capn.ps1 +426 -0
- package/bin/caps +11 -0
- package/bin/caps.bat +10 -0
- package/bin/caps.ps1 +589 -0
- package/bin/capt +11 -0
- package/bin/capt.bat +8 -0
- package/bin/capt.ps1 +449 -0
- package/bin/cx +11 -11
- package/bin/cx.bat +1 -1
- package/bin/cx.ps1 +38 -10
- package/bin/cy +11 -11
- package/bin/cy.bat +1 -1
- package/bin/cy.ps1 +38 -10
- package/bin/high +11 -11
- package/bin/high.bat +2 -2
- package/bin/high.ps1 +38 -10
- package/bin/idle +11 -11
- package/bin/idle.bat +2 -2
- package/bin/idle.ps1 +38 -10
- package/bin/realtime +11 -11
- package/bin/realtime.bat +2 -2
- package/bin/realtime.ps1 +38 -10
- package/bin/uiup +11 -11
- package/bin/uiup.bat +1 -1
- package/bin/uiup.ps1 +2 -1
- package/install/install.js +30 -15
- package/install/paths.js +28 -2
- package/install/skill.js +25 -1
- package/install/uninstall.js +11 -0
- package/package.json +7 -3
- package/skills/win-nice/SKILL.md +107 -12
- package/bin/cap.ps1 +0 -269
- package/bin/pint.bat +0 -8
- package/bin/pint.ps1 +0 -270
package/bin/cy.ps1
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
2
2
|
# win-nice: managed-file
|
|
3
3
|
# No param(): $args sidesteps PowerShell's parameter binder entirely - see
|
|
4
|
-
#
|
|
4
|
+
# capc.ps1 for why that matters (claude's own flags shouldn't get bound here).
|
|
5
5
|
$Command = @('claude', '--dangerously-skip-permissions') + @($args)
|
|
6
6
|
|
|
7
7
|
# Fallback command line for when claude isn't a directly-launchable .exe (it's
|
|
8
8
|
# typically an npm-installed .cmd shim on Windows) - see CyLauncher.Run below and
|
|
9
|
-
#
|
|
9
|
+
# capc.ps1 for the same logic and its documented "%" limitation. cy.bat has its own,
|
|
10
10
|
# more severe "%" caveat (see there) that applies before this script ever runs.
|
|
11
11
|
$commandLine = ($Command | ForEach-Object {
|
|
12
12
|
$escaped = $_ -replace '"', '\"'
|
|
@@ -64,6 +64,9 @@ public static class CyLauncher
|
|
|
64
64
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
65
65
|
static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
|
66
66
|
|
|
67
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
68
|
+
static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode);
|
|
69
|
+
|
|
67
70
|
[DllImport("kernel32.dll")]
|
|
68
71
|
static extern bool CloseHandle(IntPtr hObject);
|
|
69
72
|
|
|
@@ -114,7 +117,7 @@ public static class CyLauncher
|
|
|
114
117
|
si.cb = Marshal.SizeOf(si);
|
|
115
118
|
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
|
|
116
119
|
|
|
117
|
-
// See
|
|
120
|
+
// See capc.ps1 for why .bat/.cmd targets skip the direct attempt entirely:
|
|
118
121
|
// CreateProcess silently re-invokes them through cmd.exe on its own, using
|
|
119
122
|
// unescaped text, instead of failing the way a genuinely missing exe would.
|
|
120
123
|
// A bare name like "claude" (typically an npm .cmd shim on Windows) isn't
|
|
@@ -139,6 +142,7 @@ public static class CyLauncher
|
|
|
139
142
|
// across the whole command line, even across separate arguments) and
|
|
140
143
|
// change what actually runs. Fail loudly here instead of silently risking
|
|
141
144
|
// that - there's no reliable per-character escape for "%" at this level.
|
|
145
|
+
// No handle is held at this point, so this throw has nothing to clean up.
|
|
142
146
|
foreach (var a in argv)
|
|
143
147
|
{
|
|
144
148
|
if (a.IndexOf('%') >= 0)
|
|
@@ -162,15 +166,39 @@ public static class CyLauncher
|
|
|
162
166
|
throw new InvalidOperationException("CreateProcess failed: " + Marshal.GetLastWin32Error());
|
|
163
167
|
}
|
|
164
168
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
// Ownership of the child's handles starts here - CreateProcess has succeeded, so
|
|
170
|
+
// both are valid, and the finally below closes each of them exactly once on every
|
|
171
|
+
// way out: normal return, a thrown InvalidOperationException, or an unexpected
|
|
172
|
+
// managed exception.
|
|
173
|
+
IntPtr hProcess = pi.hProcess;
|
|
174
|
+
IntPtr hThread = pi.hThread;
|
|
175
|
+
try
|
|
176
|
+
{
|
|
177
|
+
if (WaitForSingleObject(hProcess, 0xFFFFFFFF) == 0xFFFFFFFF)
|
|
178
|
+
{
|
|
179
|
+
// The child's actual state is unknown here - don't just report
|
|
180
|
+
// failure and potentially leave it running unmanaged in the
|
|
181
|
+
// background. Best-effort kill before giving up.
|
|
182
|
+
int waitErr = Marshal.GetLastWin32Error();
|
|
183
|
+
string message = "WaitForSingleObject failed: " + waitErr;
|
|
184
|
+
// Report if the best-effort kill itself also failed.
|
|
185
|
+
if (!TerminateProcess(hProcess, 1))
|
|
186
|
+
message += "; TerminateProcess also failed: " + Marshal.GetLastWin32Error();
|
|
187
|
+
throw new InvalidOperationException(message);
|
|
188
|
+
}
|
|
169
189
|
|
|
170
|
-
|
|
171
|
-
|
|
190
|
+
uint exitCode;
|
|
191
|
+
if (!GetExitCodeProcess(hProcess, out exitCode))
|
|
192
|
+
throw new InvalidOperationException("GetExitCodeProcess failed: " + Marshal.GetLastWin32Error());
|
|
172
193
|
|
|
173
|
-
|
|
194
|
+
return (int)exitCode;
|
|
195
|
+
}
|
|
196
|
+
finally
|
|
197
|
+
{
|
|
198
|
+
// Same order as the code this replaces: thread handle, then process handle.
|
|
199
|
+
CloseHandle(hThread);
|
|
200
|
+
CloseHandle(hProcess);
|
|
201
|
+
}
|
|
174
202
|
}
|
|
175
203
|
}
|
|
176
204
|
"@
|
package/bin/high
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
-
# win-nice: managed-file
|
|
4
|
-
# Git Bash ignores PATHEXT for bare-name resolution; this shim covers that shell.
|
|
5
|
-
# MSYS2_ARG_CONV_EXCL='*' stops Git Bash/MSYS from rewriting user arguments
|
|
6
|
-
# (e.g. /c, /d, C:\...) into Windows paths before the exec; the shim's own
|
|
7
|
-
# .ps1 path is converted explicitly with cygpath -w so -File gets a Windows path.
|
|
8
|
-
MSYS2_ARG_CONV_EXCL='*'
|
|
9
|
-
export MSYS2_ARG_CONV_EXCL
|
|
10
|
-
script=$(cygpath -w "$(dirname "$0")/high.ps1" 2>/dev/null) || script="$(dirname "$0")/high.ps1"
|
|
11
|
-
exec powershell -NoProfile -ExecutionPolicy Bypass -File "$script" "$@"
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
+
# win-nice: managed-file
|
|
4
|
+
# Git Bash ignores PATHEXT for bare-name resolution; this shim covers that shell.
|
|
5
|
+
# MSYS2_ARG_CONV_EXCL='*' stops Git Bash/MSYS from rewriting user arguments
|
|
6
|
+
# (e.g. /c, /d, C:\...) into Windows paths before the exec; the shim's own
|
|
7
|
+
# .ps1 path is converted explicitly with cygpath -w so -File gets a Windows path.
|
|
8
|
+
MSYS2_ARG_CONV_EXCL='*'
|
|
9
|
+
export MSYS2_ARG_CONV_EXCL
|
|
10
|
+
script=$(cygpath -w "$(dirname "$0")/high.ps1" 2>/dev/null) || script="$(dirname "$0")/high.ps1"
|
|
11
|
+
exec powershell -NoProfile -ExecutionPolicy Bypass -File "$script" "$@"
|
package/bin/high.bat
CHANGED
|
@@ -5,8 +5,8 @@ if "%~1"=="" (
|
|
|
5
5
|
echo usage: high ^<command^> [args...] 1>&2
|
|
6
6
|
exit /b 1
|
|
7
7
|
)
|
|
8
|
-
:: A literal "%" in any argument gets corrupted here - see
|
|
8
|
+
:: A literal "%" in any argument gets corrupted here - see capc.bat for why (a
|
|
9
9
|
:: cmd.exe batch-parameter quirk, not fixable from inside a .bat). Every other
|
|
10
10
|
:: cmd.exe metacharacter (&|<>^) survives this hop untouched. Invoking "high"
|
|
11
11
|
:: bare from an actual PowerShell session skips this file (high.ps1 preferred).
|
|
12
|
-
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0high.ps1" %*
|
|
12
|
+
"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "%~dp0high.ps1" %*
|
package/bin/high.ps1
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
2
2
|
# win-nice: managed-file
|
|
3
3
|
# No param(): nothing here needs a named parameter, and $args sidesteps
|
|
4
|
-
# PowerShell's parameter binder entirely - see
|
|
4
|
+
# PowerShell's parameter binder entirely - see capc.ps1 for why that matters.
|
|
5
5
|
$Command = $args
|
|
6
6
|
|
|
7
7
|
if (-not $Command -or $Command.Count -eq 0) {
|
|
@@ -11,7 +11,7 @@ if (-not $Command -or $Command.Count -eq 0) {
|
|
|
11
11
|
|
|
12
12
|
# Fallback command line for when the target isn't a directly-launchable .exe (see
|
|
13
13
|
# HighLauncher.Run below) - re-parsed by cmd.exe (via "cmd.exe /c"), so quoting must
|
|
14
|
-
# neutralize its operators (&|<>^) and not just whitespace - see
|
|
14
|
+
# neutralize its operators (&|<>^) and not just whitespace - see capc.ps1 for the
|
|
15
15
|
# same logic and its documented "%" limitation. high.bat has its own, more severe
|
|
16
16
|
# "%" caveat (see there) that applies before this script ever runs.
|
|
17
17
|
$commandLine = ($Command | ForEach-Object {
|
|
@@ -70,6 +70,9 @@ public static class HighLauncher
|
|
|
70
70
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
71
71
|
static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
|
72
72
|
|
|
73
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
74
|
+
static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode);
|
|
75
|
+
|
|
73
76
|
[DllImport("kernel32.dll")]
|
|
74
77
|
static extern bool CloseHandle(IntPtr hObject);
|
|
75
78
|
|
|
@@ -124,7 +127,7 @@ public static class HighLauncher
|
|
|
124
127
|
si.cb = Marshal.SizeOf(si);
|
|
125
128
|
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
|
|
126
129
|
|
|
127
|
-
// See
|
|
130
|
+
// See capc.ps1 for why .bat/.cmd targets skip the direct attempt entirely:
|
|
128
131
|
// CreateProcess silently re-invokes them through cmd.exe on its own, using
|
|
129
132
|
// unescaped text, instead of failing the way a genuinely missing exe would.
|
|
130
133
|
bool isBatOrCmd = argv.Length > 0 && (
|
|
@@ -146,6 +149,7 @@ public static class HighLauncher
|
|
|
146
149
|
// across the whole command line, even across separate arguments) and
|
|
147
150
|
// change what actually runs. Fail loudly here instead of silently risking
|
|
148
151
|
// that - there's no reliable per-character escape for "%" at this level.
|
|
152
|
+
// No handle is held at this point, so this throw has nothing to clean up.
|
|
149
153
|
foreach (var a in argv)
|
|
150
154
|
{
|
|
151
155
|
if (a.IndexOf('%') >= 0)
|
|
@@ -169,15 +173,39 @@ public static class HighLauncher
|
|
|
169
173
|
throw new InvalidOperationException("CreateProcess failed: " + Marshal.GetLastWin32Error());
|
|
170
174
|
}
|
|
171
175
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
// Ownership of the child's handles starts here - CreateProcess has succeeded, so
|
|
177
|
+
// both are valid, and the finally below closes each of them exactly once on every
|
|
178
|
+
// way out: normal return, a thrown InvalidOperationException, or an unexpected
|
|
179
|
+
// managed exception.
|
|
180
|
+
IntPtr hProcess = pi.hProcess;
|
|
181
|
+
IntPtr hThread = pi.hThread;
|
|
182
|
+
try
|
|
183
|
+
{
|
|
184
|
+
if (WaitForSingleObject(hProcess, 0xFFFFFFFF) == 0xFFFFFFFF)
|
|
185
|
+
{
|
|
186
|
+
// The child's actual state is unknown here - don't just report
|
|
187
|
+
// failure and potentially leave it running unmanaged in the
|
|
188
|
+
// background. Best-effort kill before giving up.
|
|
189
|
+
int waitErr = Marshal.GetLastWin32Error();
|
|
190
|
+
string message = "WaitForSingleObject failed: " + waitErr;
|
|
191
|
+
// Report if the best-effort kill itself also failed.
|
|
192
|
+
if (!TerminateProcess(hProcess, 1))
|
|
193
|
+
message += "; TerminateProcess also failed: " + Marshal.GetLastWin32Error();
|
|
194
|
+
throw new InvalidOperationException(message);
|
|
195
|
+
}
|
|
176
196
|
|
|
177
|
-
|
|
178
|
-
|
|
197
|
+
uint exitCode;
|
|
198
|
+
if (!GetExitCodeProcess(hProcess, out exitCode))
|
|
199
|
+
throw new InvalidOperationException("GetExitCodeProcess failed: " + Marshal.GetLastWin32Error());
|
|
179
200
|
|
|
180
|
-
|
|
201
|
+
return (int)exitCode;
|
|
202
|
+
}
|
|
203
|
+
finally
|
|
204
|
+
{
|
|
205
|
+
// Same order as the code this replaces: thread handle, then process handle.
|
|
206
|
+
CloseHandle(hThread);
|
|
207
|
+
CloseHandle(hProcess);
|
|
208
|
+
}
|
|
181
209
|
}
|
|
182
210
|
}
|
|
183
211
|
"@
|
package/bin/idle
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
-
# win-nice: managed-file
|
|
4
|
-
# Git Bash ignores PATHEXT for bare-name resolution; this shim covers that shell.
|
|
5
|
-
# MSYS2_ARG_CONV_EXCL='*' stops Git Bash/MSYS from rewriting user arguments
|
|
6
|
-
# (e.g. /c, /d, C:\...) into Windows paths before the exec; the shim's own
|
|
7
|
-
# .ps1 path is converted explicitly with cygpath -w so -File gets a Windows path.
|
|
8
|
-
MSYS2_ARG_CONV_EXCL='*'
|
|
9
|
-
export MSYS2_ARG_CONV_EXCL
|
|
10
|
-
script=$(cygpath -w "$(dirname "$0")/idle.ps1" 2>/dev/null) || script="$(dirname "$0")/idle.ps1"
|
|
11
|
-
exec powershell -NoProfile -ExecutionPolicy Bypass -File "$script" "$@"
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
+
# win-nice: managed-file
|
|
4
|
+
# Git Bash ignores PATHEXT for bare-name resolution; this shim covers that shell.
|
|
5
|
+
# MSYS2_ARG_CONV_EXCL='*' stops Git Bash/MSYS from rewriting user arguments
|
|
6
|
+
# (e.g. /c, /d, C:\...) into Windows paths before the exec; the shim's own
|
|
7
|
+
# .ps1 path is converted explicitly with cygpath -w so -File gets a Windows path.
|
|
8
|
+
MSYS2_ARG_CONV_EXCL='*'
|
|
9
|
+
export MSYS2_ARG_CONV_EXCL
|
|
10
|
+
script=$(cygpath -w "$(dirname "$0")/idle.ps1" 2>/dev/null) || script="$(dirname "$0")/idle.ps1"
|
|
11
|
+
exec powershell -NoProfile -ExecutionPolicy Bypass -File "$script" "$@"
|
package/bin/idle.bat
CHANGED
|
@@ -5,8 +5,8 @@ if "%~1"=="" (
|
|
|
5
5
|
echo usage: idle ^<command^> [args...] 1>&2
|
|
6
6
|
exit /b 1
|
|
7
7
|
)
|
|
8
|
-
:: A literal "%" in any argument gets corrupted here - see
|
|
8
|
+
:: A literal "%" in any argument gets corrupted here - see capc.bat for why (a
|
|
9
9
|
:: cmd.exe batch-parameter quirk, not fixable from inside a .bat). Every other
|
|
10
10
|
:: cmd.exe metacharacter (&|<>^) survives this hop untouched. Invoking "idle"
|
|
11
11
|
:: bare from an actual PowerShell session skips this file (idle.ps1 preferred).
|
|
12
|
-
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0idle.ps1" %*
|
|
12
|
+
"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "%~dp0idle.ps1" %*
|
package/bin/idle.ps1
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
2
2
|
# win-nice: managed-file
|
|
3
3
|
# No param(): nothing here needs a named parameter, and $args sidesteps
|
|
4
|
-
# PowerShell's parameter binder entirely - see
|
|
4
|
+
# PowerShell's parameter binder entirely - see capc.ps1 for why that matters.
|
|
5
5
|
$Command = $args
|
|
6
6
|
|
|
7
7
|
if (-not $Command -or $Command.Count -eq 0) {
|
|
@@ -11,7 +11,7 @@ if (-not $Command -or $Command.Count -eq 0) {
|
|
|
11
11
|
|
|
12
12
|
# Fallback command line for when the target isn't a directly-launchable .exe (see
|
|
13
13
|
# IdleLauncher.Run below) - re-parsed by cmd.exe (via "cmd.exe /c"), so quoting must
|
|
14
|
-
# neutralize its operators (&|<>^) and not just whitespace - see
|
|
14
|
+
# neutralize its operators (&|<>^) and not just whitespace - see capc.ps1 for the
|
|
15
15
|
# same logic and its documented "%" limitation. idle.bat has its own, more severe
|
|
16
16
|
# "%" caveat (see there) that applies before this script ever runs.
|
|
17
17
|
$commandLine = ($Command | ForEach-Object {
|
|
@@ -70,6 +70,9 @@ public static class IdleLauncher
|
|
|
70
70
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
71
71
|
static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
|
72
72
|
|
|
73
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
74
|
+
static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode);
|
|
75
|
+
|
|
73
76
|
[DllImport("kernel32.dll")]
|
|
74
77
|
static extern bool CloseHandle(IntPtr hObject);
|
|
75
78
|
|
|
@@ -123,7 +126,7 @@ public static class IdleLauncher
|
|
|
123
126
|
si.cb = Marshal.SizeOf(si);
|
|
124
127
|
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
|
|
125
128
|
|
|
126
|
-
// See
|
|
129
|
+
// See capc.ps1 for why .bat/.cmd targets skip the direct attempt entirely:
|
|
127
130
|
// CreateProcess silently re-invokes them through cmd.exe on its own, using
|
|
128
131
|
// unescaped text, instead of failing the way a genuinely missing exe would.
|
|
129
132
|
bool isBatOrCmd = argv.Length > 0 && (
|
|
@@ -145,6 +148,7 @@ public static class IdleLauncher
|
|
|
145
148
|
// across the whole command line, even across separate arguments) and
|
|
146
149
|
// change what actually runs. Fail loudly here instead of silently risking
|
|
147
150
|
// that - there's no reliable per-character escape for "%" at this level.
|
|
151
|
+
// No handle is held at this point, so this throw has nothing to clean up.
|
|
148
152
|
foreach (var a in argv)
|
|
149
153
|
{
|
|
150
154
|
if (a.IndexOf('%') >= 0)
|
|
@@ -168,15 +172,39 @@ public static class IdleLauncher
|
|
|
168
172
|
throw new InvalidOperationException("CreateProcess failed: " + Marshal.GetLastWin32Error());
|
|
169
173
|
}
|
|
170
174
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
+
// Ownership of the child's handles starts here - CreateProcess has succeeded, so
|
|
176
|
+
// both are valid, and the finally below closes each of them exactly once on every
|
|
177
|
+
// way out: normal return, a thrown InvalidOperationException, or an unexpected
|
|
178
|
+
// managed exception.
|
|
179
|
+
IntPtr hProcess = pi.hProcess;
|
|
180
|
+
IntPtr hThread = pi.hThread;
|
|
181
|
+
try
|
|
182
|
+
{
|
|
183
|
+
if (WaitForSingleObject(hProcess, 0xFFFFFFFF) == 0xFFFFFFFF)
|
|
184
|
+
{
|
|
185
|
+
// The child's actual state is unknown here - don't just report
|
|
186
|
+
// failure and potentially leave it running unmanaged in the
|
|
187
|
+
// background. Best-effort kill before giving up.
|
|
188
|
+
int waitErr = Marshal.GetLastWin32Error();
|
|
189
|
+
string message = "WaitForSingleObject failed: " + waitErr;
|
|
190
|
+
// Report if the best-effort kill itself also failed.
|
|
191
|
+
if (!TerminateProcess(hProcess, 1))
|
|
192
|
+
message += "; TerminateProcess also failed: " + Marshal.GetLastWin32Error();
|
|
193
|
+
throw new InvalidOperationException(message);
|
|
194
|
+
}
|
|
175
195
|
|
|
176
|
-
|
|
177
|
-
|
|
196
|
+
uint exitCode;
|
|
197
|
+
if (!GetExitCodeProcess(hProcess, out exitCode))
|
|
198
|
+
throw new InvalidOperationException("GetExitCodeProcess failed: " + Marshal.GetLastWin32Error());
|
|
178
199
|
|
|
179
|
-
|
|
200
|
+
return (int)exitCode;
|
|
201
|
+
}
|
|
202
|
+
finally
|
|
203
|
+
{
|
|
204
|
+
// Same order as the code this replaces: thread handle, then process handle.
|
|
205
|
+
CloseHandle(hThread);
|
|
206
|
+
CloseHandle(hProcess);
|
|
207
|
+
}
|
|
180
208
|
}
|
|
181
209
|
}
|
|
182
210
|
"@
|
package/bin/realtime
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
-
# win-nice: managed-file
|
|
4
|
-
# Git Bash ignores PATHEXT for bare-name resolution; this shim covers that shell.
|
|
5
|
-
# MSYS2_ARG_CONV_EXCL='*' stops Git Bash/MSYS from rewriting user arguments
|
|
6
|
-
# (e.g. /c, /d, C:\...) into Windows paths before the exec; the shim's own
|
|
7
|
-
# .ps1 path is converted explicitly with cygpath -w so -File gets a Windows path.
|
|
8
|
-
MSYS2_ARG_CONV_EXCL='*'
|
|
9
|
-
export MSYS2_ARG_CONV_EXCL
|
|
10
|
-
script=$(cygpath -w "$(dirname "$0")/realtime.ps1" 2>/dev/null) || script="$(dirname "$0")/realtime.ps1"
|
|
11
|
-
exec powershell -NoProfile -ExecutionPolicy Bypass -File "$script" "$@"
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
+
# win-nice: managed-file
|
|
4
|
+
# Git Bash ignores PATHEXT for bare-name resolution; this shim covers that shell.
|
|
5
|
+
# MSYS2_ARG_CONV_EXCL='*' stops Git Bash/MSYS from rewriting user arguments
|
|
6
|
+
# (e.g. /c, /d, C:\...) into Windows paths before the exec; the shim's own
|
|
7
|
+
# .ps1 path is converted explicitly with cygpath -w so -File gets a Windows path.
|
|
8
|
+
MSYS2_ARG_CONV_EXCL='*'
|
|
9
|
+
export MSYS2_ARG_CONV_EXCL
|
|
10
|
+
script=$(cygpath -w "$(dirname "$0")/realtime.ps1" 2>/dev/null) || script="$(dirname "$0")/realtime.ps1"
|
|
11
|
+
exec powershell -NoProfile -ExecutionPolicy Bypass -File "$script" "$@"
|
package/bin/realtime.bat
CHANGED
|
@@ -6,8 +6,8 @@ if "%~1"=="" (
|
|
|
6
6
|
echo usage: realtime ^<command^> [args...] 1>&2
|
|
7
7
|
exit /b 1
|
|
8
8
|
)
|
|
9
|
-
:: A literal "%" in any argument gets corrupted here - see
|
|
9
|
+
:: A literal "%" in any argument gets corrupted here - see capc.bat for why (a
|
|
10
10
|
:: cmd.exe batch-parameter quirk, not fixable from inside a .bat). Every other
|
|
11
11
|
:: cmd.exe metacharacter (&|<>^) survives this hop untouched. Invoking "realtime"
|
|
12
12
|
:: bare from an actual PowerShell session skips this file (realtime.ps1 preferred).
|
|
13
|
-
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0realtime.ps1" %*
|
|
13
|
+
"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "%~dp0realtime.ps1" %*
|
package/bin/realtime.ps1
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# stop responding - the exact failure mode this whole project exists to prevent. See
|
|
6
6
|
# README before using this.
|
|
7
7
|
# No param(): nothing here needs a named parameter, and $args sidesteps
|
|
8
|
-
# PowerShell's parameter binder entirely - see
|
|
8
|
+
# PowerShell's parameter binder entirely - see capc.ps1 for why that matters.
|
|
9
9
|
$Command = $args
|
|
10
10
|
|
|
11
11
|
if (-not $Command -or $Command.Count -eq 0) {
|
|
@@ -15,7 +15,7 @@ if (-not $Command -or $Command.Count -eq 0) {
|
|
|
15
15
|
|
|
16
16
|
# Fallback command line for when the target isn't a directly-launchable .exe (see
|
|
17
17
|
# RealtimeLauncher.Run below) - re-parsed by cmd.exe (via "cmd.exe /c"), so quoting must
|
|
18
|
-
# neutralize its operators (&|<>^) and not just whitespace - see
|
|
18
|
+
# neutralize its operators (&|<>^) and not just whitespace - see capc.ps1 for the
|
|
19
19
|
# same logic and its documented "%" limitation. realtime.bat has its own, more
|
|
20
20
|
# severe "%" caveat (see there) that applies before this script ever runs.
|
|
21
21
|
$commandLine = ($Command | ForEach-Object {
|
|
@@ -74,6 +74,9 @@ public static class RealtimeLauncher
|
|
|
74
74
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
75
75
|
static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
|
76
76
|
|
|
77
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
78
|
+
static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode);
|
|
79
|
+
|
|
77
80
|
[DllImport("kernel32.dll")]
|
|
78
81
|
static extern bool CloseHandle(IntPtr hObject);
|
|
79
82
|
|
|
@@ -128,7 +131,7 @@ public static class RealtimeLauncher
|
|
|
128
131
|
si.cb = Marshal.SizeOf(si);
|
|
129
132
|
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
|
|
130
133
|
|
|
131
|
-
// See
|
|
134
|
+
// See capc.ps1 for why .bat/.cmd targets skip the direct attempt entirely:
|
|
132
135
|
// CreateProcess silently re-invokes them through cmd.exe on its own, using
|
|
133
136
|
// unescaped text, instead of failing the way a genuinely missing exe would.
|
|
134
137
|
bool isBatOrCmd = argv.Length > 0 && (
|
|
@@ -150,6 +153,7 @@ public static class RealtimeLauncher
|
|
|
150
153
|
// across the whole command line, even across separate arguments) and
|
|
151
154
|
// change what actually runs. Fail loudly here instead of silently risking
|
|
152
155
|
// that - there's no reliable per-character escape for "%" at this level.
|
|
156
|
+
// No handle is held at this point, so this throw has nothing to clean up.
|
|
153
157
|
foreach (var a in argv)
|
|
154
158
|
{
|
|
155
159
|
if (a.IndexOf('%') >= 0)
|
|
@@ -173,15 +177,39 @@ public static class RealtimeLauncher
|
|
|
173
177
|
throw new InvalidOperationException("CreateProcess failed: " + Marshal.GetLastWin32Error());
|
|
174
178
|
}
|
|
175
179
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
+
// Ownership of the child's handles starts here - CreateProcess has succeeded, so
|
|
181
|
+
// both are valid, and the finally below closes each of them exactly once on every
|
|
182
|
+
// way out: normal return, a thrown InvalidOperationException, or an unexpected
|
|
183
|
+
// managed exception.
|
|
184
|
+
IntPtr hProcess = pi.hProcess;
|
|
185
|
+
IntPtr hThread = pi.hThread;
|
|
186
|
+
try
|
|
187
|
+
{
|
|
188
|
+
if (WaitForSingleObject(hProcess, 0xFFFFFFFF) == 0xFFFFFFFF)
|
|
189
|
+
{
|
|
190
|
+
// The child's actual state is unknown here - don't just report
|
|
191
|
+
// failure and potentially leave it running unmanaged in the
|
|
192
|
+
// background. Best-effort kill before giving up.
|
|
193
|
+
int waitErr = Marshal.GetLastWin32Error();
|
|
194
|
+
string message = "WaitForSingleObject failed: " + waitErr;
|
|
195
|
+
// Report if the best-effort kill itself also failed.
|
|
196
|
+
if (!TerminateProcess(hProcess, 1))
|
|
197
|
+
message += "; TerminateProcess also failed: " + Marshal.GetLastWin32Error();
|
|
198
|
+
throw new InvalidOperationException(message);
|
|
199
|
+
}
|
|
180
200
|
|
|
181
|
-
|
|
182
|
-
|
|
201
|
+
uint exitCode;
|
|
202
|
+
if (!GetExitCodeProcess(hProcess, out exitCode))
|
|
203
|
+
throw new InvalidOperationException("GetExitCodeProcess failed: " + Marshal.GetLastWin32Error());
|
|
183
204
|
|
|
184
|
-
|
|
205
|
+
return (int)exitCode;
|
|
206
|
+
}
|
|
207
|
+
finally
|
|
208
|
+
{
|
|
209
|
+
// Same order as the code this replaces: thread handle, then process handle.
|
|
210
|
+
CloseHandle(hThread);
|
|
211
|
+
CloseHandle(hProcess);
|
|
212
|
+
}
|
|
185
213
|
}
|
|
186
214
|
}
|
|
187
215
|
"@
|
package/bin/uiup
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
-
# win-nice: managed-file
|
|
4
|
-
# Git Bash ignores PATHEXT for bare-name resolution; this shim covers that shell.
|
|
5
|
-
# MSYS2_ARG_CONV_EXCL='*' stops Git Bash/MSYS from rewriting user arguments
|
|
6
|
-
# (e.g. /c, /d, C:\...) into Windows paths before the exec; the shim's own
|
|
7
|
-
# .ps1 path is converted explicitly with cygpath -w so -File gets a Windows path.
|
|
8
|
-
MSYS2_ARG_CONV_EXCL='*'
|
|
9
|
-
export MSYS2_ARG_CONV_EXCL
|
|
10
|
-
script=$(cygpath -w "$(dirname "$0")/uiup.ps1" 2>/dev/null) || script="$(dirname "$0")/uiup.ps1"
|
|
11
|
-
exec powershell -NoProfile -ExecutionPolicy Bypass -File "$script" "$@"
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
+
# win-nice: managed-file
|
|
4
|
+
# Git Bash ignores PATHEXT for bare-name resolution; this shim covers that shell.
|
|
5
|
+
# MSYS2_ARG_CONV_EXCL='*' stops Git Bash/MSYS from rewriting user arguments
|
|
6
|
+
# (e.g. /c, /d, C:\...) into Windows paths before the exec; the shim's own
|
|
7
|
+
# .ps1 path is converted explicitly with cygpath -w so -File gets a Windows path.
|
|
8
|
+
MSYS2_ARG_CONV_EXCL='*'
|
|
9
|
+
export MSYS2_ARG_CONV_EXCL
|
|
10
|
+
script=$(cygpath -w "$(dirname "$0")/uiup.ps1" 2>/dev/null) || script="$(dirname "$0")/uiup.ps1"
|
|
11
|
+
exec powershell -NoProfile -ExecutionPolicy Bypass -File "$script" "$@"
|
package/bin/uiup.bat
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
@echo off
|
|
2
2
|
:: SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
3
|
:: win-nice: managed-file
|
|
4
|
-
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0uiup.ps1"
|
|
4
|
+
"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "%~dp0uiup.ps1"
|
package/bin/uiup.ps1
CHANGED
|
@@ -8,7 +8,8 @@ $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIden
|
|
|
8
8
|
if (-not $isAdmin) {
|
|
9
9
|
Write-Host "Not elevated - requesting admin rights (dwm/sihost run under a different account)..."
|
|
10
10
|
try {
|
|
11
|
-
$
|
|
11
|
+
$powershellPath = [Environment]::SystemDirectory + '\WindowsPowerShell\v1.0\powershell.exe'
|
|
12
|
+
$p = Start-Process -FilePath $powershellPath -Verb RunAs -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$PSCommandPath`"", '-SelfElevated') -Wait -PassThru
|
|
12
13
|
exit $p.ExitCode
|
|
13
14
|
} catch {
|
|
14
15
|
Write-Error "Elevation was cancelled or failed: $($_.Exception.Message)"
|
package/install/install.js
CHANGED
|
@@ -4,6 +4,7 @@ const path = require('path');
|
|
|
4
4
|
const paths = require('./paths');
|
|
5
5
|
const manifest = require('./manifest');
|
|
6
6
|
const { removeManagedFile } = require('./uninstall');
|
|
7
|
+
const { updateInstalledSkill } = require('./skill');
|
|
7
8
|
const pkg = require('../package.json');
|
|
8
9
|
|
|
9
10
|
const SOURCE_BIN = path.join(__dirname, '..', 'bin');
|
|
@@ -14,32 +15,38 @@ function listSourceFiles() {
|
|
|
14
15
|
return fs.readdirSync(SOURCE_BIN).filter((f) => f.endsWith('.bat') || f.endsWith('.ps1') || !f.includes('.'));
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
// Guards against `npm install`/`npm test` inside a source checkout silently
|
|
18
|
-
// touching the real system PATH - only a genuine package install (running from
|
|
19
|
-
// inside someone's node_modules) or an explicit WIN_NICE_HOME override proceeds.
|
|
20
|
-
function isSourceCheckout() {
|
|
21
|
-
return fs.existsSync(path.join(__dirname, '..', '.git'));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
18
|
// `npm install -g win-nice@newer` only runs postinstall (this function) - unlike
|
|
25
19
|
// `win-nice reinstall`, which does uninstall()+install(), it never diffs against
|
|
26
20
|
// what a previous version left behind. Without this, a tool dropped in a newer
|
|
27
21
|
// version stays orphaned in binDir forever. Safe to call before the target dir
|
|
28
22
|
// even exists (read() returns null, staleNames is empty).
|
|
29
23
|
function cleanupStaleFiles(dir, currentFiles) {
|
|
24
|
+
const currentSet = new Set(currentFiles);
|
|
30
25
|
const previous = manifest.read(paths.manifestPath());
|
|
31
|
-
if (
|
|
26
|
+
if (previous && Array.isArray(previous.files)) {
|
|
27
|
+
const staleNames = previous.files.filter((name) => !currentSet.has(name));
|
|
28
|
+
const previousDir = previous.binDir || dir;
|
|
29
|
+
for (const name of staleNames) {
|
|
30
|
+
removeManagedFile(path.join(previousDir, name), dir, { requireMarker: false });
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
// Manifest missing/corrupt (deleted by hand, or predates this file's
|
|
36
|
+
// existence) - same recovery uninstall() already relies on for the same
|
|
37
|
+
// situation: scan the known install dir directly and remove only entries
|
|
38
|
+
// that both (a) aren't part of the CURRENT tool set and (b) still carry the
|
|
39
|
+
// "win-nice: managed-file" marker. An unmarked file (something the user
|
|
40
|
+
// dropped into binDir themselves) is never touched, marker or no manifest.
|
|
41
|
+
if (!fs.existsSync(dir)) return;
|
|
42
|
+
for (const name of fs.readdirSync(dir)) {
|
|
43
|
+
if (currentSet.has(name)) continue;
|
|
44
|
+
removeManagedFile(path.join(dir, name), dir, { requireMarker: true });
|
|
38
45
|
}
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
function install({ updatePath = true } = {}) {
|
|
42
|
-
if (!process.env.WIN_NICE_HOME && isSourceCheckout()) {
|
|
49
|
+
if (!process.env.WIN_NICE_HOME && paths.isSourceCheckout()) {
|
|
43
50
|
console.log(
|
|
44
51
|
'Running from a source checkout - skipping real install. ' +
|
|
45
52
|
'Set WIN_NICE_HOME to force a target directory, or install the published package.'
|
|
@@ -73,7 +80,15 @@ function install({ updatePath = true } = {}) {
|
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
console.log(`win-nice ${pkg.version} installed: ${files.join(', ')} -> ${dir}`);
|
|
83
|
+
|
|
84
|
+
// Only touches a skill copy that's already there and still ours (opt-in
|
|
85
|
+
// stays opt-in) - see updateInstalledSkill's own comment for why this needs
|
|
86
|
+
// to run on every install(), not just the explicit `win-nice skill install`.
|
|
87
|
+
for (const r of updateInstalledSkill()) {
|
|
88
|
+
if (r.updated) console.log(`updated skill: ${r.file}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
76
91
|
return { dir, files };
|
|
77
92
|
}
|
|
78
93
|
|
|
79
|
-
module.exports = { install, listSourceFiles, isSourceCheckout };
|
|
94
|
+
module.exports = { install, listSourceFiles, isSourceCheckout: paths.isSourceCheckout };
|