Skip to content

Commit eecb553

Browse files
committed
Require Node.js 20
Fixes #50
1 parent 0288e35 commit eecb553

File tree

4 files changed

+161
-132
lines changed

4 files changed

+161
-132
lines changed

.github/workflows/main.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ jobs:
1010
fail-fast: false
1111
matrix:
1212
node-version:
13+
- 24
1314
- 20
14-
- 18
1515
os:
1616
- ubuntu-latest
1717
- macos-latest
1818
- windows-latest
1919
steps:
20-
- uses: actions/checkout@v4
21-
- uses: actions/setup-node@v4
20+
- uses: actions/checkout@v5
21+
- uses: actions/setup-node@v6
2222
with:
2323
node-version: ${{ matrix.node-version }}
2424
- run: npm install

interactive.js

Lines changed: 140 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import process from 'node:process';
22
import chalk from 'chalk';
33
import inquirer from 'inquirer';
4-
import inquirerAutocompletePrompt from 'inquirer-autocomplete-prompt';
4+
import search from '@inquirer/search';
55
import psList from 'ps-list';
66
import {numberSortDescending} from 'num-sort';
77
import escExit from 'esc-exit';
@@ -89,160 +89,189 @@ const preferHeurisicallyInterestingProcesses = (a, b) => {
8989
return preferLowAlphanumericNames(a, b);
9090
};
9191

92-
const filterProcesses = (input, processes, flags) => {
93-
const memoryThreshold = flags.verbose ? 0 : 1;
94-
const cpuThreshold = flags.verbose ? 0 : 3;
92+
const isHelperProcess = process_ => process_.name.endsWith('-helper')
93+
|| process_.name.endsWith('Helper')
94+
|| process_.name.endsWith('HelperApp');
9595

96-
const filteredProcesses = new FuzzySearch(
97-
processes,
98-
[
99-
// The name is truncated for some reason, so we always use `cmd` for now.
100-
'cmd',
101-
/// flags.verbose && !isWindows ? 'cmd' : 'name',
102-
'pid',
103-
],
104-
{
105-
caseSensitive: false,
106-
},
107-
)
108-
.search(input);
109-
110-
return filteredProcesses
111-
.filter(process_ => !(
112-
process_.name.endsWith('-helper')
113-
|| process_.name.endsWith('Helper')
114-
|| process_.name.endsWith('HelperApp')
115-
))
116-
.sort(preferHeurisicallyInterestingProcesses)
117-
.map(process_ => {
118-
const renderPercentage = percents => {
119-
const digits = Math.floor(percents * 10).toString().padStart(2, '0');
120-
const whole = digits.slice(0, -1);
121-
const fraction = digits.slice(-1);
122-
return fraction === '0' ? `${whole}%` : `${whole}.${fraction}%`;
123-
};
124-
125-
const lineLength = process.stdout.columns || 80;
126-
const ports = process_.ports.length === 0 ? '' : (' ' + process_.ports.slice(0, 4).map(x => `:${x}`).join(' '));
127-
const memory = (process_.memory !== undefined && (process_.memory > memoryThreshold)) ? ` 🐏${renderPercentage(process_.memory)}` : '';
128-
const cpu = (process_.cpu !== undefined && (process_.cpu > cpuThreshold)) ? `🚦${renderPercentage(process_.cpu)}` : '';
129-
const margins = commandLineMargins + process_.pid.toString().length + ports.length + memory.length + cpu.length;
130-
const length = lineLength - margins;
131-
const name = cliTruncate(flags.verbose && !isWindows ? process_.cmd : process_.name, length, {position: 'middle', preferTruncationOnSpace: true});
132-
const extraMargin = 2;
133-
const spacer = lineLength === process.stdout.columns ? ''.padEnd(length - name.length - extraMargin) : '';
134-
135-
return {
136-
name: `${name} ${chalk.dim(process_.pid)}${spacer}${chalk.dim(ports)}${cpu}${memory}`,
137-
value: process_.pid,
138-
};
139-
});
96+
const renderPercentage = percents => {
97+
const digits = Math.floor(percents * 10).toString().padStart(2, '0');
98+
const whole = digits.slice(0, -1);
99+
const fraction = digits.slice(-1);
100+
return fraction === '0' ? `${whole}%` : `${whole}.${fraction}%`;
140101
};
141102

142-
const handleFkillError = async processes => {
143-
const suffix = processes.length > 1 ? 'es' : '';
103+
const renderProcessForDisplay = (process_, flags, memoryThreshold, cpuThreshold) => {
104+
const lineLength = process.stdout.columns || 80;
105+
const ports = process_.ports.length === 0 ? '' : (' ' + process_.ports.slice(0, 4).map(x => `:${x}`).join(' '));
106+
const memory = (process_.memory !== undefined && (process_.memory > memoryThreshold)) ? ` 🐏${renderPercentage(process_.memory)}` : '';
107+
const cpu = (process_.cpu !== undefined && (process_.cpu > cpuThreshold)) ? `🚦${renderPercentage(process_.cpu)}` : '';
108+
const margins = commandLineMargins + process_.pid.toString().length + ports.length + memory.length + cpu.length;
109+
const length = lineLength - margins;
110+
const name = cliTruncate(flags.verbose && !isWindows ? process_.cmd : process_.name, length, {position: 'middle', preferTruncationOnSpace: true});
111+
const extraMargin = 2;
112+
const spacer = lineLength === process.stdout.columns ? ''.padEnd(length - name.length - extraMargin) : '';
113+
114+
return {
115+
name: `${name} ${chalk.dim(process_.pid)}${spacer}${chalk.dim(ports)}${cpu}${memory}`,
116+
value: process_.pid,
117+
};
118+
};
144119

145-
if (process.stdout.isTTY === false) {
146-
console.error(`Error killing process${suffix}. Try \`fkill --force ${processes.join(' ')}\``);
147-
process.exit(1); // eslint-disable-line unicorn/no-process-exit
148-
} else {
149-
const answer = await inquirer.prompt([{
150-
type: 'confirm',
151-
name: 'forceKill',
152-
message: 'Error killing process. Would you like to use the force?',
153-
}]);
154-
155-
if (answer.forceKill === true) {
156-
await fkill(processes, {
157-
force: true,
158-
ignoreCase: true,
159-
});
120+
const searchProcessesByPort = (processes, port) => processes.filter(process_ => process_.ports.includes(port));
121+
122+
const searchProcessByPid = (processes, pid) => processes.find(process_ => String(process_.pid) === pid);
123+
124+
const searchProcessesByName = (processes, term, searcher) => {
125+
const lowerTerm = term.toLowerCase();
126+
const exactMatches = [];
127+
const startsWithMatches = [];
128+
const containsMatches = [];
129+
130+
for (const process_ of processes) {
131+
const lowerName = process_.name.toLowerCase();
132+
if (lowerName === lowerTerm) {
133+
exactMatches.push(process_);
134+
} else if (lowerName.startsWith(lowerTerm)) {
135+
startsWithMatches.push(process_);
136+
} else if (lowerName.includes(lowerTerm)) {
137+
containsMatches.push(process_);
160138
}
161139
}
140+
141+
// Fuzzy matches (excluding all exact/starts/contains matches)
142+
const matchedPids = new Set([...exactMatches, ...startsWithMatches, ...containsMatches].map(process_ => process_.pid));
143+
const fuzzyResults = searcher.search(term).filter(process_ => !matchedPids.has(process_.pid));
144+
145+
// Combine in priority order
146+
return [...exactMatches, ...startsWithMatches, ...containsMatches, ...fuzzyResults];
162147
};
163148

164-
const DEFAULT_EXIT_TIMEOUT = 3000;
149+
const filterAndSortProcesses = (processes, term, searcher) => {
150+
const filtered = processes.filter(process_ => !isHelperProcess(process_));
165151

166-
const performKillSequence = async processes => {
167-
if (!Array.isArray(processes)) {
168-
processes = [processes];
152+
// No search term: show all sorted by performance
153+
if (!term) {
154+
return filtered.sort(preferHeurisicallyInterestingProcesses);
169155
}
170156

171-
let didSurvive;
172-
let hadError;
173-
try {
174-
await fkill(processes);
175-
const exited = await Promise.all(processes.map(process => processExited(process, DEFAULT_EXIT_TIMEOUT)));
176-
didSurvive = processes.filter((_, i) => !exited[i]);
177-
} catch (error) {
178-
didSurvive = processes;
179-
hadError = error;
157+
// Search by port
158+
if (term.startsWith(':')) {
159+
const port = term.slice(1);
160+
return searchProcessesByPort(filtered, port);
180161
}
181162

182-
if (didSurvive.length === 0) {
183-
return;
163+
// Search by PID
164+
const pidMatch = searchProcessByPid(filtered, term);
165+
if (pidMatch) {
166+
return [pidMatch];
184167
}
185168

186-
const suffix = didSurvive.length > 1 ? 'es' : '';
187-
const problemText = hadError ? `Error killing process${suffix}.` : `Process${suffix} didn't exit in ${DEFAULT_EXIT_TIMEOUT}ms.`;
169+
// Search by name
170+
return searchProcessesByName(filtered, term, searcher);
171+
};
172+
173+
const handleFkillError = async processes => {
174+
const shouldForceKill = await promptForceKill(processes, 'Error killing process.');
188175

176+
if (shouldForceKill) {
177+
await fkill(processes, {
178+
force: true,
179+
ignoreCase: true,
180+
});
181+
}
182+
};
183+
184+
const DEFAULT_EXIT_TIMEOUT = 3000;
185+
186+
const attemptKillProcesses = async processes => {
187+
try {
188+
await fkill(processes);
189+
const exitStatuses = await Promise.all(processes.map(process_ => processExited(process_, DEFAULT_EXIT_TIMEOUT)));
190+
const survivors = processes.filter((_, index) => !exitStatuses[index]);
191+
return {survivors, hadError: false};
192+
} catch {
193+
return {survivors: processes, hadError: true};
194+
}
195+
};
196+
197+
const promptForceKill = async (survivingProcesses, message) => {
189198
if (process.stdout.isTTY === false) {
190-
console.error(`${problemText} Try \`fkill --force ${didSurvive.join(' ')}\``);
199+
console.error(`${message} Try \`fkill --force ${survivingProcesses.join(' ')}\``);
191200
process.exit(1); // eslint-disable-line unicorn/no-process-exit
192201
}
193202

194203
const answer = await inquirer.prompt([{
195204
type: 'confirm',
196205
name: 'forceKill',
197-
message: `${problemText} Would you like to use the force?`,
206+
message: `${message} Would you like to use the force?`,
198207
}]);
199208

200-
if (!answer.forceKill) {
209+
return answer.forceKill;
210+
};
211+
212+
const performKillSequence = async processes => {
213+
const processList = Array.isArray(processes) ? processes : [processes];
214+
const {survivors, hadError} = await attemptKillProcesses(processList);
215+
216+
if (survivors.length === 0) {
201217
return;
202218
}
203219

204-
await fkill(processes, {
205-
force: true,
206-
ignoreCase: true,
207-
});
220+
const suffix = survivors.length > 1 ? 'es' : '';
221+
const message = hadError ? `Error killing process${suffix}.` : `Process${suffix} didn't exit in ${DEFAULT_EXIT_TIMEOUT}ms.`;
222+
const shouldForceKill = await promptForceKill(survivors, message);
223+
224+
if (shouldForceKill) {
225+
await fkill(processList, {
226+
force: true,
227+
ignoreCase: true,
228+
});
229+
}
230+
};
231+
232+
const findPortsForProcess = (processId, portToPidMap) => {
233+
const ports = [];
234+
235+
for (const [port, pid] of portToPidMap.entries()) {
236+
if (processId === pid) {
237+
ports.push(String(port));
238+
}
239+
}
240+
241+
return ports;
208242
};
209243

210244
const listProcesses = async (processes, flags) => {
211-
inquirer.registerPrompt('autocomplete', inquirerAutocompletePrompt);
245+
const memoryThreshold = flags.verbose ? 0 : 1;
246+
const cpuThreshold = flags.verbose ? 0 : 3;
247+
const searcher = new FuzzySearch(processes, ['name'], {caseSensitive: false});
212248

213-
const answer = await inquirer.prompt([{
214-
name: 'processes',
249+
const selectedPid = await search({
215250
message: 'Running processes:',
216-
type: 'autocomplete',
217251
pageSize: 10,
218-
source: async (answers, input) => filterProcesses(input, processes, flags),
219-
}]);
252+
async source(term = '') {
253+
const matchingProcesses = filterAndSortProcesses(processes, term, searcher);
254+
return matchingProcesses.map(process_ => renderProcessForDisplay(process_, flags, memoryThreshold, cpuThreshold));
255+
},
256+
});
220257

221-
performKillSequence(answer.processes);
258+
performKillSequence(selectedPid);
222259
};
223260

224261
const init = async flags => {
225262
escExit();
226263

227-
const getPortsFromPid = (value, list) => {
228-
const ports = [];
229-
230-
for (const [key, listValue] of list.entries()) {
231-
if (value === listValue) {
232-
ports.push(String(key));
233-
}
234-
}
235-
236-
return ports;
237-
};
238-
239-
const [pids, processes] = await Promise.all([
264+
const [portToPidMap, processes] = await Promise.all([
240265
allPortsWithPid(),
241266
psList({all: false}),
242267
]);
243268

244-
const procs = processes.map(process_ => ({...process_, ports: getPortsFromPid(process_.pid, pids)}));
245-
listProcesses(procs, flags);
269+
const processesWithPorts = processes.map(process_ => ({
270+
...process_,
271+
ports: findPortsForProcess(process_.pid, portToPidMap),
272+
}));
273+
274+
listProcesses(processesWithPorts, flags);
246275
};
247276

248277
export {init, handleFkillError};

package.json

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
},
1717
"sideEffects": false,
1818
"engines": {
19-
"node": ">=18"
19+
"node": ">=20"
2020
},
2121
"scripts": {
2222
"test": "xo && ava"
@@ -43,25 +43,25 @@
4343
"proc"
4444
],
4545
"dependencies": {
46-
"chalk": "^5.3.0",
47-
"cli-truncate": "^4.0.0",
48-
"esc-exit": "^3.0.0",
49-
"fkill": "^9.0.0",
46+
"@inquirer/search": "^3.2.1",
47+
"chalk": "^5.6.2",
48+
"cli-truncate": "^5.1.1",
49+
"esc-exit": "^3.0.1",
50+
"fkill": "^10.0.0",
5051
"fuzzy-search": "^3.2.1",
51-
"inquirer": "^9.2.11",
52-
"inquirer-autocomplete-prompt": "^3.0.1",
53-
"meow": "^12.1.1",
54-
"num-sort": "^3.0.0",
55-
"pid-port": "^1.0.0",
56-
"ps-list": "^8.1.1"
52+
"inquirer": "^12.11.0",
53+
"meow": "^14.0.0",
54+
"num-sort": "^4.0.0",
55+
"pid-port": "^2.0.0",
56+
"ps-list": "^9.0.0"
5757
},
5858
"devDependencies": {
59-
"ava": "^5.3.1",
60-
"delay": "^6.0.0",
61-
"execa": "^8.0.1",
62-
"get-port": "^7.0.0",
59+
"ava": "^6.4.1",
60+
"delay": "^7.0.0",
61+
"execa": "^9.6.0",
62+
"get-port": "^7.1.0",
6363
"noop-process": "^5.0.0",
6464
"process-exists": "^5.0.0",
65-
"xo": "^0.56.0"
65+
"xo": "^1.2.3"
6666
}
6767
}

readme.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,5 @@ Run `fkill` without arguments to launch the interactive UI.
5454

5555
## Related
5656

57-
- [fkill](https://github.com/sindresorhus/fkill) - API for this module
58-
- [alfred-fkill](https://github.com/SamVerschueren/alfred-fkill) - Alfred workflow for this module
57+
- [fkill](https://github.com/sindresorhus/fkill) - API for this package
58+
- [alfred-fkill](https://github.com/SamVerschueren/alfred-fkill) - Alfred workflow for this package

0 commit comments

Comments
 (0)