tinyps 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tinylibs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # ⚙️ tinyps
2
+
3
+ A cross-platform library of utilities for dealing with system processes.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install tinyps
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { killTree, getProcessTree } from 'tinyps';
15
+
16
+ // kill a process and everything it spawned
17
+ await killTree(child.pid);
18
+
19
+ // or inspect the tree first
20
+ const tree = await getProcessTree(child.pid);
21
+ console.log(tree.get(child.pid)); // [ 1234, 1235 ]
22
+ ```
23
+
24
+ ## API
25
+
26
+ ### `killTree(pid, signal?)`
27
+
28
+ - `pid` (`number`) - process to kill, along with all of its descendants
29
+ - `signal` (`NodeJS.Signals`, optional) - signal to send, defaults to `SIGTERM`
30
+ - Returns `Promise<void>`
31
+
32
+ Note that `signal` is not supported on Windows, and will be ignored.
33
+
34
+ ### `getProcessTree(pid)`
35
+
36
+ - `pid` (`number`) - process to build the tree from
37
+ - Returns `Promise<ProcessTree>`
38
+
39
+ ### `isRunning(pid)`
40
+
41
+ - `pid` (`number`) - process to check
42
+ - Returns `boolean`
43
+
44
+ ### `getDescendants(pid)`
45
+
46
+ - `pid` (`number`) - process to collect descendants of
47
+ - Returns `Promise<number[]>`
48
+
49
+ ### `listProcesses()`
50
+
51
+ - Returns `Promise<ProcessInfo[]>`
52
+
53
+ ### `getProcessInfo(pid)`
54
+
55
+ - `pid` (`number`) - process to look up
56
+ - Returns `Promise<ProcessInfo | undefined>`
57
+
58
+ ### `findProcessesByName(name, options?)`
59
+
60
+ - `name` (`string`) - name to match against
61
+ - `options.loose` (`boolean`, optional) - match any name containing `name`, ignoring case
62
+ - Returns `Promise<ProcessInfo[]>`
63
+
64
+ ### `findProcessesByPort(port, options?)`
65
+
66
+ - `port` (`number`) - local port to match against
67
+ - `options.protocol` (`'tcp' | 'udp'`, optional) - only match sockets of this
68
+ protocol, defaults to matching both
69
+ - Returns `Promise<ProcessInfo[]>`
70
+
71
+ Matches any process holding a socket bound to `port` locally, whether it is
72
+ listening or connected.
73
+
74
+ ## License
75
+
76
+ MIT
package/lib/main.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ export type ProcessTree = Map<number, number[]>;
2
+ export type Protocol = 'tcp' | 'udp';
3
+ export interface ProcessInfo {
4
+ pid: number;
5
+ ppid: number;
6
+ name: string;
7
+ command: string;
8
+ }
9
+ export declare function killTree(pid: number, signal?: NodeJS.Signals): Promise<void>;
10
+ export declare function listProcesses(): Promise<ProcessInfo[]>;
11
+ export declare function getProcessInfo(pid: number): Promise<ProcessInfo | undefined>;
12
+ export declare function findProcessesByName(name: string, options?: {
13
+ loose?: boolean;
14
+ }): Promise<ProcessInfo[]>;
15
+ export declare function findProcessesByPort(port: number, options?: {
16
+ protocol?: Protocol;
17
+ }): Promise<ProcessInfo[]>;
18
+ export declare function getProcessTree(pid: number): Promise<ProcessTree>;
19
+ export declare function isRunning(pid: number): boolean;
20
+ export declare function getDescendants(pid: number): Promise<number[]>;
package/lib/main.js ADDED
@@ -0,0 +1,240 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { basename } from 'node:path';
3
+ import { platform } from 'node:process';
4
+ async function spawnAsync(command, args, allowedExitCodes) {
5
+ return new Promise((resolve, reject) => {
6
+ const child = spawn(command, args);
7
+ let stdout = '';
8
+ let stderr = '';
9
+ child.stdout.on('data', (data) => {
10
+ stdout += data.toString();
11
+ });
12
+ child.stderr.on('data', (data) => {
13
+ stderr += data.toString();
14
+ });
15
+ child.on('close', (code) => {
16
+ const allowed = allowedExitCodes === undefined || code === null
17
+ ? code === 0
18
+ : allowedExitCodes.includes(code);
19
+ if (allowed) {
20
+ resolve({ stdout, stderr });
21
+ }
22
+ else {
23
+ reject(new Error(`Command failed with exit code ${code}: ${stderr}`));
24
+ }
25
+ });
26
+ child.on('error', (err) => {
27
+ reject(err);
28
+ });
29
+ });
30
+ }
31
+ function killAll(processTree, signal) {
32
+ for (const pid of processTree.keys()) {
33
+ try {
34
+ process.kill(pid, signal);
35
+ }
36
+ catch (err) {
37
+ if (err.code !== 'ESRCH') {
38
+ throw err;
39
+ }
40
+ }
41
+ }
42
+ }
43
+ function buildProcessTree(pid, processes) {
44
+ const childrenByParent = new Map();
45
+ for (const [childPid, parentPid] of processes) {
46
+ if (childPid === pid) {
47
+ continue;
48
+ }
49
+ const children = childrenByParent.get(parentPid) ?? [];
50
+ children.push(childPid);
51
+ childrenByParent.set(parentPid, children);
52
+ }
53
+ const processTree = new Map();
54
+ const queue = [pid];
55
+ for (const current of queue) {
56
+ const children = childrenByParent.get(current) ?? [];
57
+ processTree.set(current, children);
58
+ queue.push(...children);
59
+ }
60
+ return processTree;
61
+ }
62
+ // This matches `{ppid} {pid} {command}`, allowing spaces in the command
63
+ const processListPattern = /^\s*(\d+)\s+(\d+)\s+(.*)$/gm;
64
+ // `ss` reports socket owners as `users:(("name",pid=123,fd=4),...)`
65
+ const socketOwnerPattern = /pid=(\d+)/g;
66
+ // This matches the extensions windows includes in process names
67
+ const executableExtensionPattern = /\.exe$/i;
68
+ // Login shells are executed with a `-` prefixed to their argv[0]
69
+ const loginShellPattern = /^-/;
70
+ async function listProcessesUnix() {
71
+ const { stdout } = await spawnAsync('ps', [
72
+ '-A',
73
+ '-ww',
74
+ '-o',
75
+ 'ppid=,pid=,args=',
76
+ ]);
77
+ return Array.from(stdout.matchAll(processListPattern), (match) => {
78
+ const command = match[3].trim();
79
+ const end = command.indexOf(' ');
80
+ const executable = end === -1 ? command : command.slice(0, end);
81
+ return {
82
+ pid: Number(match[2]),
83
+ ppid: Number(match[1]),
84
+ name: basename(executable.replace(loginShellPattern, '')),
85
+ command,
86
+ };
87
+ });
88
+ }
89
+ async function listProcessesWindows() {
90
+ const { stdout } = await spawnAsync('powershell.exe', [
91
+ '-NoProfile',
92
+ '-NonInteractive',
93
+ '-Command',
94
+ 'Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress',
95
+ ]);
96
+ const parsed = JSON.parse(stdout);
97
+ const processes = Array.isArray(parsed) ? parsed : [parsed];
98
+ return processes.map((proc) => ({
99
+ pid: proc.ProcessId,
100
+ ppid: proc.ParentProcessId,
101
+ name: proc.Name.replace(executableExtensionPattern, ''),
102
+ command: proc.CommandLine ?? proc.Name,
103
+ }));
104
+ }
105
+ async function findPidsByPortLinux(port, protocol) {
106
+ const protocolArgs = [];
107
+ if (protocol !== 'udp') {
108
+ protocolArgs.push('-t');
109
+ }
110
+ if (protocol !== 'tcp') {
111
+ protocolArgs.push('-u');
112
+ }
113
+ const { stdout } = await spawnAsync('ss', [
114
+ '-nHpa',
115
+ ...protocolArgs,
116
+ `sport = :${port}`,
117
+ ]);
118
+ return Array.from(stdout.matchAll(socketOwnerPattern), (match) => Number(match[1]));
119
+ }
120
+ async function findPidsByPortDarwin(port, protocol) {
121
+ const selector = protocol === undefined
122
+ ? `-i:${port}`
123
+ : `-i${protocol.toUpperCase()}:${port}`;
124
+ // lsof exits 1 when nothing matches
125
+ const { stdout } = await spawnAsync('lsof', ['-nP', '-Fpn', selector], [0, 1]);
126
+ const pids = [];
127
+ let currentPid;
128
+ // Field output is a `p{pid}` line followed by an `n{address}` line
129
+ for (const line of stdout.split('\n')) {
130
+ const value = line.slice(1);
131
+ if (line[0] === 'p') {
132
+ currentPid = Number(value);
133
+ }
134
+ else if (line[0] === 'n' && currentPid !== undefined) {
135
+ // Example connected output: 192.168.1.99:57253->1.2.3.4:443
136
+ // Example listening output: [::1]:57355
137
+ const separator = value.indexOf('->');
138
+ const local = separator === -1 ? value : value.slice(0, separator);
139
+ if (Number(local.slice(local.lastIndexOf(':') + 1)) === port) {
140
+ pids.push(currentPid);
141
+ }
142
+ }
143
+ }
144
+ return pids;
145
+ }
146
+ async function findPidsByPortWindows(port, protocol) {
147
+ const queries = [];
148
+ if (protocol !== 'udp') {
149
+ queries.push(`Get-NetTCPConnection -LocalPort ${port}`);
150
+ }
151
+ if (protocol !== 'tcp') {
152
+ queries.push(`Get-NetUDPEndpoint -LocalPort ${port}`);
153
+ }
154
+ const query = queries
155
+ .map((cmd) => `${cmd} -ErrorAction SilentlyContinue`)
156
+ .join('; ');
157
+ const { stdout } = await spawnAsync('powershell.exe', [
158
+ '-NoProfile',
159
+ '-NonInteractive',
160
+ '-Command',
161
+ `@(${query}) | Select-Object -ExpandProperty OwningProcess | ` +
162
+ 'ConvertTo-Json -Compress',
163
+ ]);
164
+ if (stdout.trim() === '') {
165
+ return [];
166
+ }
167
+ const parsed = JSON.parse(stdout);
168
+ return Array.isArray(parsed) ? parsed : [parsed];
169
+ }
170
+ async function killTreeWindows(pid) {
171
+ // taskkill on windows exits 128 when its not found.
172
+ // unix/macos exit with 0 when the process is not found.
173
+ await spawnAsync('taskkill', ['/pid', String(pid), '/T', '/F'], [0, 128]);
174
+ }
175
+ async function killTreeUnix(pid, signal) {
176
+ const processTree = await getProcessTree(pid);
177
+ killAll(processTree, signal);
178
+ }
179
+ export async function killTree(pid, signal) {
180
+ if (platform === 'win32') {
181
+ return killTreeWindows(pid);
182
+ }
183
+ return killTreeUnix(pid, signal);
184
+ }
185
+ export async function listProcesses() {
186
+ if (platform === 'win32') {
187
+ return listProcessesWindows();
188
+ }
189
+ return listProcessesUnix();
190
+ }
191
+ export async function getProcessInfo(pid) {
192
+ const processes = await listProcesses();
193
+ return processes.find((proc) => proc.pid === pid);
194
+ }
195
+ export async function findProcessesByName(name, options) {
196
+ const processes = await listProcesses();
197
+ if (options?.loose) {
198
+ const needle = name.toLowerCase();
199
+ return processes.filter((proc) => proc.name.toLowerCase().includes(needle));
200
+ }
201
+ return processes.filter((proc) => proc.name === name);
202
+ }
203
+ export async function findProcessesByPort(port, options) {
204
+ let pids;
205
+ if (platform === 'win32') {
206
+ pids = await findPidsByPortWindows(port, options?.protocol);
207
+ }
208
+ else if (platform === 'linux') {
209
+ pids = await findPidsByPortLinux(port, options?.protocol);
210
+ }
211
+ else {
212
+ pids = await findPidsByPortDarwin(port, options?.protocol);
213
+ }
214
+ const matched = new Set(pids);
215
+ if (matched.size === 0) {
216
+ return [];
217
+ }
218
+ const processes = await listProcesses();
219
+ return processes.filter((proc) => matched.has(proc.pid));
220
+ }
221
+ export async function getProcessTree(pid) {
222
+ const processes = await listProcesses();
223
+ return buildProcessTree(pid, processes.map((proc) => [proc.pid, proc.ppid]));
224
+ }
225
+ export function isRunning(pid) {
226
+ if (pid <= 0) {
227
+ return false;
228
+ }
229
+ try {
230
+ process.kill(pid, 0);
231
+ return true;
232
+ }
233
+ catch (err) {
234
+ return err.code === 'EPERM';
235
+ }
236
+ }
237
+ export async function getDescendants(pid) {
238
+ const tree = await getProcessTree(pid);
239
+ return Array.from(tree.keys()).filter((current) => current !== pid);
240
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "tinyps",
3
+ "version": "0.2.0",
4
+ "description": "A library of utilities for dealing with system processes.",
5
+ "main": "./lib/main.js",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./lib/main.d.ts",
10
+ "default": "./lib/main.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "lib",
15
+ "!lib/**/*.test.js",
16
+ "!lib/**/*.test.d.ts"
17
+ ],
18
+ "scripts": {
19
+ "build:clean": "node -e \"require('fs').rmSync('./lib', { recursive: true, force: true })\"",
20
+ "build": "npm run build:clean && tsgo",
21
+ "lint:js": "oxlint src",
22
+ "lint:format": "oxfmt --check src",
23
+ "lint": "npm run lint:js && npm run lint:format",
24
+ "format": "oxfmt --write src",
25
+ "test": "vitest run"
26
+ },
27
+ "keywords": [
28
+ "proc",
29
+ "process",
30
+ "tree-kill",
31
+ "ps-tree",
32
+ "tiny",
33
+ "kill",
34
+ "tree"
35
+ ],
36
+ "author": "James Garbutt (https://github.com/43081j)",
37
+ "license": "MIT",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "homepage": "https://github.com/tinylibs/tinyps#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/tinylibs/tinyps/issues"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/tinylibs/tinyps.git"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^26.5.1",
51
+ "@typescript/native-preview": "^7.0.0-dev.20260509.2",
52
+ "@vitest/coverage-v8": "^5.0.0",
53
+ "oxfmt": "^0.67.0",
54
+ "oxlint": "^1.63.0",
55
+ "vitest": "^5.0.0"
56
+ }
57
+ }