porthawk 0.1.5 → 0.1.7

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/index.js CHANGED
@@ -19,15 +19,20 @@ function colorizeOrigin(origin) {
19
19
 
20
20
  // src/commands/list.ts
21
21
  function registerListCommand(program2) {
22
- program2.command("list").description("list all listening ports").action(async () => {
22
+ program2.command("list").description("list all listening ports").option("--json", "output as JSON instead of a table").action(async (options) => {
23
23
  try {
24
24
  const ports = await getListeningPorts();
25
+ ports.sort((a, b) => a.port - b.port);
26
+ if (options.json) {
27
+ console.log(JSON.stringify(ports, null, 2));
28
+ return;
29
+ }
25
30
  if (ports.length === 0) {
26
31
  console.log("no listening ports found");
27
32
  return;
28
33
  }
29
34
  const table = new Table({ head: [pc2.bold("PORT"), pc2.bold("PROCESS"), pc2.bold("PID"), pc2.bold("ORIGIN")] });
30
- for (const port of ports.sort((a, b) => a.port - b.port)) {
35
+ for (const port of ports) {
31
36
  table.push([String(port.port), port.processName || "?", String(port.pid), colorizeOrigin(port.origin)]);
32
37
  }
33
38
  console.log(table.toString());
@@ -151,7 +156,7 @@ function App() {
151
156
  });
152
157
  const selected = ports[selectedIndex];
153
158
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
154
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "porthawk watch \xE2\u20AC\u201D \xE2\u2020\u2018/\xE2\u2020\u201C select, k kill, q quit" }),
159
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "porthawk watch \u2014 \u2191/\u2193 select, k kill, q quit" }),
155
160
  /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
156
161
  ports.length === 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: "no listening ports found" }),
157
162
  ports.map((port, index) => {
@@ -173,7 +178,7 @@ function App() {
173
178
  selected.port,
174
179
  "? (y/n)"
175
180
  ] }) }),
176
- status === "killing" && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "killing\xE2\u20AC\xA6" }) }),
181
+ status === "killing" && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "killing\u2026" }) }),
177
182
  message && status === "idle" && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "green", children: message }) }),
178
183
  error && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: error }) })
179
184
  ] });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/commands/list.ts","../src/format.ts","../src/commands/kill.ts","../src/commands/watch.ts","../src/ui/App.tsx"],"sourcesContent":["#!/usr/bin/env node\nimport { createRequire } from 'node:module';\nimport { Command } from 'commander';\nimport { registerListCommand } from './commands/list.js';\nimport { registerKillCommand } from './commands/kill.js';\nimport { registerWatchCommand } from './commands/watch.js';\n\nconst require = createRequire(import.meta.url);\nconst pkg = require('../package.json') as { version: string };\n\nconst program = new Command();\n\nprogram\n .name('porthawk')\n .description(\"see what's listening on your machine's ports and kill what shouldn't be\")\n .version(pkg.version);\n\nregisterListCommand(program);\nregisterKillCommand(program);\nregisterWatchCommand(program);\n\nprogram.parseAsync(process.argv).catch((error: unknown) => {\n console.error(`porthawk: ${error instanceof Error ? error.message : String(error)}`);\n process.exitCode = 1;\n});\n","import type { Command } from 'commander';\nimport Table from 'cli-table3';\nimport pc from 'picocolors';\nimport { getListeningPorts } from 'porthawk-core';\nimport { colorizeOrigin } from '../format.js';\n\nexport function registerListCommand(program: Command): void {\n program\n .command('list')\n .description('list all listening ports')\n .action(async () => {\n try {\n const ports = await getListeningPorts();\n\n if (ports.length === 0) {\n console.log('no listening ports found');\n return;\n }\n\n const table = new Table({ head: [pc.bold('PORT'), pc.bold('PROCESS'), pc.bold('PID'), pc.bold('ORIGIN')] });\n\n for (const port of ports.sort((a, b) => a.port - b.port)) {\n table.push([String(port.port), port.processName || '?', String(port.pid), colorizeOrigin(port.origin)]);\n }\n\n console.log(table.toString());\n } catch (error) {\n console.error(`porthawk: ${error instanceof Error ? error.message : String(error)}`);\n process.exitCode = 1;\n }\n });\n}\n","import pc from 'picocolors';\nimport type { Origin } from 'porthawk-core';\n\nexport function colorizeOrigin(origin: Origin): string {\n if (origin === 'agent') return pc.yellow(origin);\n if (origin === 'manual') return pc.green(origin);\n return pc.dim(origin);\n}\n","import type { Command } from 'commander';\nimport * as clack from '@clack/prompts';\nimport { getListeningPorts, killProcess } from 'porthawk-core';\n\nexport function registerKillCommand(program: Command): void {\n program\n .command('kill <port>')\n .description('kill whatever is listening on a port')\n .action(async (portArg: string) => {\n const port = Number(portArg);\n if (!Number.isInteger(port) || port <= 0) {\n console.error(`porthawk: invalid port \"${portArg}\"`);\n process.exitCode = 1;\n return;\n }\n\n try {\n const ports = await getListeningPorts();\n const match = ports.find((entry) => entry.port === port);\n\n if (!match) {\n console.error(`porthawk: nothing is listening on port ${port}`);\n process.exitCode = 1;\n return;\n }\n\n const confirmed = await clack.confirm({\n message: `kill ${match.processName || 'unknown process'} (pid ${match.pid}) listening on port ${port}?`,\n initialValue: false,\n });\n\n if (clack.isCancel(confirmed) || !confirmed) {\n clack.cancel('not killed');\n return;\n }\n\n await killProcess(match.pid);\n console.log(`killed ${match.processName || 'unknown process'} (pid ${match.pid})`);\n } catch (error) {\n console.error(`porthawk: ${error instanceof Error ? error.message : String(error)}`);\n process.exitCode = 1;\n }\n });\n}\n","import type { Command } from 'commander';\nimport { render } from 'ink';\nimport React from 'react';\nimport { App } from '../ui/App.js';\n\nexport function registerWatchCommand(program: Command): void {\n program\n .command('watch')\n .description('live view of listening ports')\n .action(async () => {\n if (!process.stdin.isTTY) {\n console.error('porthawk: watch requires an interactive terminal');\n process.exitCode = 1;\n return;\n }\n\n const instance = render(React.createElement(App));\n await instance.waitUntilExit();\n });\n}\n","import React, { useCallback, useEffect, useState } from 'react';\nimport { Box, Text, useApp, useInput } from 'ink';\nimport { getListeningPorts, killProcess, type PortInfo } from 'porthawk-core';\n\nconst REFRESH_INTERVAL_MS = 2000;\n\ntype Status = 'idle' | 'confirm-kill' | 'killing';\n\nexport function App(): React.ReactElement {\n const { exit } = useApp();\n const [ports, setPorts] = useState<PortInfo[]>([]);\n const [selectedIndex, setSelectedIndex] = useState(0);\n const [status, setStatus] = useState<Status>('idle');\n const [error, setError] = useState<string | null>(null);\n const [message, setMessage] = useState<string | null>(null);\n\n const refresh = useCallback(async () => {\n try {\n const next = await getListeningPorts();\n setPorts(next);\n setError(null);\n setSelectedIndex((current) => Math.min(current, Math.max(next.length - 1, 0)));\n } catch (refreshError) {\n setError(refreshError instanceof Error ? refreshError.message : String(refreshError));\n }\n }, []);\n\n useEffect(() => {\n void refresh();\n const interval = setInterval(() => {\n if (status === 'idle') void refresh();\n }, REFRESH_INTERVAL_MS);\n return () => clearInterval(interval);\n }, [refresh, status]);\n\n useInput((input, key) => {\n if (status === 'confirm-kill') {\n if (input === 'y' || key.return) {\n const target = ports[selectedIndex];\n if (!target) {\n setStatus('idle');\n return;\n }\n setStatus('killing');\n killProcess(target.pid)\n .then(() => {\n setMessage(`killed ${target.processName || 'unknown process'} (pid ${target.pid})`);\n setStatus('idle');\n void refresh();\n })\n .catch((killError: unknown) => {\n setError(killError instanceof Error ? killError.message : String(killError));\n setStatus('idle');\n });\n return;\n }\n\n if (input === 'n' || key.escape) {\n setStatus('idle');\n }\n return;\n }\n\n if (input === 'q') {\n exit();\n return;\n }\n\n if (key.upArrow) {\n setSelectedIndex((current) => Math.max(current - 1, 0));\n return;\n }\n\n if (key.downArrow) {\n setSelectedIndex((current) => Math.min(current + 1, Math.max(ports.length - 1, 0)));\n return;\n }\n\n if (input === 'k' && ports.length > 0) {\n setMessage(null);\n setStatus('confirm-kill');\n }\n });\n\n const selected = ports[selectedIndex];\n\n return (\n <Box flexDirection=\"column\">\n <Text dimColor>porthawk watch — ↑/↓ select, k kill, q quit</Text>\n <Box flexDirection=\"column\" marginTop={1}>\n {ports.length === 0 && <Text dimColor>no listening ports found</Text>}\n {ports.map((port, index) => {\n const isSelected = index === selectedIndex;\n const label = `:${port.port} ${port.processName || '?'} (pid ${port.pid})`;\n return (\n <Text key={`${port.pid}:${port.port}:${port.protocol}`} color={isSelected ? 'cyan' : undefined}>\n {isSelected ? '> ' : ' '}\n {label}\n {port.origin === 'agent' ? ' [agent]' : ''}\n </Text>\n );\n })}\n </Box>\n {status === 'confirm-kill' && selected && (\n <Box marginTop={1}>\n <Text color=\"yellow\">\n kill {selected.processName || 'unknown process'} (pid {selected.pid}) on port {selected.port}? (y/n)\n </Text>\n </Box>\n )}\n {status === 'killing' && (\n <Box marginTop={1}>\n <Text dimColor>killing…</Text>\n </Box>\n )}\n {message && status === 'idle' && (\n <Box marginTop={1}>\n <Text color=\"green\">{message}</Text>\n </Box>\n )}\n {error && (\n <Box marginTop={1}>\n <Text color=\"red\">{error}</Text>\n </Box>\n )}\n </Box>\n );\n}\n"],"mappings":"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","names":["pc","program","pc","getListeningPorts","program","React","getListeningPorts","killProcess","program","React","require"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/commands/list.ts","../src/format.ts","../src/commands/kill.ts","../src/commands/watch.ts","../src/ui/App.tsx"],"sourcesContent":["#!/usr/bin/env node\nimport { createRequire } from 'node:module';\nimport { Command } from 'commander';\nimport { registerListCommand } from './commands/list.js';\nimport { registerKillCommand } from './commands/kill.js';\nimport { registerWatchCommand } from './commands/watch.js';\n\nconst require = createRequire(import.meta.url);\nconst pkg = require('../package.json') as { version: string };\n\nconst program = new Command();\n\nprogram\n .name('porthawk')\n .description(\"see what's listening on your machine's ports and kill what shouldn't be\")\n .version(pkg.version);\n\nregisterListCommand(program);\nregisterKillCommand(program);\nregisterWatchCommand(program);\n\nprogram.parseAsync(process.argv).catch((error: unknown) => {\n console.error(`porthawk: ${error instanceof Error ? error.message : String(error)}`);\n process.exitCode = 1;\n});\n","import type { Command } from 'commander';\nimport Table from 'cli-table3';\nimport pc from 'picocolors';\nimport { getListeningPorts } from 'porthawk-core';\nimport { colorizeOrigin } from '../format.js';\n\nexport function registerListCommand(program: Command): void {\n program\n .command('list')\n .description('list all listening ports')\n .option('--json', 'output as JSON instead of a table')\n .action(async (options: { json?: boolean }) => {\n try {\n const ports = await getListeningPorts();\n ports.sort((a, b) => a.port - b.port);\n\n if (options.json) {\n console.log(JSON.stringify(ports, null, 2));\n return;\n }\n\n if (ports.length === 0) {\n console.log('no listening ports found');\n return;\n }\n\n const table = new Table({ head: [pc.bold('PORT'), pc.bold('PROCESS'), pc.bold('PID'), pc.bold('ORIGIN')] });\n\n for (const port of ports) {\n table.push([String(port.port), port.processName || '?', String(port.pid), colorizeOrigin(port.origin)]);\n }\n\n console.log(table.toString());\n } catch (error) {\n console.error(`porthawk: ${error instanceof Error ? error.message : String(error)}`);\n process.exitCode = 1;\n }\n });\n}\n","import pc from 'picocolors';\nimport type { Origin } from 'porthawk-core';\n\nexport function colorizeOrigin(origin: Origin): string {\n if (origin === 'agent') return pc.yellow(origin);\n if (origin === 'manual') return pc.green(origin);\n return pc.dim(origin);\n}\n","import type { Command } from 'commander';\nimport * as clack from '@clack/prompts';\nimport { getListeningPorts, killProcess } from 'porthawk-core';\n\nexport function registerKillCommand(program: Command): void {\n program\n .command('kill <port>')\n .description('kill whatever is listening on a port')\n .action(async (portArg: string) => {\n const port = Number(portArg);\n if (!Number.isInteger(port) || port <= 0) {\n console.error(`porthawk: invalid port \"${portArg}\"`);\n process.exitCode = 1;\n return;\n }\n\n try {\n const ports = await getListeningPorts();\n const match = ports.find((entry) => entry.port === port);\n\n if (!match) {\n console.error(`porthawk: nothing is listening on port ${port}`);\n process.exitCode = 1;\n return;\n }\n\n const confirmed = await clack.confirm({\n message: `kill ${match.processName || 'unknown process'} (pid ${match.pid}) listening on port ${port}?`,\n initialValue: false,\n });\n\n if (clack.isCancel(confirmed) || !confirmed) {\n clack.cancel('not killed');\n return;\n }\n\n await killProcess(match.pid);\n console.log(`killed ${match.processName || 'unknown process'} (pid ${match.pid})`);\n } catch (error) {\n console.error(`porthawk: ${error instanceof Error ? error.message : String(error)}`);\n process.exitCode = 1;\n }\n });\n}\n","import type { Command } from 'commander';\nimport { render } from 'ink';\nimport React from 'react';\nimport { App } from '../ui/App.js';\n\nexport function registerWatchCommand(program: Command): void {\n program\n .command('watch')\n .description('live view of listening ports')\n .action(async () => {\n if (!process.stdin.isTTY) {\n console.error('porthawk: watch requires an interactive terminal');\n process.exitCode = 1;\n return;\n }\n\n const instance = render(React.createElement(App));\n await instance.waitUntilExit();\n });\n}\n","import React, { useCallback, useEffect, useState } from 'react';\nimport { Box, Text, useApp, useInput } from 'ink';\nimport { getListeningPorts, killProcess, type PortInfo } from 'porthawk-core';\n\nconst REFRESH_INTERVAL_MS = 2000;\n\ntype Status = 'idle' | 'confirm-kill' | 'killing';\n\nexport function App(): React.ReactElement {\n const { exit } = useApp();\n const [ports, setPorts] = useState<PortInfo[]>([]);\n const [selectedIndex, setSelectedIndex] = useState(0);\n const [status, setStatus] = useState<Status>('idle');\n const [error, setError] = useState<string | null>(null);\n const [message, setMessage] = useState<string | null>(null);\n\n const refresh = useCallback(async () => {\n try {\n const next = await getListeningPorts();\n setPorts(next);\n setError(null);\n setSelectedIndex((current) => Math.min(current, Math.max(next.length - 1, 0)));\n } catch (refreshError) {\n setError(refreshError instanceof Error ? refreshError.message : String(refreshError));\n }\n }, []);\n\n useEffect(() => {\n void refresh();\n const interval = setInterval(() => {\n if (status === 'idle') void refresh();\n }, REFRESH_INTERVAL_MS);\n return () => clearInterval(interval);\n }, [refresh, status]);\n\n useInput((input, key) => {\n if (status === 'confirm-kill') {\n if (input === 'y' || key.return) {\n const target = ports[selectedIndex];\n if (!target) {\n setStatus('idle');\n return;\n }\n setStatus('killing');\n killProcess(target.pid)\n .then(() => {\n setMessage(`killed ${target.processName || 'unknown process'} (pid ${target.pid})`);\n setStatus('idle');\n void refresh();\n })\n .catch((killError: unknown) => {\n setError(killError instanceof Error ? killError.message : String(killError));\n setStatus('idle');\n });\n return;\n }\n\n if (input === 'n' || key.escape) {\n setStatus('idle');\n }\n return;\n }\n\n if (input === 'q') {\n exit();\n return;\n }\n\n if (key.upArrow) {\n setSelectedIndex((current) => Math.max(current - 1, 0));\n return;\n }\n\n if (key.downArrow) {\n setSelectedIndex((current) => Math.min(current + 1, Math.max(ports.length - 1, 0)));\n return;\n }\n\n if (input === 'k' && ports.length > 0) {\n setMessage(null);\n setStatus('confirm-kill');\n }\n });\n\n const selected = ports[selectedIndex];\n\n return (\n <Box flexDirection=\"column\">\n <Text dimColor>porthawk watch — ↑/↓ select, k kill, q quit</Text>\n <Box flexDirection=\"column\" marginTop={1}>\n {ports.length === 0 && <Text dimColor>no listening ports found</Text>}\n {ports.map((port, index) => {\n const isSelected = index === selectedIndex;\n const label = `:${port.port} ${port.processName || '?'} (pid ${port.pid})`;\n return (\n <Text key={`${port.pid}:${port.port}:${port.protocol}`} color={isSelected ? 'cyan' : undefined}>\n {isSelected ? '> ' : ' '}\n {label}\n {port.origin === 'agent' ? ' [agent]' : ''}\n </Text>\n );\n })}\n </Box>\n {status === 'confirm-kill' && selected && (\n <Box marginTop={1}>\n <Text color=\"yellow\">\n kill {selected.processName || 'unknown process'} (pid {selected.pid}) on port {selected.port}? (y/n)\n </Text>\n </Box>\n )}\n {status === 'killing' && (\n <Box marginTop={1}>\n <Text dimColor>killing…</Text>\n </Box>\n )}\n {message && status === 'idle' && (\n <Box marginTop={1}>\n <Text color=\"green\">{message}</Text>\n </Box>\n )}\n {error && (\n <Box marginTop={1}>\n <Text color=\"red\">{error}</Text>\n </Box>\n )}\n </Box>\n );\n}\n"],"mappings":"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","names":["pc","program","pc","getListeningPorts","program","React","getListeningPorts","killProcess","program","React","require"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "porthawk",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "See every port your machine is listening on and kill what shouldn't still be running",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,7 +20,7 @@
20
20
  "ink": "^6.8.0",
21
21
  "picocolors": "^1.1.1",
22
22
  "react": "^19.2.8",
23
- "porthawk-core": "0.1.5"
23
+ "porthawk-core": "0.1.7"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^26.1.1",