slash-port 0.2.0 → 0.3.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/README.md +279 -33
- package/dist/cli.js +105 -23
- package/dist/describe.js +667 -173
- package/dist/docker.js +208 -0
- package/dist/explain.js +209 -0
- package/dist/format.js +43 -5
- package/dist/inspect.js +271 -0
- package/dist/kill.js +6 -6
- package/dist/mode.js +46 -0
- package/dist/ports.js +3 -3
- package/dist/scan/index.js +31 -10
- package/dist/scan/win32.js +1 -1
- package/dist/ui/App.js +187 -27
- package/dist/ui/theme.js +124 -10
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -3,19 +3,46 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { plainTable, toJson } from './format.js';
|
|
5
5
|
import { killEntry } from './kill.js';
|
|
6
|
+
import { isMode, resolveDocker, resolveMode } from './mode.js';
|
|
6
7
|
import { describePortSelector, looksLikePort, matchesPort, parsePortSelector } from './ports.js';
|
|
7
8
|
import { scan } from './scan/index.js';
|
|
8
9
|
import { ScanError } from './types.js';
|
|
9
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* The exit codes every slash-* tool shares, so a script that wraps one can
|
|
12
|
+
* wrap any of them:
|
|
13
|
+
*
|
|
14
|
+
* 0 success
|
|
15
|
+
* 1 invalid arguments or usage
|
|
16
|
+
* 2 the thing asked about was not found
|
|
17
|
+
* 3 refused - a confirmation was missing, or a guardrail tripped
|
|
18
|
+
* 4 the operation was attempted and failed
|
|
19
|
+
*
|
|
20
|
+
* The standard reserves 5 for an integrity failure, which this tool has
|
|
21
|
+
* nothing to verify and so never returns.
|
|
22
|
+
*/
|
|
10
23
|
const EXIT_OK = 0;
|
|
11
|
-
const
|
|
12
|
-
const
|
|
24
|
+
const EXIT_USAGE = 1;
|
|
25
|
+
const EXIT_NOT_FOUND = 2;
|
|
26
|
+
const EXIT_REFUSED = 3;
|
|
27
|
+
const EXIT_FAILED = 4;
|
|
28
|
+
/**
|
|
29
|
+
* A command line that cannot be run. Most of these are malformed and exit 1,
|
|
30
|
+
* but a missing confirmation is a refusal rather than a mistake, so the code
|
|
31
|
+
* travels with the message.
|
|
32
|
+
*/
|
|
13
33
|
class UsageError extends Error {
|
|
34
|
+
code;
|
|
35
|
+
constructor(message, code = EXIT_USAGE) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.code = code;
|
|
38
|
+
}
|
|
14
39
|
}
|
|
15
40
|
function parseArgs(argv) {
|
|
16
41
|
const options = {
|
|
17
42
|
port: null,
|
|
43
|
+
mode: null,
|
|
18
44
|
udp: false,
|
|
45
|
+
docker: null,
|
|
19
46
|
json: false,
|
|
20
47
|
plain: false,
|
|
21
48
|
kill: false,
|
|
@@ -49,10 +76,29 @@ function parseArgs(argv) {
|
|
|
49
76
|
case '--port':
|
|
50
77
|
options.port = selector(value(argument, argv[++index]));
|
|
51
78
|
break;
|
|
79
|
+
case '--beginner':
|
|
80
|
+
options.mode = 'beginner';
|
|
81
|
+
break;
|
|
82
|
+
case '--advanced':
|
|
83
|
+
options.mode = 'advanced';
|
|
84
|
+
break;
|
|
85
|
+
case '--mode': {
|
|
86
|
+
const raw = value(argument, argv[++index]).toLowerCase();
|
|
87
|
+
if (!isMode(raw))
|
|
88
|
+
throw new UsageError(`${raw} is not a mode. Use beginner or advanced.`);
|
|
89
|
+
options.mode = raw;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
52
92
|
case '-u':
|
|
53
93
|
case '--udp':
|
|
54
94
|
options.udp = true;
|
|
55
95
|
break;
|
|
96
|
+
case '--docker':
|
|
97
|
+
options.docker = true;
|
|
98
|
+
break;
|
|
99
|
+
case '--no-docker':
|
|
100
|
+
options.docker = false;
|
|
101
|
+
break;
|
|
56
102
|
case '--json':
|
|
57
103
|
options.json = true;
|
|
58
104
|
break;
|
|
@@ -108,12 +154,13 @@ function parseArgs(argv) {
|
|
|
108
154
|
if (options.kill) {
|
|
109
155
|
if (options.port === null)
|
|
110
156
|
throw new UsageError('--kill needs --port, so the target is named.');
|
|
111
|
-
if (!options.yes)
|
|
112
|
-
throw new UsageError('--kill needs --yes, so the kill is confirmed.');
|
|
157
|
+
if (!options.yes) {
|
|
158
|
+
throw new UsageError('--kill needs --yes, so the kill is confirmed.', EXIT_REFUSED);
|
|
159
|
+
}
|
|
113
160
|
// Whether a pattern happens to match one port today is not the point: the
|
|
114
161
|
// same command matches more tomorrow, so the plural is confirmed up front.
|
|
115
162
|
if (options.port.kind !== 'exact' && !options.all) {
|
|
116
|
-
throw new UsageError(`--kill needs --all to use ${options.port.text}, so killing every match is deliberate
|
|
163
|
+
throw new UsageError(`--kill needs --all to use ${options.port.text}, so killing every match is deliberate.`, EXIT_REFUSED);
|
|
117
164
|
}
|
|
118
165
|
}
|
|
119
166
|
if (options.force && !options.kill)
|
|
@@ -122,7 +169,7 @@ function parseArgs(argv) {
|
|
|
122
169
|
throw new UsageError('--all only means something with --kill.');
|
|
123
170
|
return options;
|
|
124
171
|
}
|
|
125
|
-
const HELP = `slash-port
|
|
172
|
+
const HELP = `slash-port - see what is listening on your ports, and kill it safely.
|
|
126
173
|
|
|
127
174
|
Usage
|
|
128
175
|
slash-port [ports] [options]
|
|
@@ -134,7 +181,11 @@ Ports
|
|
|
134
181
|
|
|
135
182
|
Options
|
|
136
183
|
-p, --port <ports> Only these ports, in any of the forms above
|
|
184
|
+
--beginner Explain each port in plain language (the default)
|
|
185
|
+
--advanced Show the full detail instead of the explanations
|
|
186
|
+
--mode <name> beginner or advanced. SLASH_PORT_MODE sets the default
|
|
137
187
|
-u, --udp Include UDP sockets as well as TCP
|
|
188
|
+
--docker Ask the local Docker socket which container holds a port
|
|
138
189
|
--json Print JSON to stdout and exit
|
|
139
190
|
--plain Print a plain table and exit
|
|
140
191
|
--kill Kill the process on --port. Requires --yes
|
|
@@ -150,15 +201,28 @@ Keys
|
|
|
150
201
|
up/down or j/k move / filter x or Enter kill
|
|
151
202
|
PgUp/PgDn page r rescan u toggle UDP
|
|
152
203
|
g / G first / last q quit y/f/n confirm dialog
|
|
204
|
+
m switch mode d show or hide the detail panel
|
|
205
|
+
|
|
206
|
+
Modes
|
|
207
|
+
Beginner mode is the default. It says what each port is in plain language,
|
|
208
|
+
where to open it, and whether closing it is a good idea. Advanced mode shows
|
|
209
|
+
pids, users, bind addresses, command lines, working directories, uptime, and
|
|
210
|
+
how many connections are open. Press m to switch, or set SLASH_PORT_MODE.
|
|
153
211
|
|
|
154
212
|
Exit codes
|
|
155
213
|
0 success
|
|
156
|
-
1
|
|
157
|
-
2
|
|
214
|
+
1 invalid arguments or usage
|
|
215
|
+
2 nothing is listening on the ports asked about
|
|
216
|
+
3 refused: a confirmation was missing, or a guardrail tripped
|
|
217
|
+
4 the operation was attempted and failed
|
|
158
218
|
|
|
159
219
|
slash-port never kills without confirmation; never kills init, sshd, your
|
|
160
|
-
session, or the shell that launched it; sends SIGTERM before SIGKILL
|
|
161
|
-
|
|
220
|
+
session, or the shell that launched it; and sends SIGTERM before SIGKILL.
|
|
221
|
+
|
|
222
|
+
It makes no network connections, and asks nothing else on the machine anything
|
|
223
|
+
either. --docker is the single exception: it reads the local Docker socket - a
|
|
224
|
+
file on this machine - for the name of the container behind a published port.
|
|
225
|
+
SLASH_PORT_DOCKER=1 turns that on for good.`;
|
|
162
226
|
function readVersion() {
|
|
163
227
|
try {
|
|
164
228
|
const manifest = readFileSync(new URL('../package.json', import.meta.url), 'utf8');
|
|
@@ -180,8 +244,9 @@ async function killFromCli(entries, options) {
|
|
|
180
244
|
const targets = selectPort(entries, options);
|
|
181
245
|
if (targets.length === 0) {
|
|
182
246
|
process.stderr.write(`Nothing is listening on ${describePortSelector(selector)}.\n`);
|
|
183
|
-
return
|
|
247
|
+
return EXIT_NOT_FOUND;
|
|
184
248
|
}
|
|
249
|
+
let refusals = 0;
|
|
185
250
|
let failures = 0;
|
|
186
251
|
for (const target of targets) {
|
|
187
252
|
const result = await killEntry(target, {
|
|
@@ -190,10 +255,20 @@ async function killFromCli(entries, options) {
|
|
|
190
255
|
});
|
|
191
256
|
const ok = result.status === 'terminated' || result.status === 'gone';
|
|
192
257
|
(ok ? process.stdout : process.stderr).write(`${result.message}\n`);
|
|
193
|
-
if (!ok)
|
|
194
|
-
|
|
258
|
+
if (!ok) {
|
|
259
|
+
if (result.status === 'refused')
|
|
260
|
+
refusals += 1;
|
|
261
|
+
else
|
|
262
|
+
failures += 1;
|
|
263
|
+
}
|
|
195
264
|
}
|
|
196
|
-
|
|
265
|
+
// A guardrail that stopped a kill is a refusal; a signal that was sent and
|
|
266
|
+
// did not work is a failure, and outranks it when one command did both.
|
|
267
|
+
if (failures > 0)
|
|
268
|
+
return EXIT_FAILED;
|
|
269
|
+
if (refusals > 0)
|
|
270
|
+
return EXIT_REFUSED;
|
|
271
|
+
return EXIT_OK;
|
|
197
272
|
}
|
|
198
273
|
async function main(argv) {
|
|
199
274
|
let options;
|
|
@@ -201,9 +276,16 @@ async function main(argv) {
|
|
|
201
276
|
options = parseArgs(argv);
|
|
202
277
|
}
|
|
203
278
|
catch (error) {
|
|
204
|
-
|
|
205
|
-
|
|
279
|
+
const code = error instanceof UsageError ? error.code : EXIT_USAGE;
|
|
280
|
+
process.stderr.write(`${error.message}\n`);
|
|
281
|
+
// A refusal names the flag that answers it, so the help pointer would be
|
|
282
|
+
// noise. A malformed command line is the case that needs pointing.
|
|
283
|
+
if (code === EXIT_USAGE)
|
|
284
|
+
process.stderr.write('\nRun slash-port --help for usage.\n');
|
|
285
|
+
return code;
|
|
206
286
|
}
|
|
287
|
+
const mode = resolveMode(options.mode);
|
|
288
|
+
const docker = resolveDocker(options.docker);
|
|
207
289
|
if (options.help) {
|
|
208
290
|
process.stdout.write(`${HELP}\n`);
|
|
209
291
|
return EXIT_OK;
|
|
@@ -218,7 +300,7 @@ async function main(argv) {
|
|
|
218
300
|
process.env['NO_COLOR'] = '1';
|
|
219
301
|
let entries;
|
|
220
302
|
try {
|
|
221
|
-
entries = await scan({ udp: options.udp });
|
|
303
|
+
entries = await scan({ udp: options.udp, docker });
|
|
222
304
|
}
|
|
223
305
|
catch (error) {
|
|
224
306
|
if (error instanceof ScanError) {
|
|
@@ -240,14 +322,14 @@ async function main(argv) {
|
|
|
240
322
|
process.stdout.write(`${JSON.stringify(toJson(selected), null, 2)}\n`);
|
|
241
323
|
}
|
|
242
324
|
else {
|
|
243
|
-
process.stdout.write(`${plainTable(selected)}\n`);
|
|
325
|
+
process.stdout.write(`${plainTable(selected, mode)}\n`);
|
|
244
326
|
}
|
|
245
|
-
// Asking about
|
|
246
|
-
// result
|
|
247
|
-
return options.port !== null && selected.length === 0 ?
|
|
327
|
+
// Asking about a port is a question with a yes-or-no answer, so an empty
|
|
328
|
+
// result means not found. Asking for the whole list is not a question.
|
|
329
|
+
return options.port !== null && selected.length === 0 ? EXIT_NOT_FOUND : EXIT_OK;
|
|
248
330
|
}
|
|
249
331
|
const [{ render }, { App }] = await Promise.all([import('ink'), import('./ui/App.js')]);
|
|
250
|
-
const instance = render(_jsx(App, { initialEntries: entries, initialFilter: options.port === null ? '' : options.port.text, udp: options.udp }));
|
|
332
|
+
const instance = render(_jsx(App, { initialEntries: entries, initialFilter: options.port === null ? '' : options.port.text, udp: options.udp, docker: docker, mode: mode }));
|
|
251
333
|
await instance.waitUntilExit();
|
|
252
334
|
return EXIT_OK;
|
|
253
335
|
}
|