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/capc.ps1 ADDED
@@ -0,0 +1,432 @@
1
+ # SPDX-License-Identifier: MIT OR Apache-2.0
2
+ # win-nice: managed-file
3
+ # Deliberately no param()/[CmdletBinding()]: a declared parameter name (even
4
+ # without a [Parameter()] attribute) can still be ambiguously prefix-matched by
5
+ # flags meant for the wrapped command (e.g. "-p" matching "-Percent"). Reading
6
+ # everything from $args sidesteps PowerShell's parameter binder entirely.
7
+ if ($args.Count -lt 2) {
8
+ Write-Error "usage: capc <percent 1-100> <command> [args...]"
9
+ exit 1
10
+ }
11
+ $percentValue = 0
12
+ if (-not [int]::TryParse($args[0], [ref]$percentValue) -or $percentValue -lt 1 -or $percentValue -gt 100) {
13
+ Write-Error "usage: capc <percent 1-100> <command> [args...]"
14
+ exit 1
15
+ }
16
+ $Command = @($args[1..($args.Count - 1)])
17
+
18
+ # Fallback command line for when the target isn't a directly-launchable .exe (see
19
+ # CapcLauncher.Run below) - re-parsed by cmd.exe (via "cmd.exe /c"), so quoting must
20
+ # neutralize its operators (&|<>^) and not just whitespace, or e.g. "A&B" gets split
21
+ # into two commands. NOTE: a literal "%" in an argument can still trigger cmd.exe
22
+ # environment-variable expansion (e.g. "%PATH%") even when quoted, and cmd.exe pairs
23
+ # up "%" characters across argument/quote boundaries - two unrelated arguments that
24
+ # each contain one "%" can corrupt each other. There is no reliable per-character
25
+ # escape for this at the cmd.exe /c level; it's a known, inherent limitation shared
26
+ # by anything that shells out through cmd.exe (Node's own child_process included).
27
+ # This fallback path only runs for .bat/.cmd/builtin targets - a direct .exe target
28
+ # never goes through cmd.exe at all, so it isn't exposed to this limitation.
29
+ $commandLine = ($Command | ForEach-Object {
30
+ $escaped = $_ -replace '"', '\"'
31
+ if ($escaped -eq '' -or $escaped -match '[\s"&|<>^]') { '"' + $escaped + '"' } else { $escaped }
32
+ }) -join ' '
33
+
34
+ $source = @"
35
+ using System;
36
+ using System.Runtime.InteropServices;
37
+ using System.Text;
38
+
39
+ public static class CapcLauncher
40
+ {
41
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
42
+ struct STARTUPINFO
43
+ {
44
+ public int cb;
45
+ public string lpReserved;
46
+ public string lpDesktop;
47
+ public string lpTitle;
48
+ public int dwX;
49
+ public int dwY;
50
+ public int dwXSize;
51
+ public int dwYSize;
52
+ public int dwXCountChars;
53
+ public int dwYCountChars;
54
+ public int dwFillAttribute;
55
+ public int dwFlags;
56
+ public short wShowWindow;
57
+ public short cbReserved2;
58
+ public IntPtr lpReserved2;
59
+ public IntPtr hStdInput;
60
+ public IntPtr hStdOutput;
61
+ public IntPtr hStdError;
62
+ }
63
+
64
+ [StructLayout(LayoutKind.Sequential)]
65
+ struct PROCESS_INFORMATION
66
+ {
67
+ public IntPtr hProcess;
68
+ public IntPtr hThread;
69
+ public int dwProcessId;
70
+ public int dwThreadId;
71
+ }
72
+
73
+ [StructLayout(LayoutKind.Sequential)]
74
+ struct JOBOBJECT_BASIC_LIMIT_INFORMATION
75
+ {
76
+ public long PerProcessUserTimeLimit;
77
+ public long PerJobUserTimeLimit;
78
+ public uint LimitFlags;
79
+ public UIntPtr MinimumWorkingSetSize;
80
+ public UIntPtr MaximumWorkingSetSize;
81
+ public uint ActiveProcessLimit;
82
+ public UIntPtr Affinity;
83
+ public uint PriorityClass;
84
+ public uint SchedulingClass;
85
+ }
86
+
87
+ [StructLayout(LayoutKind.Sequential)]
88
+ struct IO_COUNTERS
89
+ {
90
+ public ulong ReadOperationCount;
91
+ public ulong WriteOperationCount;
92
+ public ulong OtherOperationCount;
93
+ public ulong ReadTransferCount;
94
+ public ulong WriteTransferCount;
95
+ public ulong OtherTransferCount;
96
+ }
97
+
98
+ [StructLayout(LayoutKind.Sequential)]
99
+ struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
100
+ {
101
+ public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
102
+ public IO_COUNTERS IoInfo;
103
+ public UIntPtr ProcessMemoryLimit;
104
+ public UIntPtr JobMemoryLimit;
105
+ public UIntPtr PeakProcessMemoryUsed;
106
+ public UIntPtr PeakJobMemoryUsed;
107
+ }
108
+
109
+ [StructLayout(LayoutKind.Sequential)]
110
+ struct JOBOBJECT_CPU_RATE_CONTROL_INFORMATION
111
+ {
112
+ public uint ControlFlags;
113
+ public uint CpuRate;
114
+ }
115
+
116
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
117
+ static extern bool CreateProcess(string lpApplicationName, StringBuilder lpCommandLine,
118
+ IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles,
119
+ uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory,
120
+ ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
121
+
122
+ [DllImport("kernel32.dll", SetLastError = true)]
123
+ static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string lpName);
124
+
125
+ [DllImport("kernel32.dll", SetLastError = true)]
126
+ static extern bool SetInformationJobObject(IntPtr hJob, int JobObjectInfoClass, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);
127
+
128
+ [DllImport("kernel32.dll", SetLastError = true)]
129
+ static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess);
130
+
131
+ [DllImport("kernel32.dll", SetLastError = true)]
132
+ static extern uint ResumeThread(IntPtr hThread);
133
+
134
+ [DllImport("kernel32.dll", SetLastError = true)]
135
+ static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
136
+
137
+ [DllImport("kernel32.dll", SetLastError = true)]
138
+ static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
139
+
140
+ [DllImport("kernel32.dll", SetLastError = true)]
141
+ static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode);
142
+
143
+ [DllImport("kernel32.dll")]
144
+ static extern bool CloseHandle(IntPtr hObject);
145
+
146
+ const uint CREATE_SUSPENDED = 0x00000004;
147
+ const int JobObjectCpuRateControlInformation = 15;
148
+ const uint JOB_OBJECT_CPU_RATE_CONTROL_ENABLE = 0x1;
149
+ const uint JOB_OBJECT_CPU_RATE_CONTROL_HARD_CAP = 0x4;
150
+ const int JobObjectExtendedLimitInformation = 9;
151
+ const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
152
+
153
+ // Standard MSVCRT/CommandLineToArgvW quoting: safe for a directly-launched .exe's
154
+ // own argv parsing. No cmd.exe involved on this path, so none of its operator or
155
+ // "%" expansion semantics apply - this is the safe path, used whenever possible.
156
+ static string ArgvQuote(string arg)
157
+ {
158
+ if (arg.Length > 0 && arg.IndexOfAny(new char[] { ' ', '\t', '\n', '\v', '"' }) < 0)
159
+ return arg;
160
+
161
+ var result = new StringBuilder();
162
+ result.Append('"');
163
+ int backslashes = 0;
164
+ foreach (char c in arg)
165
+ {
166
+ if (c == '\\')
167
+ {
168
+ backslashes++;
169
+ }
170
+ else if (c == '"')
171
+ {
172
+ result.Append('\\', backslashes * 2 + 1);
173
+ result.Append('"');
174
+ backslashes = 0;
175
+ }
176
+ else
177
+ {
178
+ if (backslashes > 0) { result.Append('\\', backslashes); backslashes = 0; }
179
+ result.Append(c);
180
+ }
181
+ }
182
+ if (backslashes > 0) result.Append('\\', backslashes * 2);
183
+ result.Append('"');
184
+ return result.ToString();
185
+ }
186
+
187
+ static string BuildArgvCommandLine(string[] argv)
188
+ {
189
+ var parts = new string[argv.Length];
190
+ for (int i = 0; i < argv.Length; i++) parts[i] = ArgvQuote(argv[i]);
191
+ return string.Join(" ", parts);
192
+ }
193
+
194
+ public static int Run(int percent, string[] argv, string cmdExeCommandLine)
195
+ {
196
+ IntPtr hJob = CreateJobObject(IntPtr.Zero, null);
197
+ if (hJob == IntPtr.Zero)
198
+ throw new InvalidOperationException("CreateJobObject failed: " + Marshal.GetLastWin32Error());
199
+
200
+ // Single owner for every handle this method acquires. The finally below closes
201
+ // hThread/hProcess/hJob - in that order - on EVERY way out: normal return, any
202
+ // of the InvalidOperationExceptions thrown here, and an unexpected managed
203
+ // exception (allocation/marshalling failure) between acquisition and use.
204
+ // hProcess/hThread stay IntPtr.Zero until CreateProcess has actually succeeded,
205
+ // so each handle is closed exactly once and only if it was really acquired.
206
+ IntPtr hProcess = IntPtr.Zero;
207
+ IntPtr hThread = IntPtr.Zero;
208
+ try
209
+ {
210
+ var cpuInfo = new JOBOBJECT_CPU_RATE_CONTROL_INFORMATION
211
+ {
212
+ ControlFlags = JOB_OBJECT_CPU_RATE_CONTROL_ENABLE | JOB_OBJECT_CPU_RATE_CONTROL_HARD_CAP,
213
+ CpuRate = (uint)(percent * 100)
214
+ };
215
+ int size = Marshal.SizeOf(cpuInfo);
216
+ IntPtr ptr = Marshal.AllocHGlobal(size);
217
+ bool ok;
218
+ try
219
+ {
220
+ Marshal.StructureToPtr(cpuInfo, ptr, false);
221
+ ok = SetInformationJobObject(hJob, JobObjectCpuRateControlInformation, ptr, (uint)size);
222
+ }
223
+ finally
224
+ {
225
+ Marshal.FreeHGlobal(ptr);
226
+ }
227
+ if (!ok)
228
+ throw new InvalidOperationException("SetInformationJobObject failed: " + Marshal.GetLastWin32Error());
229
+
230
+ // KILL_ON_JOB_CLOSE: the cleanup in the finally below only runs if this
231
+ // launcher process survives to execute it. Killed from outside (taskkill
232
+ // without /T, a crash), nothing in-process ever runs - without this flag
233
+ // the last job handle dying with the process would leave every process
234
+ // still assigned to the job running on, untracked and unmanaged. With
235
+ // it, Windows itself terminates the whole job at that moment.
236
+ //
237
+ // A backstop only, never the normal exit mechanism: the last handle
238
+ // closing terminates every process still assigned to the job FOR ANY
239
+ // reason, including this wrapper's own orderly close in the finally -
240
+ // and the wait below only waits on the directly wrapped root process,
241
+ // so a daemon it spawned and left running (build server, watcher) can
242
+ // still be in the job at that point, the root long gone. Killing a
243
+ // daemon on a SUCCESSFUL exit would break the documented
244
+ // daemon-survival contract (README: the limit sticks to any daemon
245
+ // the wrapped command leaves running, for that daemon's whole
246
+ // lifetime; see also
247
+ // https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects),
248
+ // so the success path below clears this flag before returning. Every
249
+ // other field stays zero - Windows only reads a struct field when its
250
+ // own LimitFlags bit is set.
251
+ // Set via the EXTENDED info class: JobObjectBasicLimitInformation
252
+ // rejects this flag with ERROR_INVALID_PARAMETER.
253
+ var extInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
254
+ {
255
+ BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION
256
+ {
257
+ LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
258
+ }
259
+ };
260
+ int extSize = Marshal.SizeOf(extInfo);
261
+ IntPtr extPtr = Marshal.AllocHGlobal(extSize);
262
+ bool extOk;
263
+ try
264
+ {
265
+ Marshal.StructureToPtr(extInfo, extPtr, false);
266
+ extOk = SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, extPtr, (uint)extSize);
267
+ }
268
+ finally
269
+ {
270
+ Marshal.FreeHGlobal(extPtr);
271
+ }
272
+ if (!extOk)
273
+ throw new InvalidOperationException("SetInformationJobObject failed: " + Marshal.GetLastWin32Error());
274
+
275
+ var si = new STARTUPINFO();
276
+ si.cb = Marshal.SizeOf(si);
277
+ PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
278
+
279
+ // Try launching the target directly first (no shell at all) - unless it's a
280
+ // .bat/.cmd file. CreateProcess has an undocumented-but-real fallback of its
281
+ // own for those: instead of failing, it silently re-invokes them through
282
+ // cmd.exe using OUR unescaped argv text (ArgvQuote only protects CRT argv
283
+ // parsing, not cmd.exe's operators), reopening the exact "A&B" splits this
284
+ // whole file exists to prevent. A bare name with no extension is safe either
285
+ // way: CreateProcess only ever auto-appends ".exe" to it, never ".bat/.cmd",
286
+ // so it fails cleanly (ERROR_FILE_NOT_FOUND) when only a same-named .bat/.cmd
287
+ // exists, and falls through to the escaped path below.
288
+ bool isBatOrCmd = argv.Length > 0 && (
289
+ argv[0].EndsWith(".bat", StringComparison.OrdinalIgnoreCase) ||
290
+ argv[0].EndsWith(".cmd", StringComparison.OrdinalIgnoreCase));
291
+
292
+ bool created = false;
293
+ if (!isBatOrCmd)
294
+ {
295
+ var directCommandLine = new StringBuilder(BuildArgvCommandLine(argv));
296
+ created = CreateProcess(null, directCommandLine, IntPtr.Zero, IntPtr.Zero, true,
297
+ CREATE_SUSPENDED, IntPtr.Zero, null, ref si, out pi);
298
+ }
299
+
300
+ if (!created)
301
+ {
302
+ // Falling back to cmd.exe /c: a literal "%" in any argument could now
303
+ // trigger environment-variable expansion (cmd.exe pairs up "%" characters
304
+ // across the whole command line, even across separate arguments) and
305
+ // change what actually runs. Fail loudly here instead of silently risking
306
+ // that - there's no reliable per-character escape for "%" at this level.
307
+ foreach (var a in argv)
308
+ {
309
+ if (a.IndexOf('%') >= 0)
310
+ throw new InvalidOperationException(
311
+ "Refusing to run: argument contains '%' and the target needs the cmd.exe " +
312
+ "fallback (not a directly-launchable .exe), where '%' can trigger unintended " +
313
+ "environment-variable expansion. See README's Argument handling section.");
314
+ }
315
+
316
+ string cmdExe = Environment.SystemDirectory + "\\cmd.exe";
317
+ // /d: skip HKCU AutoRun (user-writable registry key). /v:off: disable delayed
318
+ // expansion so "!var!" in an argument can't be expanded. /s plus the extra outer
319
+ // quote pair: cmd's /S rule strips exactly that outer pair and leaves the rest of
320
+ // the string untouched - without /S, cmd strips the first and last quote of the
321
+ // whole line instead, which breaks quoting whenever the target path itself needs
322
+ // quotes AND another argument is also quoted.
323
+ var shellCommandLine = new StringBuilder("\"" + cmdExe + "\" /d /v:off /s /c \"" + cmdExeCommandLine + "\"");
324
+ created = CreateProcess(null, shellCommandLine, IntPtr.Zero, IntPtr.Zero, true,
325
+ CREATE_SUSPENDED, IntPtr.Zero, null, ref si, out pi);
326
+ if (!created)
327
+ throw new InvalidOperationException("CreateProcess failed: " + Marshal.GetLastWin32Error());
328
+ }
329
+
330
+ // Ownership of the child's handles transfers here, once CreateProcess has
331
+ // actually succeeded - from this point the finally below is what closes them.
332
+ hProcess = pi.hProcess;
333
+ hThread = pi.hThread;
334
+
335
+ if (!AssignProcessToJobObject(hJob, hProcess))
336
+ {
337
+ // Can't guarantee the cap - kill instead of letting it run uncapped and orphaned.
338
+ int err = Marshal.GetLastWin32Error();
339
+ string message = "AssignProcessToJobObject failed: " + err;
340
+ // Report if the best-effort kill itself also failed.
341
+ if (!TerminateProcess(hProcess, 1))
342
+ message += "; TerminateProcess also failed: " + Marshal.GetLastWin32Error();
343
+ throw new InvalidOperationException(message);
344
+ }
345
+
346
+ if (ResumeThread(hThread) == 0xFFFFFFFF)
347
+ {
348
+ // Still suspended - an unbounded wait below would hang forever. Kill
349
+ // it instead of leaving an orphaned, permanently-suspended process.
350
+ int resumeErr = Marshal.GetLastWin32Error();
351
+ string message = "ResumeThread failed: " + resumeErr;
352
+ // Report if the best-effort kill itself also failed.
353
+ if (!TerminateProcess(hProcess, 1))
354
+ message += "; TerminateProcess also failed: " + Marshal.GetLastWin32Error();
355
+ throw new InvalidOperationException(message);
356
+ }
357
+
358
+ if (WaitForSingleObject(hProcess, 0xFFFFFFFF) == 0xFFFFFFFF)
359
+ {
360
+ // The child's actual state is unknown here - don't just report
361
+ // failure and potentially leave it running unmanaged in the
362
+ // background. Best-effort kill before giving up.
363
+ int waitErr = Marshal.GetLastWin32Error();
364
+ string message = "WaitForSingleObject failed: " + waitErr;
365
+ // Report if the best-effort kill itself also failed.
366
+ if (!TerminateProcess(hProcess, 1))
367
+ message += "; TerminateProcess also failed: " + Marshal.GetLastWin32Error();
368
+ throw new InvalidOperationException(message);
369
+ }
370
+
371
+ uint exitCode;
372
+ if (!GetExitCodeProcess(hProcess, out exitCode))
373
+ throw new InvalidOperationException("GetExitCodeProcess failed: " + Marshal.GetLastWin32Error());
374
+
375
+ // Normal success: the root process finished and reported its exit
376
+ // code - release the kill-on-close backstop so the finally below
377
+ // closes hJob WITHOUT terminating anything still assigned to the
378
+ // job. This is the documented daemon-survival contract, not a leak:
379
+ // whatever the command left running detached stays in the job and
380
+ // stays CPU-capped, it just outlives this wrapper's handle.
381
+ // Deliberately best-effort, not a throw: the wrapped command
382
+ // succeeded, and a failed cleanup syscall must not turn its real
383
+ // exit code below into a wrapper error - warn on stderr instead.
384
+ var releaseInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
385
+ {
386
+ BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION
387
+ {
388
+ LimitFlags = 0
389
+ }
390
+ };
391
+ int releaseSize = Marshal.SizeOf(releaseInfo);
392
+ IntPtr releasePtr = Marshal.AllocHGlobal(releaseSize);
393
+ bool releaseOk;
394
+ int releaseErr = 0;
395
+ try
396
+ {
397
+ Marshal.StructureToPtr(releaseInfo, releasePtr, false);
398
+ releaseOk = SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, releasePtr, (uint)releaseSize);
399
+ // Capture the Win32 error immediately, before any other call can
400
+ // overwrite it - every other native failure branch in this file
401
+ // reports the code for the same diagnosability reason.
402
+ if (!releaseOk)
403
+ releaseErr = Marshal.GetLastWin32Error();
404
+ }
405
+ finally
406
+ {
407
+ Marshal.FreeHGlobal(releasePtr);
408
+ }
409
+ if (!releaseOk)
410
+ Console.Error.WriteLine("warning: could not release the job's kill-on-close guard (SetInformationJobObject failed with Win32 error " + releaseErr + ") - a still-running background process left by the wrapped command may be terminated when this wrapper exits");
411
+
412
+ return (int)exitCode;
413
+ }
414
+ finally
415
+ {
416
+ // Same order as the code this replaces: thread handle, process handle, job handle.
417
+ if (hThread != IntPtr.Zero) CloseHandle(hThread);
418
+ if (hProcess != IntPtr.Zero) CloseHandle(hProcess);
419
+ if (hJob != IntPtr.Zero) CloseHandle(hJob);
420
+ }
421
+ }
422
+ }
423
+ "@
424
+
425
+ Add-Type -TypeDefinition $source -Language CSharp
426
+
427
+ try {
428
+ exit ([CapcLauncher]::Run($percentValue, [string[]]$Command, $commandLine))
429
+ } catch {
430
+ Write-Error $_.Exception.InnerException.Message
431
+ exit 1
432
+ }
@@ -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")/pint.ps1" 2>/dev/null) || script="$(dirname "$0")/pint.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")/capm.ps1" 2>/dev/null) || script="$(dirname "$0")/capm.ps1"
11
+ exec powershell -NoProfile -ExecutionPolicy Bypass -File "$script" "$@"
package/bin/capm.bat ADDED
@@ -0,0 +1,12 @@
1
+ @echo off
2
+ :: SPDX-License-Identifier: MIT OR Apache-2.0
3
+ :: win-nice: managed-file
4
+ :: A literal "%" in any argument gets corrupted here (cmd.exe rescans %1/%* for
5
+ :: %...% patterns the moment a batch file reads them - confirmed with nothing more
6
+ :: than a bare "echo %1", no forwarding involved; there's no per-character escape
7
+ :: for this from inside a .bat). Every other cmd.exe metacharacter (&|<>^) survives
8
+ :: this hop untouched. Invoking "capm" bare from an actual PowerShell session skips
9
+ :: this file entirely (PowerShell prefers capm.ps1) and has no "%" problem at all.
10
+ :: A lone trailing "%" in the <size> argument itself (e.g. "20%") survives this
11
+ :: hop - only %...%/%x-style pairs elsewhere on the line get corrupted.
12
+ "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "%~dp0capm.ps1" %*