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.
Files changed (3) hide show
  1. package/index.js +374 -0
  2. package/package.json +14 -0
  3. 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
+ }
package/readme.md ADDED
@@ -0,0 +1,9 @@
1
+ # killport
2
+ CLI to kill ports.
3
+
4
+ ## Usage
5
+
6
+ killport 8080
7
+ killport 8080 3000
8
+ killport --all
9
+ killport 8080 --force