win-nice 0.1.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.
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const paths = require('./paths');
5
+ const manifest = require('./manifest');
6
+
7
+ // Rejects any candidate that doesn't resolve inside expectedDir - guards against a
8
+ // corrupted/tampered manifest (absolute paths, "../.." traversal) pointing deletion
9
+ // outside the install directory.
10
+ function isInsideDir(candidate, expectedDir) {
11
+ const relative = path.relative(path.resolve(expectedDir), path.resolve(candidate));
12
+ return relative !== '' && relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative);
13
+ }
14
+
15
+ function removeManagedFile(filePath, expectedDir, { requireMarker }) {
16
+ if (!isInsideDir(filePath, expectedDir)) {
17
+ return { file: filePath, removed: false, reason: 'rejected (escapes install directory)' };
18
+ }
19
+ if (!fs.existsSync(filePath)) return { file: filePath, removed: false, reason: 'missing' };
20
+ if (requireMarker && !manifest.hasMarker(filePath)) {
21
+ return { file: filePath, removed: false, reason: 'marker missing (modified by user?)' };
22
+ }
23
+ fs.unlinkSync(filePath);
24
+ return { file: filePath, removed: true };
25
+ }
26
+
27
+ function uninstall({ updatePath = true } = {}) {
28
+ const dir = paths.binDir();
29
+ const manifestFile = paths.manifestPath();
30
+ const data = manifest.read(manifestFile);
31
+
32
+ let candidates;
33
+ let requireMarker;
34
+ if (data && Array.isArray(data.files)) {
35
+ // Files tracked by a valid manifest are owned by the package - remove them
36
+ // regardless of local edits (reinstall is expected to replace them anyway).
37
+ candidates = data.files.map((name) => path.join(data.binDir || dir, name));
38
+ requireMarker = false;
39
+ } else if (fs.existsSync(dir)) {
40
+ // Manifest missing/corrupt - fall back to scanning the known install dir. This
41
+ // scan can hit files we didn't put there, so only remove ones that still carry
42
+ // the marker.
43
+ candidates = fs.readdirSync(dir).map((name) => path.join(dir, name));
44
+ requireMarker = true;
45
+ } else {
46
+ candidates = [];
47
+ requireMarker = true;
48
+ }
49
+
50
+ const results = candidates.map((candidate) => removeManagedFile(candidate, dir, { requireMarker }));
51
+ for (const r of results) {
52
+ console.log(r.removed ? `removed ${r.file}` : `skipped ${r.file} (${r.reason})`);
53
+ }
54
+
55
+ if (fs.existsSync(dir) && fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
56
+ if (fs.existsSync(manifestFile)) fs.unlinkSync(manifestFile);
57
+
58
+ // Leaves %LOCALAPPDATA%\win-nice itself behind otherwise - only bin/ and the
59
+ // manifest were ever tracked. Only remove it if uninstall left it truly empty;
60
+ // a user-added file there must survive.
61
+ const root = paths.installRoot();
62
+ if (fs.existsSync(root) && fs.readdirSync(root).length === 0) fs.rmdirSync(root);
63
+
64
+ if (updatePath) {
65
+ const current = paths.readUserPath();
66
+ const next = paths.removeFromPathString(current, dir);
67
+ if (next !== current) {
68
+ paths.writeUserPath(next);
69
+ console.log(`Removed ${dir} from your PATH.`);
70
+ }
71
+ }
72
+
73
+ return results;
74
+ }
75
+
76
+ module.exports = { uninstall, removeManagedFile };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "win-nice",
3
+ "version": "0.1.0",
4
+ "description": "Windows nice/renice/cpulimit for parallel AI coding agents - process priority (idle/belownormal/abovenormal/high/realtime), hard CPU quotas and thread affinity via Job Objects, elevation (admin), a UI responsiveness boost, and an optional Claude Code/Codex reference skill, with no dependencies.",
5
+ "license": "MIT OR Apache-2.0",
6
+ "author": {
7
+ "name": "Marat K",
8
+ "url": "https://github.com/PHPCraftdream"
9
+ },
10
+ "os": [
11
+ "win32"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/PHPCraftdream/win-nice.git"
16
+ },
17
+ "homepage": "https://github.com/PHPCraftdream/win-nice#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/PHPCraftdream/win-nice/issues"
20
+ },
21
+ "bin": {
22
+ "win-nice": "install/cli.js"
23
+ },
24
+ "files": [
25
+ "bin",
26
+ "install",
27
+ "skills",
28
+ "README.md",
29
+ "LICENSE-MIT",
30
+ "LICENSE-APACHE"
31
+ ],
32
+ "scripts": {
33
+ "postinstall": "node install/cli.js install",
34
+ "test": "node --test test/cli.test.js test/docs-sync.test.js test/gitbash-shims.test.js test/install-uninstall.test.js test/manifest.test.js test/paths.test.js test/skill.test.js"
35
+ },
36
+ "keywords": [
37
+ "windows",
38
+ "priority",
39
+ "cpu",
40
+ "job-object",
41
+ "nice",
42
+ "cpulimit",
43
+ "agents"
44
+ ],
45
+ "engines": {
46
+ "node": ">=18"
47
+ }
48
+ }
@@ -0,0 +1,132 @@
1
+ ---
2
+ name: win-nice
3
+ description: Reference for win-nice's Windows CLI tools for process priority, hard CPU quotas, and CPU affinity (idle, belownormal, abovenormal, high, realtime, cap, pint, uiup, admin). Use when the user asks how to limit CPU usage, priority, or thread/core affinity for a command on Windows, wants to avoid a build/test freezing the desktop, or mentions any of these tool names.
4
+ ---
5
+
6
+ <!-- win-nice: managed-skill -->
7
+
8
+ # win-nice
9
+
10
+ Windows equivalent of the unix `nice` / `renice` / `cpulimit` family. Built for
11
+ running several parallel builds/tests/AI coding agents on one Windows box without
12
+ pegging every core and freezing the desktop (mouse, keyboard, window dragging,
13
+ audio - all of it).
14
+
15
+ Every tool below runs `<command> [args...]`, blocks until it exits, and
16
+ propagates its exit code. None of them need Node.js at runtime.
17
+
18
+ ## Priority tools
19
+
20
+ Windows priority classes, lowest to highest. `idle`/`belownormal` propagate to
21
+ the *whole* process tree (Windows inherits `IDLE`/`BELOW_NORMAL` into child
22
+ processes by default). `abovenormal`/`high`/`realtime` do **not** propagate -
23
+ only the directly wrapped process gets the boosted priority; anything it spawns
24
+ runs at ordinary `Normal` priority. Confirmed empirically, not just from docs.
25
+
26
+ - `idle <command> [args...]` — `IDLE_PRIORITY_CLASS`.
27
+ - `belownormal <command> [args...]` — `BELOW_NORMAL_PRIORITY_CLASS`, a lighter
28
+ touch than `idle`.
29
+ - `abovenormal <command> [args...]` — `ABOVE_NORMAL_PRIORITY_CLASS`. Single
30
+ process only (see above) — pick this over `high`/`realtime` when you just need
31
+ a mild boost for one foreground process.
32
+ - `high <command> [args...]` — `HIGH_PRIORITY_CLASS`. Single process only.
33
+ - `realtime <command> [args...]` — `REALTIME_PRIORITY_CLASS`. **Dangerous**:
34
+ outranks the OS's own input/audio/UI threads: a busy realtime process can
35
+ freeze the whole desktop, the exact failure mode win-nice otherwise exists to
36
+ prevent. Needs `SeIncreaseBasePriorityPrivilege` (elevated processes have it
37
+ by default) — without it, Windows silently downgrades the request to `HIGH`
38
+ instead of erroring. Single process only, same as `high`.
39
+
40
+ ## Resource limits
41
+
42
+ Job-Object-based; cover the *whole* process tree from the wrapped command's
43
+ first instruction (created suspended, assigned to the job, only then resumed —
44
+ no race window), including anything it spawns, recursively.
45
+
46
+ - `cap <percent 1-100> <command> [args...]` — hard CPU quota
47
+ (`JOBOBJECT_CPU_RATE_CONTROL_INFORMATION`, hard cap). A real ceiling on total
48
+ CPU%, not just scheduling priority — holds even when nothing else is
49
+ contending for CPU. Example: `cap 50 npm run build`.
50
+ - `pint <thread-count> <command> [args...]` — short for "pin threads": restricts
51
+ the whole tree to the first N *logical processors* via process affinity
52
+ (`JOB_OBJECT_LIMIT_AFFINITY`). Threads, not physical cores — on
53
+ Hyper-Threading/SMT hardware, N logical processors can be fewer physical
54
+ cores. `<thread-count>` must be between 1 and
55
+ `min([Environment]::ProcessorCount, 63)`. Example: `pint 4 npm run build`.
56
+
57
+ **A limit sticks to any daemon the wrapped command leaves running**, for that
58
+ daemon's whole lifetime, not just the one `cap`/`pint` call — Job Object
59
+ membership is permanent once assigned. Build tools that reuse a background
60
+ process to skip cold-start cost (`dotnet build`'s VBCSCompiler/MSBuild node
61
+ reuse, a Gradle daemon, `npm run watch`-style file watchers) can leave a
62
+ *later, uncapped-looking* invocation actually running inside an earlier
63
+ `cap`/`pint` call's job. Escape hatches: `dotnet build
64
+ -p:UseSharedCompilation=false`, `gradle --no-daemon` — or accept the daemon
65
+ stays limited until it's killed.
66
+
67
+ ## Elevation / desktop responsiveness
68
+
69
+ - `admin <command> [args...]` — runs elevated (as Administrator); triggers the
70
+ standard UAC consent prompt if the calling shell isn't already elevated, runs
71
+ inline with no extra prompt if it is. The elevated equivalent of `idle`.
72
+ - `uiup` (no arguments) — one-shot `HIGH` priority boost for the live
73
+ shell/UI/audio processes (`explorer`, `dwm`, `sihost`,
74
+ `ShellExperienceHost`, `StartMenuExperienceHost`, `StartMenu`, `SearchApp`,
75
+ `audiodg`) so the desktop stays responsive while heavy background work runs
76
+ underneath. Self-elevates via UAC. Does **not** affect apps launched from
77
+ Explorer afterward (`HIGH` isn't inherited by default).
78
+
79
+ ## Argument safety
80
+
81
+ Every tool tries to launch the wrapped command directly first (no shell
82
+ involved) and only falls back to `cmd.exe /c` when the target is a `.bat`/
83
+ `.cmd` file or a cmd.exe builtin that genuinely needs one.
84
+
85
+ **Direct-launch path** (the common case: a real `.exe`): `&`, `|`, `<`, `>`,
86
+ `^`, `%`, quotes, spaces, and empty strings all pass through exactly as
87
+ given — cmd.exe is never invoked, so there's nothing to expand.
88
+
89
+ **`cmd.exe /c` fallback path** (`.bat`/`.cmd` targets or cmd.exe builtins
90
+ only): `&|<>^`/quotes/spaces/empty strings are still fully protected. A
91
+ literal `%` used to be able to trigger environment-variable expansion here;
92
+ there's no reliable per-character escape for that at the `cmd.exe /c` level.
93
+ This path now **fails closed** instead: if any argument contains `%`, the
94
+ tool refuses to run, prints an error to stderr, and exits with code `1` — the
95
+ command never reaches cmd.exe.
96
+
97
+ **Separately, and unaffected by the fail-closed fix above:** each tool ships
98
+ as up to three files, and which one a bare `name ...` invocation resolves to
99
+ depends on the calling shell:
100
+
101
+ | Calling shell | Resolves to | `%` handling |
102
+ | --- | --- | --- |
103
+ | PowerShell | `name.ps1` | full argument safety (see above) |
104
+ | cmd.exe, or PATHEXT-based resolution (e.g. Node's `child_process` — `PATHEXT` doesn't include `.PS1` by default) | `name.bat` | corrupted before `.ps1` ever runs |
105
+ | POSIX shell (Git Bash only — ignores `PATHEXT`; WSL not supported) | `name` (extensionless shim) | full argument safety — `exec`s straight into `name.ps1` with MSYS argument conversion disabled, same as PowerShell |
106
+
107
+ The `.bat` file corrupts any literal `%` in its arguments before the command,
108
+ and before `.ps1` (and its fail-closed `%` check), ever runs at all
109
+ (cmd.exe's own batch-parameter substitution rescanning for `%...%` patterns
110
+ while parsing the `.bat` entry point itself; not fixable from inside a
111
+ `.bat`). Every other special character survives that hop untouched.
112
+
113
+ **PowerShell execution policy:** Windows client editions default to
114
+ `Restricted`, which blocks a bare `.ps1` invoked directly by PowerShell with
115
+ "running scripts is disabled on this system". One-time fix:
116
+ `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`. `.bat` files and the
117
+ Git Bash shims are unaffected — both pass `-ExecutionPolicy Bypass`
118
+ themselves (each shim runs powershell with that flag explicitly). Group
119
+ Policy can still override this in managed environments.
120
+
121
+ ## Install / manage
122
+
123
+ ```
124
+ npm install -g win-nice # puts the tools above on PATH
125
+ npx win-nice status # what's installed, where, which version
126
+ npx win-nice reinstall # re-copy from the current package version
127
+ npx win-nice uninstall # remove files + PATH entry
128
+ ```
129
+
130
+ Not covered here: `cy`/`cx`, unrelated one-line launchers for
131
+ `claude --dangerously-skip-permissions` / `codex --dangerously-bypass-approvals-and-sandbox`
132
+ that happen to ship alongside these tools.