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.
Files changed (53) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/README.md +309 -41
  3. package/bin/abovenormal +11 -11
  4. package/bin/abovenormal.bat +2 -2
  5. package/bin/abovenormal.ps1 +38 -10
  6. package/bin/admin +11 -11
  7. package/bin/admin.bat +2 -2
  8. package/bin/admin.ps1 +52 -13
  9. package/bin/belownormal +11 -11
  10. package/bin/belownormal.bat +2 -2
  11. package/bin/belownormal.ps1 +38 -10
  12. package/bin/{cap → capc} +11 -11
  13. package/bin/{cap.bat → capc.bat} +3 -3
  14. package/bin/capc.ps1 +432 -0
  15. package/bin/{pint → capm} +11 -11
  16. package/bin/capm.bat +12 -0
  17. package/bin/capm.ps1 +506 -0
  18. package/bin/capn +11 -0
  19. package/bin/capn.bat +8 -0
  20. package/bin/capn.ps1 +426 -0
  21. package/bin/caps +11 -0
  22. package/bin/caps.bat +10 -0
  23. package/bin/caps.ps1 +589 -0
  24. package/bin/capt +11 -0
  25. package/bin/capt.bat +8 -0
  26. package/bin/capt.ps1 +449 -0
  27. package/bin/cx +11 -11
  28. package/bin/cx.bat +1 -1
  29. package/bin/cx.ps1 +38 -10
  30. package/bin/cy +11 -11
  31. package/bin/cy.bat +1 -1
  32. package/bin/cy.ps1 +38 -10
  33. package/bin/high +11 -11
  34. package/bin/high.bat +2 -2
  35. package/bin/high.ps1 +38 -10
  36. package/bin/idle +11 -11
  37. package/bin/idle.bat +2 -2
  38. package/bin/idle.ps1 +38 -10
  39. package/bin/realtime +11 -11
  40. package/bin/realtime.bat +2 -2
  41. package/bin/realtime.ps1 +38 -10
  42. package/bin/uiup +11 -11
  43. package/bin/uiup.bat +1 -1
  44. package/bin/uiup.ps1 +2 -1
  45. package/install/install.js +30 -15
  46. package/install/paths.js +28 -2
  47. package/install/skill.js +25 -1
  48. package/install/uninstall.js +11 -0
  49. package/package.json +7 -3
  50. package/skills/win-nice/SKILL.md +107 -12
  51. package/bin/cap.ps1 +0 -269
  52. package/bin/pint.bat +0 -8
  53. 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
- # cap.ps1 for why that matters (claude's own flags shouldn't get bound here).
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
- # cap.ps1 for the same logic and its documented "%" limitation. cy.bat has its own,
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 cap.ps1 for why .bat/.cmd targets skip the direct attempt entirely:
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
- WaitForSingleObject(pi.hProcess, 0xFFFFFFFF);
166
-
167
- uint exitCode;
168
- GetExitCodeProcess(pi.hProcess, out exitCode);
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
- CloseHandle(pi.hThread);
171
- CloseHandle(pi.hProcess);
190
+ uint exitCode;
191
+ if (!GetExitCodeProcess(hProcess, out exitCode))
192
+ throw new InvalidOperationException("GetExitCodeProcess failed: " + Marshal.GetLastWin32Error());
172
193
 
173
- return (int)exitCode;
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 cap.bat for why (a
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 cap.ps1 for why that matters.
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 cap.ps1 for the
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 cap.ps1 for why .bat/.cmd targets skip the direct attempt entirely:
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
- WaitForSingleObject(pi.hProcess, 0xFFFFFFFF);
173
-
174
- uint exitCode;
175
- GetExitCodeProcess(pi.hProcess, out exitCode);
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
- CloseHandle(pi.hThread);
178
- CloseHandle(pi.hProcess);
197
+ uint exitCode;
198
+ if (!GetExitCodeProcess(hProcess, out exitCode))
199
+ throw new InvalidOperationException("GetExitCodeProcess failed: " + Marshal.GetLastWin32Error());
179
200
 
180
- return (int)exitCode;
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 cap.bat for why (a
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 cap.ps1 for why that matters.
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 cap.ps1 for the
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 cap.ps1 for why .bat/.cmd targets skip the direct attempt entirely:
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
- WaitForSingleObject(pi.hProcess, 0xFFFFFFFF);
172
-
173
- uint exitCode;
174
- GetExitCodeProcess(pi.hProcess, out exitCode);
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
- CloseHandle(pi.hThread);
177
- CloseHandle(pi.hProcess);
196
+ uint exitCode;
197
+ if (!GetExitCodeProcess(hProcess, out exitCode))
198
+ throw new InvalidOperationException("GetExitCodeProcess failed: " + Marshal.GetLastWin32Error());
178
199
 
179
- return (int)exitCode;
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 cap.bat for why (a
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 cap.ps1 for why that matters.
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 cap.ps1 for the
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 cap.ps1 for why .bat/.cmd targets skip the direct attempt entirely:
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
- WaitForSingleObject(pi.hProcess, 0xFFFFFFFF);
177
-
178
- uint exitCode;
179
- GetExitCodeProcess(pi.hProcess, out exitCode);
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
- CloseHandle(pi.hThread);
182
- CloseHandle(pi.hProcess);
201
+ uint exitCode;
202
+ if (!GetExitCodeProcess(hProcess, out exitCode))
203
+ throw new InvalidOperationException("GetExitCodeProcess failed: " + Marshal.GetLastWin32Error());
183
204
 
184
- return (int)exitCode;
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
- $p = Start-Process powershell -Verb RunAs -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$PSCommandPath`"", '-SelfElevated') -Wait -PassThru
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)"
@@ -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 (!previous || !Array.isArray(previous.files)) return;
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
- const currentSet = new Set(currentFiles);
34
- const staleNames = previous.files.filter((name) => !currentSet.has(name));
35
- const previousDir = previous.binDir || dir;
36
- for (const name of staleNames) {
37
- removeManagedFile(path.join(previousDir, name), dir, { requireMarker: false });
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 };