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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.2.0] - 2026-09-04
|
|
8
|
+
|
|
9
|
+
The first release since `0.1.0` - `0.1.1` was never tagged or published;
|
|
10
|
+
everything below shipped together as `0.2.0`.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `capm` - hard memory ceiling via Job Objects; `<size>` accepts a bare
|
|
15
|
+
integer `1`-`100` (percent of total physical RAM, same convention as
|
|
16
|
+
`capc`'s own `<percent 1-100>`), or `m`/`M` (MB), or `g`/`G` (GB).
|
|
17
|
+
- `caps` - wall-clock timeout wrapper: runs `<command>` for up to `<seconds>`
|
|
18
|
+
and if it's still running, force-terminates it and its whole process tree in
|
|
19
|
+
one kernel call (`TerminateJobObject` on the tool's Job Object), then exits
|
|
20
|
+
`124` (the unix `timeout` convention) with a message on stderr. Finishes
|
|
21
|
+
inside the deadline: the wrapped exit code propagates like every other
|
|
22
|
+
launcher. `<seconds>` accepts a positive whole or decimal number (e.g. `2`
|
|
23
|
+
or `2.5`), converted to whole milliseconds, minimum 1 ms, maximum
|
|
24
|
+
`4294967294` ms (~49.7 days) - a deliberate usage ceiling, not an API limit
|
|
25
|
+
(the deadline is an absolute 64-bit FILETIME, so no uint32 boundary applies)
|
|
26
|
+
- anything larger is a usage error, not a
|
|
27
|
+
silently truncated deadline. The deadline is absolute, not a relative wait:
|
|
28
|
+
it is computed from `DateTime.UtcNow`, armed as the absolute due time of a
|
|
29
|
+
one-shot waitable timer, and waited on together with the process handle via
|
|
30
|
+
`WaitForMultipleObjects` (a `GetProcessTimes` check rejects an exit that
|
|
31
|
+
only won the simultaneous-signal race after the deadline), so time spent in
|
|
32
|
+
sleep/suspend counts against it and the timeout fires on wake if the
|
|
33
|
+
deadline passed during sleep (a relative wait doesn't count sleep time on
|
|
34
|
+
Windows 8+). The due time is a system-clock value, so a manual or
|
|
35
|
+
service-driven system-clock adjustment during the wait can shorten or
|
|
36
|
+
lengthen the actual wait relative to `<seconds>`. Unlike
|
|
37
|
+
`capc`/`capt`/`capm` it sets no resource
|
|
38
|
+
limit - the Job Object (with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`, so a
|
|
39
|
+
non-cooperatively killed wrapper still takes the tree down) exists purely to
|
|
40
|
+
make the timeout kill cover the whole spawned tree.
|
|
41
|
+
- `capn` - process-count ceiling wrapper: caps the number of simultaneously
|
|
42
|
+
active processes in the wrapped command's whole tree via
|
|
43
|
+
`JOB_OBJECT_LIMIT_ACTIVE_PROCESS`. The count includes the wrapped process
|
|
44
|
+
itself (it is assigned to the still-empty job before it can spawn
|
|
45
|
+
anything), so `capn 1 <command>` lets the command run but fails its first
|
|
46
|
+
child-spawn attempt. Exceeding the limit refuses only the offending spawn -
|
|
47
|
+
nothing already running is killed or throttled, and a command within budget
|
|
48
|
+
is unaffected (all confirmed empirically). `<count>` accepts a positive
|
|
49
|
+
whole number, minimum 1, maximum 4294967295 (`ActiveProcessLimit` is a
|
|
50
|
+
uint32 field). Like the other Job Object launchers it carries
|
|
51
|
+
`JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`, so a non-cooperatively killed `capn`
|
|
52
|
+
wrapper still takes the whole tree down.
|
|
53
|
+
- These tools can be chained by name (e.g. `capm 50 capc 50 idle <command>`)
|
|
54
|
+
- each wrapper's Job Object nests inside the outer one (Windows 8+). Nested
|
|
55
|
+
limits do *not* uniformly take the smaller value: CPU rate (`capc`)
|
|
56
|
+
multiplies relative to its parent (`capc 50 capc 50` ≈ 25%, not 50%);
|
|
57
|
+
memory (`capm`) ceilings apply independently to accounting scopes that
|
|
58
|
+
aren't the same size (a parent job's accounting includes every child job's
|
|
59
|
+
committed memory plus its own process, so nested `capm` ceilings don't
|
|
60
|
+
reduce to a simple minimum). Process count (`capn`) limits are enforced
|
|
61
|
+
independently per job, and an outer job's count already includes the inner
|
|
62
|
+
wrapper process itself, so nested `capn` ceilings aren't a simple minimum
|
|
63
|
+
either - leave the outer value headroom for the chain itself. See README's
|
|
64
|
+
"Chaining these tools together" section for the full picture.
|
|
65
|
+
|
|
66
|
+
### Changed
|
|
67
|
+
|
|
68
|
+
- `cap` renamed to `capc`; `pint` renamed to `capt` (breaking - the old names
|
|
69
|
+
no longer exist).
|
|
70
|
+
- Every launcher's embedded native-process primitive now checks
|
|
71
|
+
`ResumeThread`/`WaitForSingleObject`/`GetExitCodeProcess`/`TerminateProcess`
|
|
72
|
+
return values instead of assuming success, and `AllocHGlobal`/`FreeHGlobal`
|
|
73
|
+
around each Job Object limit struct is wrapped in `try`/`finally`. On a
|
|
74
|
+
`ResumeThread`, `AssignProcessToJobObject`, or `WaitForSingleObject`
|
|
75
|
+
failure, the launcher now attempts to terminate the child instead of
|
|
76
|
+
either waiting on a still-suspended process forever or reporting failure
|
|
77
|
+
while it may still be running unmanaged in the background - and if that
|
|
78
|
+
best-effort kill itself also fails, the thrown error says so explicitly.
|
|
79
|
+
Hardening for a class of rare Win32 failures - not a fix for an observed
|
|
80
|
+
regression.
|
|
81
|
+
- A plain `install`/upgrade (including `postinstall`) now also refreshes an
|
|
82
|
+
already-installed, still-marked `win-nice skill install` copy
|
|
83
|
+
(`~/.claude/skills/win-nice/SKILL.md`, `~/.agents/skills/win-nice/SKILL.md`)
|
|
84
|
+
to the new version's content. Initial skill installation is still opt-in -
|
|
85
|
+
this only ever touches a copy that's already there.
|
|
86
|
+
|
|
87
|
+
### Fixed
|
|
88
|
+
|
|
89
|
+
- Fixed executable search-order hardening for Windows system helpers: all
|
|
90
|
+
PowerShell-delegating `.bat` wrappers, the installer, and the `admin`/`uiup`
|
|
91
|
+
UAC paths now launch Windows PowerShell or `cmd.exe` by an absolute system
|
|
92
|
+
path. `admin` also carries the absolute path returned by `Get-Command` into
|
|
93
|
+
its direct elevated launch instead of resolving the original bare name a
|
|
94
|
+
second time through ShellExecute. This prevents a same-named executable in
|
|
95
|
+
the caller's current directory from replacing the intended process,
|
|
96
|
+
including at the elevation boundary.
|
|
97
|
+
- `capt` under 32-bit Windows PowerShell now rejects thread counts above the
|
|
98
|
+
32-bit affinity-mask width with an actionable usage error instead of
|
|
99
|
+
reaching a `UIntPtr` conversion and reporting a raw overflow exception.
|
|
100
|
+
- `uninstall` and `reinstall` run from a source checkout of this repository
|
|
101
|
+
without an explicit `WIN_NICE_HOME` set now refuse to run, matching what
|
|
102
|
+
`install` already did - previously they deleted a real
|
|
103
|
+
`%LOCALAPPDATA%\win-nice` installation and its PATH entry, then silently
|
|
104
|
+
failed to restore it.
|
|
105
|
+
- `capc`/`capt`/`capm`/`caps`/`capn` now set `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`
|
|
106
|
+
on their Job Object as a backstop for non-cooperative termination: a wrapper
|
|
107
|
+
killed from outside (`taskkill` without `/T`, a crash), or a `caps` deadline
|
|
108
|
+
expiring, no longer leaves orphaned grandchild processes (e.g. a linker
|
|
109
|
+
spawned by a build) running unbounded outside the resource limit - Windows
|
|
110
|
+
itself terminates everything still in the job at that moment. A clean,
|
|
111
|
+
successful exit releases this guard first, so a daemon/compiler-server/
|
|
112
|
+
watcher the wrapped command legitimately left running still survives -
|
|
113
|
+
matching this same section's existing "a limit sticks to any daemon ... for
|
|
114
|
+
that daemon's whole lifetime" guarantee, which an earlier draft of this
|
|
115
|
+
change would otherwise have silently broken.
|
|
116
|
+
|
|
117
|
+
## [0.1.0] - 2026-09-02
|
|
118
|
+
|
|
119
|
+
### Added
|
|
120
|
+
|
|
121
|
+
- `idle`, `belownormal`, `abovenormal`, `high`, `realtime` - Windows priority
|
|
122
|
+
class launchers.
|
|
123
|
+
- `cap` - hard CPU quota via Job Objects.
|
|
124
|
+
- `pint` - CPU affinity ("pin threads") via Job Objects.
|
|
125
|
+
- `admin` - run a command elevated, with UAC prompt when needed.
|
|
126
|
+
- `uiup` - one-shot desktop-responsiveness priority boost.
|
|
127
|
+
- `cy` / `cx` - `claude`/`codex` launchers with permission/approval bypass
|
|
128
|
+
flags, for use in already-sandboxed/disposable environments.
|
|
129
|
+
- Each tool ships a `.bat`, `.ps1`, and extensionless (Git Bash) entry point.
|
|
130
|
+
- npm-based installer (`postinstall`, `win-nice status|reinstall|uninstall`),
|
|
131
|
+
PATH management via the Windows registry. Uninstall is an explicit `win-nice
|
|
132
|
+
uninstall` command, not an npm `preuninstall` lifecycle hook - npm >= 7
|
|
133
|
+
doesn't invoke `preuninstall` on a global `npm uninstall -g`.
|
|
134
|
+
- Optional `win-nice skill install|uninstall` - installs a reference skill
|
|
135
|
+
for Claude Code and Codex CLI.
|
|
136
|
+
- Node test suite (installer logic) and Pester integration suite (real
|
|
137
|
+
Windows process/priority/Job-Object behavior).
|
|
138
|
+
|
|
139
|
+
[Unreleased]: https://github.com/PHPCraftdream/win-nice/compare/v0.2.0...HEAD
|
|
140
|
+
[0.2.0]: https://github.com/PHPCraftdream/win-nice/compare/v0.1.0...v0.2.0
|
|
141
|
+
[0.1.0]: https://github.com/PHPCraftdream/win-nice/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -53,6 +53,16 @@ invocation resolves to depends on the calling shell:
|
|
|
53
53
|
| cmd.exe, or PATHEXT-based resolution (e.g. Node's `child_process`, which doesn't include `.PS1` in `PATHEXT` by default) | `name.bat` | corrupted before `.ps1` ever runs (see below) |
|
|
54
54
|
| POSIX shell (Git Bash only — ignores `PATHEXT`/bare-name extension resolution entirely) | `name` (no extension) | full argument safety — the shim `exec`s straight into `name.ps1` via `powershell -File`, the same direct-to-`.ps1` path PowerShell itself uses, with MSYS argument conversion disabled so slash-style switches (`/c`, `/d`) and Windows paths arrive untouched; no `.bat`/cmd.exe hop involved |
|
|
55
55
|
|
|
56
|
+
Known limitation of the extensionless shims: the `MSYS2_ARG_CONV_EXCL='*'`
|
|
57
|
+
they set to keep their own arguments intact is inherited by the wrapped
|
|
58
|
+
command and its whole process tree, so an MSYS program spawned *inside* the
|
|
59
|
+
wrapped command (an inner `bash`/`sh`, a `#!/bin/sh` git hook — not a native
|
|
60
|
+
program like `node.exe` or `cmd.exe`) inherits it too and stops converting
|
|
61
|
+
POSIX-style paths in its own children's arguments — e.g.
|
|
62
|
+
`caps 600 bash -c 'node /c/proj/run.js'` fails with `Cannot find module`
|
|
63
|
+
where the same command without the shim works. The shim's own documented
|
|
64
|
+
argument guarantee (its arguments arrive untouched) is unaffected.
|
|
65
|
+
|
|
56
66
|
The extensionless shims are Git Bash-specific; WSL is not supported — WSL has
|
|
57
67
|
no bare `powershell` (only `powershell.exe`), Windows PowerShell can't resolve
|
|
58
68
|
the `/mnt/...` script path such a shim would pass to `-File`, and a WSL-side
|
|
@@ -104,7 +114,7 @@ have it by default); without it, Windows doesn't error out, it silently downgrad
|
|
|
104
114
|
the request to `HIGH_PRIORITY_CLASS` instead — confirmed empirically. Same
|
|
105
115
|
single-process-only caveat as `abovenormal`/`high` applies on top of all that.
|
|
106
116
|
|
|
107
|
-
### `
|
|
117
|
+
### `capc <percent> <command> [args...]`
|
|
108
118
|
Hard CPU quota (1-100) for the whole process tree, enforced by a Windows Job
|
|
109
119
|
Object (`JOBOBJECT_CPU_RATE_CONTROL_INFORMATION`, hard cap). Unlike `idle`/
|
|
110
120
|
`belownormal`, this is a real ceiling on total CPU%, not just a scheduling
|
|
@@ -112,49 +122,98 @@ priority — it holds even when nothing else on the machine is contending for CP
|
|
|
112
122
|
|
|
113
123
|
The cap covers the whole subtree from its very first instruction: the wrapped
|
|
114
124
|
command is created suspended, assigned to the Job Object, and only then resumed
|
|
115
|
-
— there's no window where it runs uncapped. Every
|
|
116
|
-
children, recursively) automatically joins the
|
|
117
|
-
Object behavior on any supported Windows
|
|
118
|
-
newer ones. The
|
|
119
|
-
`CREATE_BREAKAWAY_FROM_JOB
|
|
120
|
-
breakaway-allowed flag, that fails closed — the child just
|
|
121
|
-
rather than silently escaping the cap
|
|
125
|
+
— there's no window where it runs uncapped. Every ordinary descendant created
|
|
126
|
+
via `CreateProcess` (and their children, recursively) automatically joins the
|
|
127
|
+
same job; this is standard Job Object behavior on any supported Windows
|
|
128
|
+
version, not something specific to newer ones. The known ways out: a
|
|
129
|
+
descendant explicitly requesting `CREATE_BREAKAWAY_FROM_JOB` (and since the job
|
|
130
|
+
here never sets a breakaway-allowed flag, that fails closed — the child just
|
|
131
|
+
fails to launch rather than silently escaping the cap), or a process brought
|
|
132
|
+
up through an external broker/service that never goes through the wrapped
|
|
133
|
+
tree's own `CreateProcess` calls (Microsoft documents, for example, that a
|
|
134
|
+
process started via WMI's `Win32_Process.Create` doesn't join the caller's
|
|
135
|
+
job).
|
|
122
136
|
|
|
123
137
|
Windows 8+ specifically matters if something inside the wrapped command creates
|
|
124
|
-
*its own* Job Object (some tools do, e.g. Chromium-based ones
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
*its own* Job Object (some tools do, e.g. Chromium-based ones — or these tools
|
|
139
|
+
themselves, when chained together, see "Chaining these tools together" below):
|
|
140
|
+
before Windows 8 a process could belong to only one job at a time, so that
|
|
141
|
+
inner `AssignProcessToJobObject` call would fail. Windows 8+ allows nested
|
|
142
|
+
jobs, so it succeeds instead, and both jobs' limits apply — but *how* they
|
|
143
|
+
combine depends on the limit type, not one universal "smaller wins" rule. For
|
|
144
|
+
CPU rate control specifically, a nested job's rate is relative to what its
|
|
145
|
+
parent already lets through, so equal caps **multiply**: `capc 50 capc 50 ...`
|
|
146
|
+
yields roughly 25% of total system CPU, not 50% (see "Chaining these tools
|
|
147
|
+
together" below for the full picture across limit types).
|
|
129
148
|
|
|
130
149
|
Blocks until the command exits, propagates its exit code.
|
|
131
150
|
|
|
132
151
|
```
|
|
133
|
-
|
|
152
|
+
capc 50 npm run build
|
|
134
153
|
```
|
|
135
154
|
|
|
136
|
-
### `
|
|
137
|
-
Short for **
|
|
155
|
+
### `capt <thread-count> <command> [args...]`
|
|
156
|
+
Short for **cap threads**. Restricts the whole process tree to the first
|
|
138
157
|
`<thread-count>` logical processors via Windows process affinity
|
|
139
158
|
(`JOBOBJECT_BASIC_LIMIT_INFORMATION`, `JOB_OBJECT_LIMIT_AFFINITY`) — same
|
|
140
|
-
suspend-then-assign-then-resume Job Object mechanism as `
|
|
159
|
+
suspend-then-assign-then-resume Job Object mechanism as `capc`, so the same
|
|
141
160
|
"covers the whole subtree from the first instruction" and "breakaway fails
|
|
142
161
|
closed" guarantees apply.
|
|
143
162
|
|
|
144
163
|
Deliberately *threads*, not *cores*, in both the name and the semantics:
|
|
145
164
|
Windows affinity masks address logical processors (hardware threads), not
|
|
146
|
-
physical cores. On a machine with Hyper-Threading/SMT, `
|
|
165
|
+
physical cores. On a machine with Hyper-Threading/SMT, `capt 4` pins to 4
|
|
147
166
|
*logical processors* — depending on which ones, that could be 2 fully-used
|
|
148
167
|
physical cores or 4 half-used ones; the affinity API has no concept of "whole
|
|
149
168
|
core" grouping on its own. `<thread-count>` must be between 1 and the number
|
|
150
169
|
of logical processors on the machine (`[Environment]::ProcessorCount`, capped
|
|
151
|
-
at 63 — a single affinity mask can't address more).
|
|
170
|
+
at 63 — a single affinity mask can't address more). Under 32-bit Windows
|
|
171
|
+
PowerShell (the `SysWOW64` host) the effective cap is additionally 32: the
|
|
172
|
+
affinity mask is a pointer-sized `UIntPtr`, so a 32-bit process can only
|
|
173
|
+
address 32 logical processors — counts of 33-63 are rejected up front with a
|
|
174
|
+
usage error naming 64-bit PowerShell (the same process-width limit `capm`
|
|
175
|
+
enforces on its memory cap).
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
capt 4 npm run build
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### `capm <size> <command> [args...]`
|
|
182
|
+
Hard memory ceiling for the whole process tree, enforced by a Windows Job
|
|
183
|
+
Object (`JOBOBJECT_EXTENDED_LIMIT_INFORMATION`, `JOB_OBJECT_LIMIT_JOB_MEMORY`)
|
|
184
|
+
— same suspend-then-assign-then-resume mechanism as `capc`/`capt`, so the same
|
|
185
|
+
"covers the whole subtree from the first instruction" and "breakaway fails
|
|
186
|
+
closed" guarantees apply. Caps the whole job's *aggregate* committed memory,
|
|
187
|
+
not any single process — like `capc`'s CPU% and `capt`'s affinity, it's one
|
|
188
|
+
ceiling for the whole tree, not a per-process limit.
|
|
189
|
+
|
|
190
|
+
`<size>` accepts three forms:
|
|
191
|
+
|
|
192
|
+
| Form | Meaning |
|
|
193
|
+
| --- | --- |
|
|
194
|
+
| `50` (bare integer, `1`-`100`) | percent of total physical RAM (`GlobalMemoryStatusEx`), not of whatever's currently free — the cap means the same thing regardless of what else is running on the machine at invocation time. Same convention as `capc`'s own `<percent 1-100>` — deliberately no `%` character; see "Chaining these tools together" below for why |
|
|
195
|
+
| `512m` / `512M` | megabytes |
|
|
196
|
+
| `2g` / `2G` | gigabytes |
|
|
152
197
|
|
|
153
198
|
```
|
|
154
|
-
|
|
199
|
+
capm 50 npm run build
|
|
200
|
+
capm 512m npm run build
|
|
201
|
+
capm 2g npm run build
|
|
155
202
|
```
|
|
156
203
|
|
|
157
|
-
**
|
|
204
|
+
**Unlike `capc`, exceeding the limit doesn't throttle — it fails the
|
|
205
|
+
allocation.** A CPU cap just makes things slower; a memory cap that's
|
|
206
|
+
exceeded causes the *allocation call itself* to fail (`VirtualAlloc`-family
|
|
207
|
+
APIs return an error, .NET throws `OutOfMemoryException`) rather than the OS
|
|
208
|
+
gracefully degrading anything. Most programs don't handle allocation failure
|
|
209
|
+
cleanly, so in practice this usually looks like a crash. Set it too low and
|
|
210
|
+
even the wrapped program's own runtime can fail to start (confirmed: capping
|
|
211
|
+
Windows PowerShell 5.1 itself at 30 MB crashes it with
|
|
212
|
+
`StackOverflowException` before it can run anything) — leave headroom above
|
|
213
|
+
whatever interpreter/runtime the wrapped command needs just to start, on top
|
|
214
|
+
of what your actual workload needs.
|
|
215
|
+
|
|
216
|
+
**A `capc`/`capt`/`capm`/`capn` limit sticks to any daemon the wrapped command leaves
|
|
158
217
|
running**, for that daemon's entire lifetime — not just for the wrapped
|
|
159
218
|
command's own run. Job Object membership is permanent for a process once
|
|
160
219
|
assigned (short of an explicit, disallowed breakaway); a background process
|
|
@@ -163,19 +222,196 @@ for as long as it stays alive. This bites build tools that reuse a persistent
|
|
|
163
222
|
process across invocations to skip cold-start cost: `dotnet build`'s
|
|
164
223
|
`VBCSCompiler`/MSBuild node reuse, a Gradle daemon, file-watcher processes
|
|
165
224
|
left running by `npm run watch`-style scripts. A follow-up **uncapped**
|
|
166
|
-
`dotnet build` (or `gradle`) can end up running inside the *previous* `
|
|
167
|
-
call's Job Object without a new `
|
|
168
|
-
because a stale daemon from an earlier call is doing the work. Either
|
|
169
|
-
leave the daemon running across a
|
|
170
|
-
|
|
171
|
-
|
|
225
|
+
`dotnet build` (or `gradle`) can end up running inside the *previous* `capc`
|
|
226
|
+
call's Job Object without a new `capc`/`capt`/`capm` invocation of its own,
|
|
227
|
+
capped because a stale daemon from an earlier call is doing the work. Either
|
|
228
|
+
don't leave the daemon running across a call whose limit shouldn't persist
|
|
229
|
+
(`dotnet build -p:UseSharedCompilation=false`, `gradle --no-daemon`), or
|
|
230
|
+
accept that the limit is now effectively attached to the daemon until it's
|
|
172
231
|
killed.
|
|
173
232
|
|
|
233
|
+
### `caps <seconds> <command> [args...]`
|
|
234
|
+
Runs the command with a wall-clock deadline: if it's still running when
|
|
235
|
+
`<seconds>` have passed, it is force-killed and `caps` exits with code **124** (the unix
|
|
236
|
+
`timeout(1)` convention), printing
|
|
237
|
+
`caps: timed out after <seconds>s - job and every process in it were force-killed`
|
|
238
|
+
to stderr. If the command finishes in time, `caps` propagates its exit code
|
|
239
|
+
exactly like every other launcher in this family.
|
|
240
|
+
|
|
241
|
+
The deadline is genuinely wall-clock: `caps` computes it once as an absolute
|
|
242
|
+
UTC timestamp and arms a one-shot waitable timer with that absolute due time
|
|
243
|
+
(`SetWaitableTimer`), then waits on the timer and the wrapped process together
|
|
244
|
+
(`WaitForMultipleObjects`). An absolute timer due time is a property of the
|
|
245
|
+
wall clock, not of an in-progress wait, so time the machine spends
|
|
246
|
+
asleep/suspended counts against it: if the deadline passes during sleep, the
|
|
247
|
+
timer is already signaled when the machine wakes, and `caps` fires immediately
|
|
248
|
+
instead of waiting out any leftover countdown (a relative `WaitForSingleObject`
|
|
249
|
+
wait does not count sleep time on Windows 8+ and keeps counting its pre-sleep
|
|
250
|
+
remainder after the wake). If the two signals race - a child exiting right
|
|
251
|
+
around the deadline - `caps` asks the kernel for the process's real exit time
|
|
252
|
+
(`GetProcessTimes`) and only accepts it as on-time if it actually finished at
|
|
253
|
+
or before the deadline. Because that due time is a value on the system clock,
|
|
254
|
+
a manual or service-driven system-clock adjustment during the wait moves it
|
|
255
|
+
with the clock: the actual wait can come out shorter or longer than the
|
|
256
|
+
requested `<seconds>` (sleep/suspend still counts correctly - only clock
|
|
257
|
+
adjustments shift the deadline).
|
|
258
|
+
|
|
259
|
+
The same "covers the whole subtree from the first instruction" guarantee
|
|
260
|
+
applies: same suspend-then-assign-then-resume Job Object mechanism as
|
|
261
|
+
`capc`/`capt`/`capm`, so at expiry a single `TerminateJobObject` kernel call
|
|
262
|
+
kills the direct child *and every descendant it spawned* — no process-tree
|
|
263
|
+
walking, no window where a grandchild outlives the child. The job carries only
|
|
264
|
+
`JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`, which doubles as the backstop if the
|
|
265
|
+
`caps` wrapper itself is killed non-cooperatively (`taskkill` without `/T`, a
|
|
266
|
+
crash): Windows itself then terminates the whole job at that moment. That
|
|
267
|
+
backstop is released before a normal, inside-the-deadline exit, so a
|
|
268
|
+
background daemon the wrapped command legitimately left running survives -
|
|
269
|
+
`caps` only forces a whole-tree kill on its own timeout or on the wrapper's
|
|
270
|
+
own non-cooperative death, never on an ordinary successful exit.
|
|
271
|
+
|
|
272
|
+
`<seconds>` accepts a positive whole or decimal number (`2`, `2.5`), converted
|
|
273
|
+
to whole milliseconds (floored; minimum 1 ms). The maximum is 4294967294 ms
|
|
274
|
+
(~49.7 days) — a deliberate usage ceiling, not a Win32 API limit: the deadline
|
|
275
|
+
is an absolute 64-bit `FILETIME` armed into a waitable timer, and the
|
|
276
|
+
`WaitForMultipleObjects` wait itself always passes `INFINITE` (the timer is
|
|
277
|
+
what bounds it). Anything larger is rejected as a usage error (exit 1), never
|
|
278
|
+
silently truncated into a
|
|
279
|
+
different deadline.
|
|
280
|
+
|
|
281
|
+
**`caps` sets no resource limit of any kind** — no CPU, memory, priority, or
|
|
282
|
+
affinity limit. It is not a quota tool: `capc`/`capt`/`capm` deliberately
|
|
283
|
+
impose no timeout (a quota tool shouldn't unilaterally decide a legitimate long
|
|
284
|
+
build is stuck — same precedent as `nice`/`cpulimit`); `caps` is the
|
|
285
|
+
complement, for when *you* have already decided that N seconds is the limit
|
|
286
|
+
(see the system-clock caveat above).
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
caps 300 npm test
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### `capn <count> <command> [args...]`
|
|
293
|
+
Runs the command under a hard ceiling on the number of **simultaneously active
|
|
294
|
+
processes** in its whole process tree (`JOBOBJECT_BASIC_LIMIT_INFORMATION`,
|
|
295
|
+
`JOB_OBJECT_LIMIT_ACTIVE_PROCESS`) — same suspend-then-assign-then-resume Job
|
|
296
|
+
Object mechanism as `capc`/`capt`/`capm`, so the same "covers the whole subtree
|
|
297
|
+
from the first instruction" and "breakaway fails closed" guarantees apply.
|
|
298
|
+
|
|
299
|
+
The count **includes the directly wrapped process itself**: it is assigned to
|
|
300
|
+
the still-empty job before it can spawn anything, so `capn 1 <command>` runs
|
|
301
|
+
the command but the instant it tries to spawn any child — including
|
|
302
|
+
infrastructure children like PowerShell's own `Add-Type` compiler (`csc.exe`,
|
|
303
|
+
plus the `CVTRES.EXE` that compiler runs) — that spawn attempt fails.
|
|
304
|
+
Confirmed empirically, not just from docs: under `capn 1`, a wrapped
|
|
305
|
+
PowerShell's `Start-Process` fails with "Not enough quota is available to
|
|
306
|
+
process this command." and the parent keeps running and exits 0.
|
|
307
|
+
|
|
308
|
+
Unlike a `capc` throttle, exceeding the limit doesn't degrade anything — the
|
|
309
|
+
offending spawn attempt itself is what fails (closer to `capm`'s failed
|
|
310
|
+
allocation): nothing is killed, nothing already running is affected, and the
|
|
311
|
+
wrapped process sees an ordinary process-creation failure from its own spawn
|
|
312
|
+
call. A command that stays within the budget is completely unaffected —
|
|
313
|
+
`capn 3` wrapping a parent that spawns 2 children runs exactly like an
|
|
314
|
+
uncapped one (also confirmed empirically).
|
|
315
|
+
|
|
316
|
+
`<count>` is a positive whole number, minimum 1, maximum 4294967295 — the
|
|
317
|
+
`ActiveProcessLimit` struct field is a uint32, so anything larger (or
|
|
318
|
+
negative, or non-numeric) is a usage error (exit 1), never a silently
|
|
319
|
+
truncated limit.
|
|
320
|
+
|
|
321
|
+
```
|
|
322
|
+
capn 10 npm run build
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Chaining these tools together
|
|
326
|
+
|
|
327
|
+
These tools can be stacked by passing one as another's `<command>`:
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
capm 50 capc 50 idle npm run build
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Each wrapper wraps everything after its own arguments, so the outermost
|
|
334
|
+
wrapper is whichever one you type first. A few things to know before relying
|
|
335
|
+
on a combination:
|
|
336
|
+
|
|
337
|
+
**Bare tool names resolve through the same `cmd.exe`/`PATHEXT` fallback
|
|
338
|
+
documented above.** None of these ship a `.exe`, so e.g. `capm`'s attempt to
|
|
339
|
+
launch `capc` directly always fails and falls back to `cmd.exe`, which finds
|
|
340
|
+
`capc.bat` via `PATHEXT` — meaning chaining only works when the tools'
|
|
341
|
+
install directory is actually on `PATH`, and any `%` elsewhere on that
|
|
342
|
+
command line trips the same fail-closed check described above. `capm`'s own
|
|
343
|
+
`<size>` deliberately has no `%` form for exactly this reason: an earlier
|
|
344
|
+
version accepted `25%`, and `capc 50 capm 25% ...` failed the fail-closed
|
|
345
|
+
check while `capm 25% capc 50 ...` worked fine — an order-dependent foot-gun.
|
|
346
|
+
A bare percent (`capm 50 capc 50 ...`) sidesteps it entirely, in any order.
|
|
347
|
+
|
|
348
|
+
**Nested Job Object limits do not follow one universal "smaller wins" rule —
|
|
349
|
+
each limit type combines differently:**
|
|
350
|
+
|
|
351
|
+
- **CPU (`capc`)**: a nested job's CPU rate is relative to what its parent
|
|
352
|
+
already lets through, so equal caps *multiply* rather than take the
|
|
353
|
+
minimum — `capc 50 capc 50 ...` yields roughly 25% of total system CPU, not
|
|
354
|
+
50%. This is documented Windows behavior for
|
|
355
|
+
[`JOBOBJECT_CPU_RATE_CONTROL_INFORMATION`](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_cpu_rate_control_information),
|
|
356
|
+
not a bug here.
|
|
357
|
+
- **Memory (`capm`)**: each ceiling applies independently to its own
|
|
358
|
+
accounting scope, and those scopes are *not* the same size — a job's
|
|
359
|
+
committed-memory accounting includes its own processes **plus every child
|
|
360
|
+
job's committed memory**, while a child job's own accounting doesn't see
|
|
361
|
+
the outer wrapper's process at all
|
|
362
|
+
([Nested Jobs — Resource Accounting](https://learn.microsoft.com/en-us/windows/win32/procthread/nested-jobs)).
|
|
363
|
+
So `capm 500m capm 400m ...` is *not* guaranteed to behave like a plain
|
|
364
|
+
`min(500m, 400m) = 400m` ceiling on the innermost workload — the outer
|
|
365
|
+
500m job can run out of budget first purely from its own wrapper
|
|
366
|
+
process's memory use, on top of whatever the inner 400m job is using.
|
|
367
|
+
Don't rely on nested `capm` ceilings combining to an exact number; treat
|
|
368
|
+
the outer value as an upper bound that can bind earlier than expected.
|
|
369
|
+
- **Priority (`idle`/`belownormal`/`abovenormal`/`high`/`realtime`)**: not a
|
|
370
|
+
Job Object limit — the *last* one applied to a given process simply wins,
|
|
371
|
+
same as invoking any one of them alone.
|
|
372
|
+
- **Affinity (`capt`)**: nested affinity is an *effective-limits* chain — a
|
|
373
|
+
child job's mask can be as tight as it wants but is clamped to whatever
|
|
374
|
+
the parent already allows, never wider
|
|
375
|
+
([Nested Jobs — Job Limits](https://learn.microsoft.com/en-us/windows/win32/procthread/nested-jobs)).
|
|
376
|
+
Confirmed empirically: `capt 2 capt 3 ...` (inner asking for *more*
|
|
377
|
+
processors than the outer allows) still comes back pinned to the outer's
|
|
378
|
+
2, not the inner's 3 — no error, just silently clamped to the tighter mask.
|
|
379
|
+
- **Process count (`capn`)**: each job enforces its own `ActiveProcessLimit`
|
|
380
|
+
independently against its own simultaneously-active count — a spawn has to
|
|
381
|
+
fit under *every* job in the chain at once, and an outer job's count
|
|
382
|
+
includes the inner wrapper process itself plus everything beneath it. Not a
|
|
383
|
+
"silently clamped to the tighter limit" rule like affinity, and not a plain
|
|
384
|
+
`min()`: confirmed empirically, `capn 1 capn 5 ...` fails outright (the
|
|
385
|
+
outer job is already full with the inner wrapper alone, so the inner
|
|
386
|
+
wrapper can't even start its target — for a PowerShell-based inner tool
|
|
387
|
+
this surfaces as its own `Add-Type`/`csc.exe` child spawn being refused,
|
|
388
|
+
exit 1); `capn 2 capn 5 ...` still fails one step later (the compiler's own
|
|
389
|
+
`CVTRES.EXE` child doesn't fit); `capn 3 capn 5 ...` works. In the other
|
|
390
|
+
direction, `capn 5 capn 1 <cmd-that-spawns>` runs the command but its child
|
|
391
|
+
spawn is refused by the *inner* limit while the outer still has room. Leave
|
|
392
|
+
real headroom in an outer `capn` for the chain itself — roughly 3 slots
|
|
393
|
+
before a PowerShell-based inner tool's actual workload even starts.
|
|
394
|
+
- **Timeout (`caps`)**: not a Job Object limit being combined at all — each
|
|
395
|
+
`caps` enforces its own deadline on its direct child. The innermost `caps`
|
|
396
|
+
wrapping the eventual work fires at its own deadline and the outer one
|
|
397
|
+
propagates the inner's 124 exit code like any other tool's. If the *outer*
|
|
398
|
+
deadline fires first, its `TerminateJobObject` kills the whole subtree
|
|
399
|
+
including the inner `caps` wrapper — and everything the inner wrapper had
|
|
400
|
+
assigned to its own job dies with it via that inner job's own
|
|
401
|
+
`KILL_ON_JOB_CLOSE` flag (the same cascade documented above). Either way the
|
|
402
|
+
caller sees 124.
|
|
403
|
+
|
|
404
|
+
Test any combination you actually plan to depend on; don't assume "more
|
|
405
|
+
wrappers, more restrictive" holds uniformly across limit types.
|
|
406
|
+
|
|
174
407
|
### `uiup`
|
|
175
|
-
One-shot priority boost (`HIGH`) for the live shell/UI/audio processes
|
|
176
|
-
desktop
|
|
177
|
-
`explorer`, `dwm`, `sihost`, `ShellExperienceHost`,
|
|
178
|
-
`StartMenu`, `SearchApp`, `audiodg`.
|
|
408
|
+
One-shot priority boost (`HIGH`) for the live shell/UI/audio processes,
|
|
409
|
+
intended to improve desktop responsiveness while heavy background work runs
|
|
410
|
+
underneath: `explorer`, `dwm`, `sihost`, `ShellExperienceHost`,
|
|
411
|
+
`StartMenuExperienceHost`, `StartMenu`, `SearchApp`, `audiodg`. This is a
|
|
412
|
+
best-effort one-shot tweak, not a guarantee — memory pressure, I/O
|
|
413
|
+
saturation, driver/GPU stalls, or a realtime-priority workload elsewhere can
|
|
414
|
+
still make the desktop stutter regardless.
|
|
179
415
|
|
|
180
416
|
Self-elevates via UAC — `dwm`/`sihost` run under a separate account
|
|
181
417
|
(`Window Manager\DWM-1`), so raising their priority needs admin rights.
|
|
@@ -186,10 +422,11 @@ The boost does **not** propagate to apps you launch from Explorer afterwards:
|
|
|
186
422
|
see the project history for the test.
|
|
187
423
|
|
|
188
424
|
### `admin <command> [args...]`
|
|
189
|
-
Runs `command` elevated (as Administrator), waits for it to exit, propagates
|
|
190
|
-
exit code —
|
|
191
|
-
|
|
192
|
-
|
|
425
|
+
Runs `command` elevated (as Administrator), waits for it to exit, propagates
|
|
426
|
+
its exit code — a blocking elevation wrapper with the same wait-and-propagate
|
|
427
|
+
semantics as the other wrappers here, but it does not set a priority class
|
|
428
|
+
(unlike `idle`). If it's already elevated, runs inline sharing the current
|
|
429
|
+
console, with the full direct-launch argument safety described above.
|
|
193
430
|
|
|
194
431
|
If the calling shell isn't already elevated, triggers the standard UAC consent
|
|
195
432
|
prompt (via `ShellExecute`, always opening its own console window, incompatible
|
|
@@ -240,7 +477,7 @@ either order — `npx` re-fetches the package to run it, so it still works even
|
|
|
240
477
|
after the global package itself is gone.
|
|
241
478
|
|
|
242
479
|
The commands themselves are never registered through npm's own global `bin`
|
|
243
|
-
shimming — `idle`/`
|
|
480
|
+
shimming — `idle`/`capc`/etc. are too generic a name to risk colliding with
|
|
244
481
|
someone else's global npm package. npm here is only the delivery mechanism for
|
|
245
482
|
a dedicated, PATH-managed install directory.
|
|
246
483
|
|
|
@@ -269,7 +506,12 @@ npx win-nice skill install # add the win-nice reference skill
|
|
|
269
506
|
npx win-nice skill uninstall # remove it
|
|
270
507
|
```
|
|
271
508
|
|
|
272
|
-
|
|
509
|
+
Initial installation is a separate, opt-in step — `postinstall` never creates a
|
|
510
|
+
skill copy for a user who hasn't run `skill install`. Once a copy exists,
|
|
511
|
+
though, every subsequent ordinary package install/upgrade (including
|
|
512
|
+
`postinstall`) automatically refreshes that *existing* marked copy to the new
|
|
513
|
+
version's content, so it never goes stale after an upgrade. Explicit `skill
|
|
514
|
+
install` copies
|
|
273
515
|
[`skills/win-nice/SKILL.md`](skills/win-nice/SKILL.md) (documents every tool
|
|
274
516
|
above except `cy`/`cx`) to `~/.claude/skills/win-nice/SKILL.md` and
|
|
275
517
|
`~/.agents/skills/win-nice/SKILL.md` (Codex CLI's personal-skill location) —
|
|
@@ -315,16 +557,22 @@ Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
|
|
|
315
557
|
- `WIN_NICE_HOME` — overrides the install root (default
|
|
316
558
|
`%LOCALAPPDATA%\win-nice`). Used by the test suite; also useful for a
|
|
317
559
|
non-default install location.
|
|
318
|
-
- `WIN_NICE_SKILL_HOME` — overrides the home directory
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
skill
|
|
560
|
+
- `WIN_NICE_SKILL_HOME` — overrides the home directory `~/.claude/skills/...`
|
|
561
|
+
and `~/.agents/skills/...` are resolved against (default: the real user
|
|
562
|
+
home). Mirrors `WIN_NICE_HOME`, for the skill files instead of `bin/`.
|
|
563
|
+
Affects both the explicit `skill install`/`skill uninstall` commands and the
|
|
564
|
+
automatic postinstall/upgrade refresh of an already-installed copy described
|
|
565
|
+
above.
|
|
322
566
|
- `WIN_NICE_NO_PATH` — if set (to anything), `install`/`uninstall`/
|
|
323
567
|
`reinstall` skip the user `PATH` update/removal entirely, only managing
|
|
324
568
|
files under the install directory.
|
|
325
569
|
|
|
326
570
|
## Testing
|
|
327
571
|
|
|
572
|
+
From a source checkout (`test/` and `scripts/` aren't part of the published
|
|
573
|
+
npm package - the `test`/`test:elevated`/`release-check` scripts below only
|
|
574
|
+
work when run from a git clone, not against an installed package):
|
|
575
|
+
|
|
328
576
|
```
|
|
329
577
|
npm test # installer logic (fast; isolated scratch registry key, cleaned up automatically)
|
|
330
578
|
powershell -Command "Invoke-Pester -Path test\win-nice.Tests.ps1" # real tool behavior
|
|
@@ -350,6 +598,26 @@ CPU-cap test retries a few times if the machine is too busy to get a clean
|
|
|
350
598
|
baseline). It never touches the real system `PATH`/registry; the installer
|
|
351
599
|
tests use `WIN_NICE_HOME` to redirect installs into a temp directory instead.
|
|
352
600
|
|
|
601
|
+
3 of its cases only exercise `admin.ps1`'s already-elevated branch, which
|
|
602
|
+
needs the whole test-runner process to already be elevated (not just
|
|
603
|
+
`admin.ps1` itself) - a normal, unelevated run reports them `Skipped`, which
|
|
604
|
+
is expected, not a failure. A separate, disjoint set of 4 cases only makes
|
|
605
|
+
sense when NOT elevated, and `Skip`s under elevation instead. `npm run
|
|
606
|
+
test:elevated` (`test/run-elevated.ps1`) is a single entry point for the
|
|
607
|
+
first group: one UAC prompt elevates the runner once, then the suite runs
|
|
608
|
+
inside that elevated session, activating those 3 cases (and skipping the
|
|
609
|
+
other 4). Neither a normal run nor an elevated run alone exercises every
|
|
610
|
+
case - run both for full coverage.
|
|
611
|
+
|
|
612
|
+
`npm run release-check` (`scripts/release-check.js`) is a maintainer-only,
|
|
613
|
+
source-checkout-only command that packs the actual npm tarball, installs it
|
|
614
|
+
into an isolated temp directory, and verifies the real installed artifact
|
|
615
|
+
(launcher file set, manifest version, `capc`/`capt`/`capm`/`caps`/`capn` exit-code smoke
|
|
616
|
+
tests, and CHANGELOG/tag consistency) - this is what `publish.yml` runs right
|
|
617
|
+
before `npm publish`. It needs `scripts/` and git tag history, neither of
|
|
618
|
+
which is part of the published package, so it can't run against an installed
|
|
619
|
+
copy.
|
|
620
|
+
|
|
353
621
|
## License
|
|
354
622
|
|
|
355
623
|
Dual-licensed under [MIT](LICENSE-MIT) or [Apache License, Version 2.0](LICENSE-APACHE),
|
package/bin/abovenormal
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")/abovenormal.ps1" 2>/dev/null) || script="$(dirname "$0")/abovenormal.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")/abovenormal.ps1" 2>/dev/null) || script="$(dirname "$0")/abovenormal.ps1"
|
|
11
|
+
exec powershell -NoProfile -ExecutionPolicy Bypass -File "$script" "$@"
|
package/bin/abovenormal.bat
CHANGED
|
@@ -5,9 +5,9 @@ if "%~1"=="" (
|
|
|
5
5
|
echo usage: abovenormal ^<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
|
|
11
11
|
:: "abovenormal" bare from an actual PowerShell session skips this file
|
|
12
12
|
:: (abovenormal.ps1 preferred).
|
|
13
|
-
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0abovenormal.ps1" %*
|
|
13
|
+
"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "%~dp0abovenormal.ps1" %*
|