killtask 1.0.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/index.js +374 -0
- package/package.json +14 -0
- package/readme.md +9 -0
package/index.js
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { exec } from "child_process";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import readline from "readline";
|
|
7
|
+
|
|
8
|
+
// ─── Args ────────────────────────────────────────────────────────────────────
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const FLAGS = new Set(["--soft","--no-force","--all","--yes","-y","--verbose","-v","--help","-h"]);
|
|
11
|
+
const isForce = !args.includes("--soft") && !args.includes("--no-force");
|
|
12
|
+
const isAll = args.includes("--all");
|
|
13
|
+
const isYes = args.includes("--yes") || args.includes("-y");
|
|
14
|
+
const isVerbose = args.includes("--verbose") || args.includes("-v");
|
|
15
|
+
const ports = args.filter(a => !FLAGS.has(a) && !a.startsWith("-"));
|
|
16
|
+
const isWin = os.platform() === "win32";
|
|
17
|
+
|
|
18
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
19
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
20
|
+
|
|
21
|
+
function run(cmd) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
exec(cmd, { timeout: 8_000 }, (err, stdout) => {
|
|
24
|
+
if (err) return reject(err);
|
|
25
|
+
resolve(stdout.trim());
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function log(...msg) {
|
|
31
|
+
if (isVerbose) console.log(chalk.gray(" ·"), ...msg);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function confirm(q) {
|
|
35
|
+
if (isYes) return Promise.resolve(true);
|
|
36
|
+
return new Promise(resolve => {
|
|
37
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
38
|
+
rl.question(chalk.yellow(` ⚠️ ${q} [y/N] `), a => { rl.close(); resolve(a.toLowerCase() === "y"); });
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function clearLine() {
|
|
43
|
+
process.stdout.write("\r" + " ".repeat(72) + "\r");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Animation ───────────────────────────────────────────────────────────────
|
|
47
|
+
// Runs CONCURRENTLY with the kill — bullet travels while kill executes,
|
|
48
|
+
// explosion plays after kill resolves.
|
|
49
|
+
|
|
50
|
+
const TRACK = 24;
|
|
51
|
+
const MARIO = chalk.red("♜");
|
|
52
|
+
const ENEMY = chalk.green("♟");
|
|
53
|
+
|
|
54
|
+
function drawFrame(bulletPos, enemyChar, label) {
|
|
55
|
+
let track = "";
|
|
56
|
+
for (let i = 0; i < TRACK; i++) {
|
|
57
|
+
if (i === bulletPos) track += chalk.yellowBright("►");
|
|
58
|
+
else if (i < bulletPos) track += chalk.gray("·");
|
|
59
|
+
else track += chalk.gray("─");
|
|
60
|
+
}
|
|
61
|
+
process.stdout.write(`\r ${MARIO} ${track} ${enemyChar} ${label}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Animate bullet travelling — returns a promise that resolves when bullet reaches end
|
|
65
|
+
async function animateBullet() {
|
|
66
|
+
// Aim flash
|
|
67
|
+
for (let i = 0; i < 3; i++) {
|
|
68
|
+
process.stdout.write(
|
|
69
|
+
`\r ${MARIO} ${chalk.gray("─".repeat(TRACK))} ${ENEMY} ` +
|
|
70
|
+
(i % 2 === 0 ? chalk.gray("aiming…") : chalk.yellowBright("🔫 FIRE!"))
|
|
71
|
+
);
|
|
72
|
+
await sleep(180);
|
|
73
|
+
}
|
|
74
|
+
// Bullet travels
|
|
75
|
+
for (let pos = 0; pos < TRACK; pos++) {
|
|
76
|
+
drawFrame(pos, ENEMY, chalk.white("pew pew…"));
|
|
77
|
+
await sleep(36);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function animateExplosion() {
|
|
82
|
+
const frames = [
|
|
83
|
+
[chalk.yellowBright("✸"), chalk.bgRed.bold(" HIT ") ],
|
|
84
|
+
[chalk.red("✺"), chalk.red("💥 BOOM!") ],
|
|
85
|
+
[chalk.yellowBright("✦"), chalk.red("☠ TERMINATED") ],
|
|
86
|
+
[chalk.white("·"), chalk.green("✔ port killed") ],
|
|
87
|
+
[" ", chalk.green("✔ port killed") ],
|
|
88
|
+
];
|
|
89
|
+
for (const [sym, label] of frames) {
|
|
90
|
+
process.stdout.write(
|
|
91
|
+
`\r ${MARIO} ${chalk.gray("·".repeat(TRACK))} ${sym} ${label}`
|
|
92
|
+
);
|
|
93
|
+
await sleep(85);
|
|
94
|
+
}
|
|
95
|
+
clearLine();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function animateBounce() {
|
|
99
|
+
for (let pos = TRACK - 1; pos >= 0; pos--) {
|
|
100
|
+
let track = "";
|
|
101
|
+
for (let i = 0; i < TRACK; i++) {
|
|
102
|
+
if (i === pos) track += chalk.red("◄");
|
|
103
|
+
else if (i > pos) track += chalk.gray("·");
|
|
104
|
+
else track += chalk.gray("─");
|
|
105
|
+
}
|
|
106
|
+
process.stdout.write(
|
|
107
|
+
`\r ${MARIO} ${track} ${chalk.red("⛨")} ${chalk.red("blocked!")}`
|
|
108
|
+
);
|
|
109
|
+
await sleep(22);
|
|
110
|
+
}
|
|
111
|
+
clearLine();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Kill + animate concurrently ─────────────────────────────────────────────
|
|
115
|
+
async function killWithAnimation(pid) {
|
|
116
|
+
// Start bullet animation and kill in parallel
|
|
117
|
+
const killPromise = killPid(pid);
|
|
118
|
+
await animateBullet();
|
|
119
|
+
|
|
120
|
+
// Wait for kill to finish (it's usually done by now)
|
|
121
|
+
const ok = await killPromise;
|
|
122
|
+
|
|
123
|
+
if (ok) {
|
|
124
|
+
await animateExplosion();
|
|
125
|
+
} else {
|
|
126
|
+
await animateBounce();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return ok;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── App Name ─────────────────────────────────────────────────────────────────
|
|
133
|
+
const RUNTIMES = new Set(["node","python","python3","ruby","java","php","deno","bun"]);
|
|
134
|
+
|
|
135
|
+
async function getAppLabel(pid) {
|
|
136
|
+
try {
|
|
137
|
+
let name = "", cmdLine = "";
|
|
138
|
+
if (isWin) {
|
|
139
|
+
const tl = await run(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`).catch(() => "");
|
|
140
|
+
name = tl.split(",")[0]?.replace(/"/g, "").trim() ?? "";
|
|
141
|
+
const ps = await run(
|
|
142
|
+
`powershell -NoProfile -Command "(Get-Process -Id ${pid} -ErrorAction SilentlyContinue).CommandLine"`
|
|
143
|
+
).catch(() => "");
|
|
144
|
+
cmdLine = ps.trim();
|
|
145
|
+
} else {
|
|
146
|
+
[cmdLine, name] = await Promise.all([
|
|
147
|
+
run(`ps -p ${pid} -o args=`).catch(() => ""),
|
|
148
|
+
run(`ps -p ${pid} -o comm=`).catch(() => ""),
|
|
149
|
+
]);
|
|
150
|
+
}
|
|
151
|
+
return buildLabel(name, cmdLine);
|
|
152
|
+
} catch {
|
|
153
|
+
return chalk.gray("unknown");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildLabel(name, cmdLine) {
|
|
158
|
+
const tokens = (cmdLine || name).trim().split(/\s+/);
|
|
159
|
+
const bin = (name || tokens[0]?.split(/[/\\]/).pop() || "")
|
|
160
|
+
.replace(/\.exe$/i, "").toLowerCase();
|
|
161
|
+
const entry = tokens.slice(1).find(t => !t.startsWith("-")) ?? "";
|
|
162
|
+
const short = entry.split(/[/\\]/).pop();
|
|
163
|
+
if (RUNTIMES.has(bin) && short)
|
|
164
|
+
return `${chalk.white(bin)} ${chalk.gray("·")} ${chalk.cyan(short)}`;
|
|
165
|
+
return chalk.white(bin || "unknown");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Process Discovery ───────────────────────────────────────────────────────
|
|
169
|
+
async function getPidsForPort(port) {
|
|
170
|
+
const pids = new Map();
|
|
171
|
+
if (isWin) {
|
|
172
|
+
const out = await run(`netstat -ano -p TCP`).catch(() => "");
|
|
173
|
+
for (const line of out.split("\n")) {
|
|
174
|
+
if (!line.match(new RegExp(`:${port}[^\\d]`))) continue;
|
|
175
|
+
const p = line.trim().split(/\s+/);
|
|
176
|
+
if (p.length < 5) continue;
|
|
177
|
+
const [,,, state, pid] = p;
|
|
178
|
+
if (!["LISTENING","ESTABLISHED"].includes(state)) continue;
|
|
179
|
+
if (pid && !pids.has(pid))
|
|
180
|
+
pids.set(pid, { pid, appLabel: await getAppLabel(pid), port });
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
const out = await run(`lsof -nP -i TCP:${port}`).catch(() => "");
|
|
184
|
+
for (const line of out.split("\n").slice(1)) {
|
|
185
|
+
if (!line) continue;
|
|
186
|
+
const pid = line.trim().split(/\s+/)[1];
|
|
187
|
+
if (pid && !pids.has(pid))
|
|
188
|
+
pids.set(pid, { pid, appLabel: await getAppLabel(pid), port });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return pids;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function getAllListeningPids() {
|
|
195
|
+
const pids = new Map();
|
|
196
|
+
if (isWin) {
|
|
197
|
+
const out = await run(`netstat -ano -p TCP`).catch(() => "");
|
|
198
|
+
for (const line of out.split("\n")) {
|
|
199
|
+
if (!line.includes("LISTENING")) continue;
|
|
200
|
+
const p = line.trim().split(/\s+/);
|
|
201
|
+
if (p.length < 5) continue;
|
|
202
|
+
const pid = p[4];
|
|
203
|
+
const port = p[1]?.split(":").pop() ?? "?";
|
|
204
|
+
if (pid && pid !== "0" && pid !== "4" && !pids.has(pid))
|
|
205
|
+
pids.set(pid, { pid, appLabel: await getAppLabel(pid), port });
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
const out = await run(`lsof -nP -i TCP -s TCP:LISTEN`).catch(() => "");
|
|
209
|
+
for (const line of out.split("\n").slice(1)) {
|
|
210
|
+
if (!line) continue;
|
|
211
|
+
const parts = line.trim().split(/\s+/);
|
|
212
|
+
const pid = parts[1];
|
|
213
|
+
const m = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
214
|
+
const port = m?.[1] ?? "?";
|
|
215
|
+
if (pid && !pids.has(pid))
|
|
216
|
+
pids.set(pid, { pid, appLabel: await getAppLabel(pid), port });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return pids;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── Kill ─────────────────────────────────────────────────────────────────────
|
|
223
|
+
async function killPid(pid) {
|
|
224
|
+
if (isWin) {
|
|
225
|
+
try {
|
|
226
|
+
await run(`taskkill ${isForce ? "/F" : ""} /PID ${pid}`);
|
|
227
|
+
log(`PID ${pid} terminated`);
|
|
228
|
+
return true;
|
|
229
|
+
} catch (e) {
|
|
230
|
+
log(chalk.red(e.message.split("\n").find(l => l.trim()) ?? e.message));
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
const sigs = isForce ? ["-9"] : ["-15", "-9"];
|
|
235
|
+
for (const sig of sigs) {
|
|
236
|
+
try {
|
|
237
|
+
await run(`kill ${sig} ${pid}`);
|
|
238
|
+
await sleep(200);
|
|
239
|
+
const still = await run(`ps -p ${pid} -o pid=`).catch(() => "");
|
|
240
|
+
if (!still.trim()) { log(`PID ${pid} gone (${sig})`); return true; }
|
|
241
|
+
} catch { return true; }
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ─── Counters ─────────────────────────────────────────────────────────────────
|
|
248
|
+
let nKilled = 0, nFailed = 0, nNotFound = 0;
|
|
249
|
+
|
|
250
|
+
// ─── killPort ─────────────────────────────────────────────────────────────────
|
|
251
|
+
async function killPort(port) {
|
|
252
|
+
if (!/^\d+$/.test(port) || +port < 1 || +port > 65535) {
|
|
253
|
+
console.log(chalk.red(` ✗ Invalid port: ${port}`)); return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const pids = await getPidsForPort(port);
|
|
257
|
+
|
|
258
|
+
if (pids.size === 0) {
|
|
259
|
+
console.log(` ${chalk.gray("○")} :${chalk.white(port)} ${chalk.gray("— nothing listening")}`);
|
|
260
|
+
nNotFound++; return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const entry of pids.values()) {
|
|
264
|
+
// Single info line
|
|
265
|
+
console.log(
|
|
266
|
+
`\n ${chalk.cyan("▸")} :${chalk.bold.cyan(port)} ` +
|
|
267
|
+
`${chalk.gray(`PID ${entry.pid}`)} ${entry.appLabel}\n`
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const ok = await killWithAnimation(entry.pid);
|
|
271
|
+
|
|
272
|
+
if (ok) {
|
|
273
|
+
console.log(` ${chalk.red("☠")} :${chalk.bold.magenta(port)} killed ${chalk.gray("←")} ${entry.appLabel}`);
|
|
274
|
+
nKilled++;
|
|
275
|
+
} else {
|
|
276
|
+
console.log(` ${chalk.red("✗")} :${chalk.bold(port)} could not be killed — try ${chalk.yellow("--soft")}`);
|
|
277
|
+
nFailed++;
|
|
278
|
+
}
|
|
279
|
+
console.log();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ─── killAll ──────────────────────────────────────────────────────────────────
|
|
284
|
+
async function killAll() {
|
|
285
|
+
const pids = await getAllListeningPids();
|
|
286
|
+
if (pids.size === 0) {
|
|
287
|
+
console.log(chalk.gray(" ○ No listening processes found.")); return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
console.log(chalk.cyan(`\n ${pids.size} process(es) listening:\n`));
|
|
291
|
+
for (const { pid, appLabel, port } of pids.values())
|
|
292
|
+
console.log(` ${chalk.gray(`:${String(port).padEnd(6)}`)} ${chalk.gray(`PID ${String(pid).padStart(6)}`)} ${appLabel}`);
|
|
293
|
+
console.log();
|
|
294
|
+
|
|
295
|
+
const ok = await confirm(`Terminate ALL ${pids.size} processes?`);
|
|
296
|
+
if (!ok) { console.log(chalk.gray("\n Aborted.\n")); return; }
|
|
297
|
+
|
|
298
|
+
for (const entry of pids.values()) {
|
|
299
|
+
console.log(
|
|
300
|
+
`\n ${chalk.cyan("▸")} :${chalk.bold.cyan(entry.port)} ` +
|
|
301
|
+
`${chalk.gray(`PID ${entry.pid}`)} ${entry.appLabel}\n`
|
|
302
|
+
);
|
|
303
|
+
const killed = await killWithAnimation(entry.pid);
|
|
304
|
+
if (killed) {
|
|
305
|
+
console.log(` ${chalk.red("☠")} :${chalk.bold.magenta(entry.port)} killed ${chalk.gray("←")} ${entry.appLabel}`);
|
|
306
|
+
nKilled++;
|
|
307
|
+
} else {
|
|
308
|
+
console.log(` ${chalk.red("✗")} :${chalk.bold(entry.port)} could not be killed`);
|
|
309
|
+
nFailed++;
|
|
310
|
+
}
|
|
311
|
+
console.log();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ─── Help ─────────────────────────────────────────────────────────────────────
|
|
316
|
+
function showHelp() {
|
|
317
|
+
console.log(`
|
|
318
|
+
${chalk.red("♜")} ${chalk.bold("killport")}
|
|
319
|
+
|
|
320
|
+
${chalk.cyan("Usage:")}
|
|
321
|
+
killport <port> [port ...]
|
|
322
|
+
killport --all
|
|
323
|
+
|
|
324
|
+
${chalk.cyan("Options:")}
|
|
325
|
+
--soft, --no-force Graceful SIGTERM before SIGKILL
|
|
326
|
+
--all Kill every listening process
|
|
327
|
+
--yes, -y Skip --all confirmation
|
|
328
|
+
--verbose, -v Show signal details
|
|
329
|
+
--help, -h This help
|
|
330
|
+
|
|
331
|
+
${chalk.cyan("Examples:")}
|
|
332
|
+
killport 3000
|
|
333
|
+
killport 3000 8080
|
|
334
|
+
killport --all -y
|
|
335
|
+
`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
339
|
+
async function main() {
|
|
340
|
+
if (args.includes("--help") || args.includes("-h")) { showHelp(); process.exit(0); }
|
|
341
|
+
|
|
342
|
+
if (!isAll && ports.length === 0) {
|
|
343
|
+
console.error(chalk.red("\n ❌ Provide at least one port, or use --all\n"));
|
|
344
|
+
showHelp(); process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const target = isAll
|
|
348
|
+
? chalk.red("all processes")
|
|
349
|
+
: ports.map(p => chalk.cyan(`:${p}`)).join(chalk.gray(" "));
|
|
350
|
+
|
|
351
|
+
console.log(`\n ${chalk.red("♜")} ${chalk.bold("killport")} ${chalk.gray("·")} ${target}\n`);
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
if (isAll) {
|
|
355
|
+
await killAll();
|
|
356
|
+
} else {
|
|
357
|
+
for (const port of ports) await killPort(port);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const parts = [];
|
|
361
|
+
if (nKilled) parts.push(chalk.green(`☠ ${nKilled} killed`));
|
|
362
|
+
if (nFailed) parts.push(chalk.red(`✗ ${nFailed} failed`));
|
|
363
|
+
if (nNotFound) parts.push(chalk.gray(`○ ${nNotFound} not found`));
|
|
364
|
+
if (parts.length) console.log(" " + parts.join(chalk.gray(" · ")));
|
|
365
|
+
console.log();
|
|
366
|
+
|
|
367
|
+
} catch (err) {
|
|
368
|
+
clearLine();
|
|
369
|
+
console.error(chalk.red(" ❌"), err.message);
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "killtask",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Port killer CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"killtask": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"chalk": "^5.3.0"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["cli", "port", "kill", "kill-task","task-kill","process-kill","process"],
|
|
13
|
+
"license": "MIT"
|
|
14
|
+
}
|