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/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
- /** 0 success · 1 the action could not be completed · 2 invalid usage. */
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 EXIT_FAILED = 1;
12
- const EXIT_USAGE = 2;
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 — see what is listening on your ports, and kill it safely.
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 the requested action could not be completed
157
- 2 invalid usage
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; and
161
- never touches the network.`;
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 EXIT_FAILED;
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
- failures += 1;
258
+ if (!ok) {
259
+ if (result.status === 'refused')
260
+ refusals += 1;
261
+ else
262
+ failures += 1;
263
+ }
195
264
  }
196
- return failures === 0 ? EXIT_OK : EXIT_FAILED;
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
- process.stderr.write(`${error.message}\n\nRun slash-port --help for usage.\n`);
205
- return EXIT_USAGE;
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 one port is a question with a yes-or-no answer, so an empty
246
- // result is a failure. Asking for the whole list is not.
247
- return options.port !== null && selected.length === 0 ? EXIT_FAILED : EXIT_OK;
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
  }