slash-port 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Slash UI Pte Ltd
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,209 @@
1
+ # slash-port
2
+
3
+ [![npm](https://img.shields.io/npm/v/slash-port?logo=npm&logoColor=white&color=0f8b7d)](https://www.npmjs.com/package/slash-port)
4
+ [![downloads](https://img.shields.io/npm/dm/slash-port?color=0f8b7d)](https://www.npmjs.com/package/slash-port)
5
+ [![CI](https://github.com/Slash-ui/slash-port/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Slash-ui/slash-port/actions/workflows/ci.yml)
6
+ [![Release](https://github.com/Slash-ui/slash-port/actions/workflows/release.yml/badge.svg?branch=main)](https://github.com/Slash-ui/slash-port/actions/workflows/release.yml)
7
+ [![node](https://img.shields.io/node/v/slash-port?color=5a6a78)](https://nodejs.org)
8
+ [![licence](https://img.shields.io/npm/l/slash-port?color=5a6a78)](LICENSE)
9
+ [![Conventional Commits](https://img.shields.io/badge/commits-conventional-0f8b7d)](https://www.conventionalcommits.org)
10
+
11
+ See what is listening on your ports, understand what it is, and kill it safely.
12
+
13
+ **[slash-ui.github.io/slash-port](https://slash-ui.github.io/slash-port/)**
14
+
15
+ `EADDRINUSE: address already in use :::3000` tells you a port is taken. It does
16
+ not tell you what took it, which project it belongs to, or whether killing it is
17
+ a good idea. `slash-port` answers all three, then kills the process for you
18
+ after a confirmation that names it.
19
+
20
+ ```
21
+ slash-port 6/6 tcp
22
+ PORT PID USER PROCESS DESCRIPTION
23
+ 3000/tcp 41822 amin node Next.js (shop)
24
+ 5173/tcp 41905 amin node Vite dev server (admin)
25
+ 5432/tcp 612 postgres postgres PostgreSQL
26
+ 6379/tcp 788 redis redis-server Redis
27
+ 8080/tcp 39044 amin docker-proxy Docker published port
28
+ 22/tcp 1 root sshd OpenSSH server [protected]
29
+ ↑↓/jk move · PgUp/PgDn/g/G jump · / filter · x kill · r rescan · u udp · q quit
30
+ ```
31
+
32
+ ## Install
33
+
34
+ ```sh
35
+ npm install -g slash-port
36
+ ```
37
+
38
+ Or run it once, without installing:
39
+
40
+ ```sh
41
+ npx slash-port
42
+ ```
43
+
44
+ Requires Node 22 or newer. Works on Linux, macOS, and Windows.
45
+
46
+ ## Use
47
+
48
+ ```sh
49
+ slash-port # the interactive list
50
+ slash-port 3000 # open on port 3000
51
+ slash-port --plain # a plain table, for a pipe or a script
52
+ slash-port --json # the same data as JSON
53
+ slash-port --udp # include UDP as well as TCP
54
+ ```
55
+
56
+ To kill something from a script, name it and confirm it:
57
+
58
+ ```sh
59
+ slash-port --kill --port 3000 --yes
60
+ ```
61
+
62
+ There is no flag combination that kills something without naming it first.
63
+
64
+ ### Keys
65
+
66
+ | Key | Does |
67
+ | --- | --- |
68
+ | `↑` `↓` or `j` `k` | Move |
69
+ | `PgUp` `PgDn` | Page |
70
+ | `g` `G` | First, last |
71
+ | `/` | Filter; `Enter` keeps it, `Esc` clears it |
72
+ | `x` or `Enter` | Kill the selected port |
73
+ | `r` | Rescan |
74
+ | `u` | Show UDP as well as TCP |
75
+ | `q` | Quit |
76
+
77
+ In the confirmation dialog: `y` sends SIGTERM, `f` forces with SIGKILL, `n`
78
+ cancels. The kill key is deliberately not next to a navigation key.
79
+
80
+ ### Options
81
+
82
+ | Option | Does |
83
+ | --- | --- |
84
+ | `-p`, `--port <number>` | Only this port |
85
+ | `-u`, `--udp` | Include UDP sockets |
86
+ | `--json` | Print JSON and exit |
87
+ | `--plain` | Print a plain table and exit |
88
+ | `--kill` | Kill the process on `--port`. Requires `--yes` |
89
+ | `--force` | Escalate to SIGKILL instead of SIGTERM |
90
+ | `-y`, `--yes` | Confirm a kill made from the command line |
91
+ | `--grace <ms>` | Wait this long for a graceful exit (default 3000) |
92
+ | `--no-color` | Disable colour. `NO_COLOR` is honoured too |
93
+ | `-h`, `--help` | Show help |
94
+ | `-v`, `--version` | Show the version |
95
+
96
+ ### Exit codes
97
+
98
+ | Code | Means |
99
+ | --- | --- |
100
+ | `0` | Success |
101
+ | `1` | The requested action could not be completed |
102
+ | `2` | Invalid usage |
103
+
104
+ Asking about one port is a question with a yes-or-no answer, so
105
+ `slash-port --port 3000 --plain` exits `1` when nothing is listening there.
106
+ Listing every port exits `0` even when the list is empty.
107
+
108
+ ## Safety
109
+
110
+ Killing the wrong process at a terminal is easy and unrecoverable, so the rules
111
+ are fixed rather than configurable:
112
+
113
+ - **Nothing is killed without a confirmation that names it.** Not in the
114
+ interactive list, and not with flags.
115
+ - **Some processes are refused outright**, before any dialog is offered: the
116
+ init process, `sshd` — killing it locks you out of a remote machine — macOS
117
+ and Windows session processes, `slash-port` itself, and the shell that
118
+ launched it.
119
+ - **SIGTERM before SIGKILL.** A process that ignores SIGTERM is reported as
120
+ having survived. Escalating is a second, deliberate action, never automatic.
121
+ - **A process that has already exited is never signalled**, because by then its
122
+ pid may belong to something else.
123
+
124
+ On Windows there is no signal delivery: SIGTERM becomes `TerminateProcess`,
125
+ which a process cannot catch or ignore, so nothing there gets the chance to
126
+ shut down cleanly. The confirmation still applies — but "terminate" and "force"
127
+ do the same thing.
128
+
129
+ ## Privacy
130
+
131
+ `slash-port` makes no network connections at any point. It reads the local
132
+ socket table and the local process table, and that is all it does. There is no
133
+ telemetry, no update check, and no configuration file.
134
+
135
+ ## Terminal behaviour
136
+
137
+ - Only the sixteen named terminal colours, so the display inherits your theme
138
+ rather than fighting it.
139
+ - Colour never carries meaning on its own — a protected row is labelled
140
+ `[protected]` as well as coloured.
141
+ - `NO_COLOR` and `--no-color` are honoured.
142
+ - Redirected or piped output is plain text with no control codes, and the
143
+ interactive interface never starts unless both streams are a terminal.
144
+ - The list is windowed to the visible rows, so a machine with four hundred
145
+ listening sockets renders a screenful, not four hundred lines.
146
+ - Columns are dropped in order of how little they carry as the window narrows,
147
+ and values that are cut are marked with an ellipsis.
148
+
149
+ ## How it identifies a process
150
+
151
+ Three sources, in priority order:
152
+
153
+ 1. **The command line.** Specific frameworks are matched before the runtimes
154
+ that host them, so `node …/vite` reports Vite rather than Node.js.
155
+ 2. **The project.** The directory above `node_modules` in the command line, so
156
+ two Vite servers on 5173 and 5174 can be told apart.
157
+ 3. **A well-known port registry**, used only when the process itself could not
158
+ be identified — mostly other users' processes. Entries that would add
159
+ nothing are suppressed: "dev server" on port 3000 is not information.
160
+
161
+ Per platform:
162
+
163
+ - **Linux** reads `/proc/net/tcp` and maps socket inodes through
164
+ `/proc/[pid]/fd`. No `lsof`, which many container images do not have, and no
165
+ subprocess. Descriptors belonging to other users are not readable without
166
+ privileges, so those rows show no owner rather than failing the scan — run
167
+ with `sudo` to resolve them.
168
+ - **macOS** uses `lsof` in field-output mode, plus `ps` for full command lines.
169
+ - **Windows** uses `netstat -ano` and `tasklist`, which exist on every edition
170
+ and avoid PowerShell's startup cost.
171
+
172
+ ## Not built yet
173
+
174
+ Deliberate omissions, listed so you know they are choices rather than
175
+ oversights:
176
+
177
+ - **Docker awareness.** A published port shows `docker-proxy` rather than the
178
+ container behind it. Resolving that means talking to the Docker socket, which
179
+ is a real dependency and belongs behind a flag.
180
+ - **Process trees.** Killing a dev server sometimes leaves children behind. A
181
+ `--tree` option would signal the whole group.
182
+ - **Watch mode.** The list rescans on `r`, not on a timer.
183
+ - **Port history.** "What was on 3000 an hour ago" needs persistent state, and
184
+ this tool currently has none — which is worth keeping.
185
+
186
+ ## Versioning and releases
187
+
188
+ Versions follow [Semantic Versioning](https://semver.org) and are chosen by the
189
+ pipeline rather than by hand: every commit on `main` is a
190
+ [Conventional Commit](https://www.conventionalcommits.org), and the release
191
+ workflow reads the types since the last tag to decide between a major, a minor,
192
+ and a patch. Every release is published with npm
193
+ [provenance](https://docs.npmjs.com/generating-provenance-statements), so the
194
+ tarball can be traced to the exact commit and workflow that built it.
195
+
196
+ See the [changelog](CHANGELOG.md) for what changed when.
197
+
198
+ ## Contributing
199
+
200
+ See [CONTRIBUTING.md](CONTRIBUTING.md). The one step that is a requirement
201
+ rather than a suggestion is enabling the repository's git hooks:
202
+
203
+ ```sh
204
+ git config core.hooksPath .githooks
205
+ ```
206
+
207
+ ## Licence
208
+
209
+ MIT © Amin Shariati
package/dist/cli.js ADDED
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { readFileSync } from 'node:fs';
4
+ import { plainTable, toJson } from './format.js';
5
+ import { killEntry } from './kill.js';
6
+ import { scan } from './scan/index.js';
7
+ import { ScanError } from './types.js';
8
+ /** 0 success · 1 the action could not be completed · 2 invalid usage. */
9
+ const EXIT_OK = 0;
10
+ const EXIT_FAILED = 1;
11
+ const EXIT_USAGE = 2;
12
+ class UsageError extends Error {
13
+ }
14
+ function parseArgs(argv) {
15
+ const options = {
16
+ port: null,
17
+ udp: false,
18
+ json: false,
19
+ plain: false,
20
+ kill: false,
21
+ force: false,
22
+ yes: false,
23
+ graceMs: 3000,
24
+ color: true,
25
+ help: false,
26
+ version: false,
27
+ };
28
+ const value = (flag, next) => {
29
+ if (next === undefined || next.startsWith('-'))
30
+ throw new UsageError(`${flag} needs a value.`);
31
+ return next;
32
+ };
33
+ for (let index = 0; index < argv.length; index += 1) {
34
+ const argument = argv[index];
35
+ switch (argument) {
36
+ case '-p':
37
+ case '--port': {
38
+ const raw = value(argument, argv[++index]);
39
+ const port = Number.parseInt(raw, 10);
40
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
41
+ throw new UsageError(`${raw} is not a port number between 1 and 65535.`);
42
+ }
43
+ options.port = port;
44
+ break;
45
+ }
46
+ case '-u':
47
+ case '--udp':
48
+ options.udp = true;
49
+ break;
50
+ case '--json':
51
+ options.json = true;
52
+ break;
53
+ case '--plain':
54
+ options.plain = true;
55
+ break;
56
+ case '--kill':
57
+ options.kill = true;
58
+ break;
59
+ case '--force':
60
+ options.force = true;
61
+ break;
62
+ case '-y':
63
+ case '--yes':
64
+ options.yes = true;
65
+ break;
66
+ case '--grace': {
67
+ const raw = value(argument, argv[++index]);
68
+ const ms = Number.parseInt(raw, 10);
69
+ if (!Number.isInteger(ms) || ms < 0)
70
+ throw new UsageError(`${raw} is not a number of milliseconds.`);
71
+ options.graceMs = ms;
72
+ break;
73
+ }
74
+ case '--no-color':
75
+ case '--no-colour':
76
+ options.color = false;
77
+ break;
78
+ case '-h':
79
+ case '--help':
80
+ options.help = true;
81
+ break;
82
+ case '-v':
83
+ case '--version':
84
+ options.version = true;
85
+ break;
86
+ default:
87
+ if (/^\d+$/.test(argument) && options.port === null) {
88
+ // `slash-port 3000` is what everyone tries first.
89
+ const port = Number.parseInt(argument, 10);
90
+ if (port < 1 || port > 65535)
91
+ throw new UsageError(`${argument} is not a port number.`);
92
+ options.port = port;
93
+ break;
94
+ }
95
+ throw new UsageError(`Unknown option: ${argument}`);
96
+ }
97
+ }
98
+ if (options.json && options.plain)
99
+ throw new UsageError('--json and --plain cannot both be used.');
100
+ // The one rule that matters: nothing is ever killed without being named and
101
+ // confirmed, and no flag combination gets round it.
102
+ if (options.kill) {
103
+ if (options.port === null)
104
+ throw new UsageError('--kill needs --port, so the target is named.');
105
+ if (!options.yes)
106
+ throw new UsageError('--kill needs --yes, so the kill is confirmed.');
107
+ }
108
+ if (options.force && !options.kill)
109
+ throw new UsageError('--force only means something with --kill.');
110
+ return options;
111
+ }
112
+ const HELP = `slash-port — see what is listening on your ports, and kill it safely.
113
+
114
+ Usage
115
+ slash-port [port] [options]
116
+
117
+ Options
118
+ -p, --port <number> Only this port
119
+ -u, --udp Include UDP sockets as well as TCP
120
+ --json Print JSON to stdout and exit
121
+ --plain Print a plain table and exit
122
+ --kill Kill the process on --port. Requires --yes
123
+ --force Escalate to SIGKILL instead of SIGTERM
124
+ -y, --yes Confirm a kill made from the command line
125
+ --grace <ms> Wait this long for a graceful exit (default 3000)
126
+ --no-color Disable colour
127
+ -h, --help Show this help
128
+ -v, --version Show the version
129
+
130
+ Keys
131
+ up/down or j/k move / filter x or Enter kill
132
+ PgUp/PgDn page r rescan u toggle UDP
133
+ g / G first / last q quit y/f/n confirm dialog
134
+
135
+ Exit codes
136
+ 0 success
137
+ 1 the requested action could not be completed
138
+ 2 invalid usage
139
+
140
+ slash-port never kills without confirmation; never kills init, sshd, your
141
+ session, or the shell that launched it; sends SIGTERM before SIGKILL; and
142
+ never touches the network.`;
143
+ function readVersion() {
144
+ try {
145
+ const manifest = readFileSync(new URL('../package.json', import.meta.url), 'utf8');
146
+ return JSON.parse(manifest).version ?? '0.0.0';
147
+ }
148
+ catch {
149
+ return '0.0.0';
150
+ }
151
+ }
152
+ function selectPort(entries, options) {
153
+ if (options.port === null)
154
+ return [...entries];
155
+ return entries.filter((entry) => entry.port === options.port && (options.udp || entry.protocol === 'tcp'));
156
+ }
157
+ async function killFromCli(entries, options) {
158
+ const targets = selectPort(entries, options);
159
+ if (targets.length === 0) {
160
+ process.stderr.write(`Nothing is listening on port ${options.port}.\n`);
161
+ return EXIT_FAILED;
162
+ }
163
+ let failures = 0;
164
+ for (const target of targets) {
165
+ const result = await killEntry(target, {
166
+ signal: options.force ? 'SIGKILL' : 'SIGTERM',
167
+ graceMs: options.graceMs,
168
+ });
169
+ const ok = result.status === 'terminated' || result.status === 'gone';
170
+ (ok ? process.stdout : process.stderr).write(`${result.message}\n`);
171
+ if (!ok)
172
+ failures += 1;
173
+ }
174
+ return failures === 0 ? EXIT_OK : EXIT_FAILED;
175
+ }
176
+ async function main(argv) {
177
+ let options;
178
+ try {
179
+ options = parseArgs(argv);
180
+ }
181
+ catch (error) {
182
+ process.stderr.write(`${error.message}\n\nRun slash-port --help for usage.\n`);
183
+ return EXIT_USAGE;
184
+ }
185
+ if (options.help) {
186
+ process.stdout.write(`${HELP}\n`);
187
+ return EXIT_OK;
188
+ }
189
+ if (options.version) {
190
+ process.stdout.write(`${readVersion()}\n`);
191
+ return EXIT_OK;
192
+ }
193
+ // Set before Ink or chalk is loaded, because both decide colour support at
194
+ // import time. This is why the UI is imported lazily further down.
195
+ if (!options.color)
196
+ process.env['NO_COLOR'] = '1';
197
+ let entries;
198
+ try {
199
+ entries = await scan({ udp: options.udp });
200
+ }
201
+ catch (error) {
202
+ if (error instanceof ScanError) {
203
+ process.stderr.write(`${error.message}\n`);
204
+ if (error.hint)
205
+ process.stderr.write(`${error.hint}\n`);
206
+ return EXIT_FAILED;
207
+ }
208
+ throw error;
209
+ }
210
+ if (options.kill)
211
+ return killFromCli(entries, options);
212
+ // A pipe or a redirect gets plain text, never control codes, and the TUI is
213
+ // never started when either stream is not a terminal.
214
+ const interactive = !options.json && !options.plain && Boolean(process.stdout.isTTY) && Boolean(process.stdin.isTTY);
215
+ if (!interactive) {
216
+ const selected = selectPort(entries, options);
217
+ if (options.json) {
218
+ process.stdout.write(`${JSON.stringify(toJson(selected), null, 2)}\n`);
219
+ }
220
+ else {
221
+ process.stdout.write(`${plainTable(selected)}\n`);
222
+ }
223
+ // Asking about one port is a question with a yes-or-no answer, so an empty
224
+ // result is a failure. Asking for the whole list is not.
225
+ return options.port !== null && selected.length === 0 ? EXIT_FAILED : EXIT_OK;
226
+ }
227
+ const [{ render }, { App }] = await Promise.all([import('ink'), import('./ui/App.js')]);
228
+ const instance = render(_jsx(App, { initialEntries: entries, initialFilter: options.port === null ? '' : String(options.port), udp: options.udp }));
229
+ await instance.waitUntilExit();
230
+ return EXIT_OK;
231
+ }
232
+ main(process.argv.slice(2)).then((code) => {
233
+ // Set rather than exit, so piped stdout is flushed before the process ends.
234
+ process.exitCode = code;
235
+ }, (error) => {
236
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
237
+ process.exitCode = EXIT_FAILED;
238
+ });