portsmith 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+
9
+ # Virtual envs
10
+ .venv/
11
+ venv/
12
+ env/
13
+
14
+ # Tools
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+
19
+ # OS
20
+ .DS_Store
21
+ Thumbs.db
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Atharva Kokane
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.
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: portsmith
3
+ Version: 0.1.0
4
+ Summary: Local port management CLI — list, kill, watch, and snapshot listening ports
5
+ Project-URL: Homepage, https://github.com/atharvashashankk/portman
6
+ Project-URL: Issues, https://github.com/atharvashashankk/portman/issues
7
+ Author-email: Atharva Kokane <atharvashashankkokane@gmail.com>
8
+ License: MIT License
9
+
10
+ Copyright (c) 2026 Atharva Kokane
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ License-File: LICENSE
30
+ Keywords: cli,devtools,network,port,process
31
+ Classifier: Development Status :: 3 - Alpha
32
+ Classifier: Environment :: Console
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Topic :: System :: Networking
41
+ Classifier: Topic :: Utilities
42
+ Requires-Python: >=3.10
43
+ Requires-Dist: psutil>=5.9.0
44
+ Requires-Dist: rich>=13.0.0
45
+ Requires-Dist: typer>=0.9.0
46
+ Description-Content-Type: text/markdown
47
+
48
+ # portman
49
+
50
+ A local port management CLI for macOS and Linux.
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ pip install portman
56
+ ```
57
+
58
+ Or install from source:
59
+
60
+ ```bash
61
+ git clone <repo>
62
+ cd portman
63
+ pip install -e .
64
+ ```
65
+
66
+ ## Commands
67
+
68
+ ### `portman list`
69
+
70
+ Show all listening ports with PID, process name, port, protocol, and status. System ports (<1024) are highlighted in yellow, user ports in green.
71
+
72
+ ```bash
73
+ portman list
74
+
75
+ # Only show ports opened from this terminal session
76
+ portman list --mine
77
+
78
+ # Machine-readable JSON output
79
+ portman list --json
80
+ ```
81
+
82
+ > **`--mine` note:** this flag filters to processes running under your OS user account. It removes system services (svchost, lsass, etc.) but will still show other user-owned background apps that happen to listen on ports (e.g. Chrome, cloud sync clients). It correctly catches dev servers started in any shell or terminal window.
83
+
84
+ ### `portman kill <port>`
85
+
86
+ Kill the process listening on a given port. Asks for confirmation unless `--force` is passed.
87
+
88
+ ```bash
89
+ portman kill 8080
90
+
91
+ # Skip confirmation
92
+ portman kill 8080 --force
93
+ portman kill 8080 -f
94
+ ```
95
+
96
+ ### `portman watch`
97
+
98
+ Live-updating view of all active ports, refreshing every 2 seconds. New ports are highlighted green, closed ports red. Exit with `Ctrl+C`.
99
+
100
+ ```bash
101
+ portman watch
102
+
103
+ # Only watch ports opened from this terminal session
104
+ portman watch --mine
105
+ ```
106
+
107
+ ### `portman save <name>`
108
+
109
+ Save a snapshot of all currently active user ports (port ≥1024) to `~/.portman/profiles/<name>.json`.
110
+
111
+ ```bash
112
+ portman save myproject
113
+ portman save dev-stack
114
+ ```
115
+
116
+ ### `portman restore <name>`
117
+
118
+ Check a saved profile against currently running ports. Shows which saved processes are still running and which are missing, along with hints for restarting them.
119
+
120
+ ```bash
121
+ portman restore myproject
122
+ ```
123
+
124
+ ### `portman profiles`
125
+
126
+ List all saved profiles with their creation timestamp and port count.
127
+
128
+ ```bash
129
+ portman profiles
130
+ ```
131
+
132
+ ## Requirements
133
+
134
+ - Python 3.10+
135
+ - macOS or Linux (Windows is not supported)
136
+
137
+ ## Notes
138
+
139
+ - Ports below 1024 are system ports and may require `sudo` to kill.
140
+ - `portman restore` does not restart processes — it only reports their status and suggests what needs to be restarted.
141
+ - Profiles are stored in `~/.portman/profiles/` as JSON files.
@@ -0,0 +1,94 @@
1
+ # portman
2
+
3
+ A local port management CLI for macOS and Linux.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install portman
9
+ ```
10
+
11
+ Or install from source:
12
+
13
+ ```bash
14
+ git clone <repo>
15
+ cd portman
16
+ pip install -e .
17
+ ```
18
+
19
+ ## Commands
20
+
21
+ ### `portman list`
22
+
23
+ Show all listening ports with PID, process name, port, protocol, and status. System ports (<1024) are highlighted in yellow, user ports in green.
24
+
25
+ ```bash
26
+ portman list
27
+
28
+ # Only show ports opened from this terminal session
29
+ portman list --mine
30
+
31
+ # Machine-readable JSON output
32
+ portman list --json
33
+ ```
34
+
35
+ > **`--mine` note:** this flag filters to processes running under your OS user account. It removes system services (svchost, lsass, etc.) but will still show other user-owned background apps that happen to listen on ports (e.g. Chrome, cloud sync clients). It correctly catches dev servers started in any shell or terminal window.
36
+
37
+ ### `portman kill <port>`
38
+
39
+ Kill the process listening on a given port. Asks for confirmation unless `--force` is passed.
40
+
41
+ ```bash
42
+ portman kill 8080
43
+
44
+ # Skip confirmation
45
+ portman kill 8080 --force
46
+ portman kill 8080 -f
47
+ ```
48
+
49
+ ### `portman watch`
50
+
51
+ Live-updating view of all active ports, refreshing every 2 seconds. New ports are highlighted green, closed ports red. Exit with `Ctrl+C`.
52
+
53
+ ```bash
54
+ portman watch
55
+
56
+ # Only watch ports opened from this terminal session
57
+ portman watch --mine
58
+ ```
59
+
60
+ ### `portman save <name>`
61
+
62
+ Save a snapshot of all currently active user ports (port ≥1024) to `~/.portman/profiles/<name>.json`.
63
+
64
+ ```bash
65
+ portman save myproject
66
+ portman save dev-stack
67
+ ```
68
+
69
+ ### `portman restore <name>`
70
+
71
+ Check a saved profile against currently running ports. Shows which saved processes are still running and which are missing, along with hints for restarting them.
72
+
73
+ ```bash
74
+ portman restore myproject
75
+ ```
76
+
77
+ ### `portman profiles`
78
+
79
+ List all saved profiles with their creation timestamp and port count.
80
+
81
+ ```bash
82
+ portman profiles
83
+ ```
84
+
85
+ ## Requirements
86
+
87
+ - Python 3.10+
88
+ - macOS or Linux (Windows is not supported)
89
+
90
+ ## Notes
91
+
92
+ - Ports below 1024 are system ports and may require `sudo` to kill.
93
+ - `portman restore` does not restart processes — it only reports their status and suggests what needs to be restarted.
94
+ - Profiles are stored in `~/.portman/profiles/` as JSON files.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,53 @@
1
+ import typer
2
+ import psutil
3
+ from rich.console import Console
4
+ from portman.utils import check_platform, get_listening_ports
5
+
6
+ console = Console()
7
+
8
+
9
+ def command(
10
+ port: int = typer.Argument(..., help="Port number of the process to kill"),
11
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt"),
12
+ ) -> None:
13
+ """Kill the process listening on the given port."""
14
+ check_platform()
15
+ ports = get_listening_ports()
16
+ matches = [p for p in ports if p.port == port]
17
+
18
+ if not matches:
19
+ console.print(f"[red]No process found listening on port {port}.[/red]")
20
+ raise typer.Exit(1)
21
+
22
+ target = matches[0]
23
+
24
+ if not target.pid:
25
+ console.print(
26
+ f"[red]Cannot kill process on port {port}: PID unknown. "
27
+ "Try running with sudo.[/red]"
28
+ )
29
+ raise typer.Exit(1)
30
+
31
+ if not force:
32
+ confirmed = typer.confirm(
33
+ f"Kill {target.name} (PID {target.pid}) on port {port}?"
34
+ )
35
+ if not confirmed:
36
+ console.print("[yellow]Aborted.[/yellow]")
37
+ raise typer.Exit()
38
+
39
+ try:
40
+ proc = psutil.Process(target.pid)
41
+ proc.terminate()
42
+ console.print(
43
+ f"[green]Killed[/green] [bold]{target.name}[/bold] "
44
+ f"(PID {target.pid}) on port [bold]{port}[/bold]."
45
+ )
46
+ except psutil.NoSuchProcess:
47
+ console.print(f"[yellow]Process {target.pid} was already gone.[/yellow]")
48
+ except psutil.AccessDenied:
49
+ console.print(
50
+ f"[red]Permission denied killing PID {target.pid}. "
51
+ "Try running with sudo.[/red]"
52
+ )
53
+ raise typer.Exit(1)
@@ -0,0 +1,67 @@
1
+ import json
2
+ import typer
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+ from portman.utils import check_platform, get_listening_ports, get_user_pids
6
+
7
+ console = Console()
8
+
9
+
10
+ def command(
11
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
12
+ mine: bool = typer.Option(False, "--mine", help="Only show ports opened from this terminal session"),
13
+ ) -> None:
14
+ """Show all listening ports with PID, process name, port, protocol, and status."""
15
+ check_platform()
16
+ ports = get_listening_ports()
17
+
18
+ if mine:
19
+ user_pids = get_user_pids()
20
+ ports = [p for p in ports if p.pid in user_pids]
21
+
22
+ if json_output:
23
+ data = [
24
+ {
25
+ "pid": p.pid,
26
+ "name": p.name,
27
+ "port": p.port,
28
+ "protocol": p.protocol,
29
+ "status": p.status,
30
+ }
31
+ for p in ports
32
+ ]
33
+ typer.echo(json.dumps(data, indent=2))
34
+ return
35
+
36
+ if not ports:
37
+ console.print("[yellow]No listening ports found.[/yellow]")
38
+ return
39
+
40
+ table = Table(
41
+ title="Active Listening Ports",
42
+ show_header=True,
43
+ header_style="bold cyan",
44
+ )
45
+ table.add_column("Port", justify="right", style="bold")
46
+ table.add_column("Proto")
47
+ table.add_column("PID", justify="right")
48
+ table.add_column("Process")
49
+ table.add_column("Status")
50
+ table.add_column("Type")
51
+
52
+ for p in ports:
53
+ if p.is_system():
54
+ type_label = "[yellow]system[/yellow]"
55
+ else:
56
+ type_label = "[green]user[/green]"
57
+
58
+ table.add_row(
59
+ str(p.port),
60
+ p.protocol,
61
+ str(p.pid) if p.pid else "[dim]-[/dim]",
62
+ p.name,
63
+ p.status,
64
+ type_label,
65
+ )
66
+
67
+ console.print(table)
@@ -0,0 +1,35 @@
1
+ import json
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+ from portman.utils import check_platform, get_profiles_dir
5
+
6
+ console = Console()
7
+
8
+
9
+ def command() -> None:
10
+ """List all saved profiles with their timestamp and port count."""
11
+ check_platform()
12
+ profiles_dir = get_profiles_dir()
13
+ files = sorted(profiles_dir.glob("*.json"))
14
+
15
+ if not files:
16
+ console.print("[yellow]No saved profiles. Use 'portman save <name>' to create one.[/yellow]")
17
+ return
18
+
19
+ table = Table(
20
+ title="Saved Profiles",
21
+ show_header=True,
22
+ header_style="bold cyan",
23
+ )
24
+ table.add_column("Name", style="bold")
25
+ table.add_column("Saved At")
26
+ table.add_column("Ports", justify="right")
27
+
28
+ for f in files:
29
+ try:
30
+ data = json.loads(f.read_text())
31
+ table.add_row(data["name"], data["saved_at"], str(len(data["ports"])))
32
+ except (json.JSONDecodeError, KeyError):
33
+ table.add_row(f.stem, "[red]corrupted[/red]", "[dim]-[/dim]")
34
+
35
+ console.print(table)
@@ -0,0 +1,51 @@
1
+ import json
2
+ import typer
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+ from portman.utils import check_platform, get_listening_ports, get_profiles_dir
6
+
7
+ console = Console()
8
+
9
+
10
+ def command(
11
+ name: str = typer.Argument(..., help="Name of the profile to check"),
12
+ ) -> None:
13
+ """Show which saved ports are still running and which are missing."""
14
+ check_platform()
15
+ path = get_profiles_dir() / f"{name}.json"
16
+
17
+ if not path.exists():
18
+ console.print(f"[red]Profile '{name}' not found.[/red]")
19
+ raise typer.Exit(1)
20
+
21
+ profile = json.loads(path.read_text())
22
+ saved_ports: dict[int, dict] = {p["port"]: p for p in profile["ports"]}
23
+ active_ports: set[int] = {p.port for p in get_listening_ports()}
24
+
25
+ table = Table(
26
+ title=f"Profile: {name} [dim](saved {profile['saved_at']})[/dim]",
27
+ show_header=True,
28
+ header_style="bold cyan",
29
+ )
30
+ table.add_column("Port", justify="right")
31
+ table.add_column("Process")
32
+ table.add_column("Status")
33
+
34
+ missing: list[dict] = []
35
+ for port, info in sorted(saved_ports.items()):
36
+ if port in active_ports:
37
+ table.add_row(str(port), info["name"], "[green]running[/green]")
38
+ else:
39
+ table.add_row(str(port), info["name"], "[red]missing[/red]")
40
+ missing.append(info)
41
+
42
+ console.print(table)
43
+
44
+ if missing:
45
+ console.print(
46
+ "\n[bold yellow]The following processes are no longer running:[/bold yellow]"
47
+ )
48
+ for info in missing:
49
+ console.print(
50
+ f" [dim]# Restart '{info['name']}' and have it listen on port {info['port']}[/dim]"
51
+ )
@@ -0,0 +1,38 @@
1
+ import json
2
+ from datetime import datetime
3
+ import typer
4
+ from rich.console import Console
5
+ from portman.utils import check_platform, get_listening_ports, get_profiles_dir
6
+
7
+ console = Console()
8
+
9
+
10
+ def command(
11
+ name: str = typer.Argument(..., help="Name for the profile snapshot"),
12
+ ) -> None:
13
+ """Save a snapshot of currently active user ports (>=1024) to ~/.portman/profiles/<name>.json."""
14
+ check_platform()
15
+ ports = get_listening_ports()
16
+ user_ports = [p for p in ports if not p.is_system()]
17
+
18
+ profile = {
19
+ "name": name,
20
+ "saved_at": datetime.now().isoformat(),
21
+ "ports": [
22
+ {
23
+ "pid": p.pid,
24
+ "name": p.name,
25
+ "port": p.port,
26
+ "protocol": p.protocol,
27
+ "status": p.status,
28
+ }
29
+ for p in user_ports
30
+ ],
31
+ }
32
+
33
+ path = get_profiles_dir() / f"{name}.json"
34
+ path.write_text(json.dumps(profile, indent=2))
35
+ console.print(
36
+ f"[green]Saved[/green] [bold]{len(user_ports)}[/bold] user port(s) "
37
+ f"to profile [bold]'{name}'[/bold] ({path})"
38
+ )
@@ -0,0 +1,81 @@
1
+ import time
2
+ import typer
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+ from rich.live import Live
6
+ from portman.utils import check_platform, get_listening_ports, get_user_pids, PortInfo
7
+
8
+ console = Console()
9
+
10
+
11
+ def _build_table(
12
+ ports: list[PortInfo],
13
+ new_ports: set[int],
14
+ closed_ports: set[int],
15
+ ) -> Table:
16
+ table = Table(
17
+ title="Active Ports [dim](Ctrl+C to exit)[/dim]",
18
+ show_header=True,
19
+ header_style="bold cyan",
20
+ )
21
+ table.add_column("Port", justify="right", style="bold")
22
+ table.add_column("Proto")
23
+ table.add_column("PID", justify="right")
24
+ table.add_column("Process")
25
+ table.add_column("Status")
26
+ table.add_column("Change", justify="center")
27
+
28
+ for p in ports:
29
+ if p.port in new_ports:
30
+ change = "[bold green]NEW[/bold green]"
31
+ port_str = f"[green]{p.port}[/green]"
32
+ else:
33
+ change = ""
34
+ port_str = str(p.port)
35
+
36
+ table.add_row(
37
+ port_str,
38
+ p.protocol,
39
+ str(p.pid) if p.pid else "[dim]-[/dim]",
40
+ p.name,
41
+ p.status,
42
+ change,
43
+ )
44
+
45
+ for port in sorted(closed_ports):
46
+ table.add_row(
47
+ f"[red]{port}[/red]",
48
+ "", "", "", "",
49
+ "[bold red]CLOSED[/bold red]",
50
+ )
51
+
52
+ return table
53
+
54
+
55
+ def command(
56
+ mine: bool = typer.Option(False, "--mine", help="Only show ports opened from this terminal session"),
57
+ ) -> None:
58
+ """Live-updating view of all active ports. Refreshes every 2 seconds."""
59
+ check_platform()
60
+
61
+ def _filtered_ports():
62
+ ports = get_listening_ports()
63
+ if mine:
64
+ user_pids = get_user_pids()
65
+ ports = [p for p in ports if p.pid in user_pids]
66
+ return ports
67
+
68
+ prev_ports: set[int] = {p.port for p in _filtered_ports()}
69
+
70
+ try:
71
+ with Live(console=console, refresh_per_second=2) as live:
72
+ while True:
73
+ ports = _filtered_ports()
74
+ current = {p.port for p in ports}
75
+ new_ports = current - prev_ports
76
+ closed_ports = prev_ports - current
77
+ live.update(_build_table(ports, new_ports, closed_ports))
78
+ prev_ports = current
79
+ time.sleep(2)
80
+ except KeyboardInterrupt:
81
+ console.print("\n[yellow]Stopped watching.[/yellow]")
@@ -0,0 +1,18 @@
1
+ import typer
2
+ from portman.commands import list_ports, kill, watch, save, restore, profiles
3
+
4
+ app = typer.Typer(
5
+ name="portman",
6
+ help="portman — local port management CLI",
7
+ no_args_is_help=True,
8
+ )
9
+
10
+ app.command("list")(list_ports.command)
11
+ app.command("kill")(kill.command)
12
+ app.command("watch")(watch.command)
13
+ app.command("save")(save.command)
14
+ app.command("restore")(restore.command)
15
+ app.command("profiles")(profiles.command)
16
+
17
+ if __name__ == "__main__":
18
+ app()
@@ -0,0 +1,88 @@
1
+ import os
2
+ import sys
3
+ import socket
4
+ import psutil
5
+ from dataclasses import dataclass
6
+
7
+
8
+ def check_platform() -> None:
9
+ pass
10
+
11
+
12
+ @dataclass
13
+ class PortInfo:
14
+ pid: int
15
+ name: str
16
+ port: int
17
+ protocol: str
18
+ status: str
19
+
20
+ def is_system(self) -> bool:
21
+ return self.port < 1024
22
+
23
+
24
+ def _get_process_name(pid: int) -> str:
25
+ if not pid:
26
+ return "unknown"
27
+ try:
28
+ return psutil.Process(pid).name()
29
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
30
+ return "unknown"
31
+
32
+
33
+ def get_listening_ports() -> list[PortInfo]:
34
+ seen: dict[tuple[int, str], PortInfo] = {}
35
+ try:
36
+ for conn in psutil.net_connections(kind="inet"):
37
+ if not conn.laddr:
38
+ continue
39
+ if conn.type == socket.SOCK_STREAM and conn.status != "LISTEN":
40
+ continue
41
+
42
+ port = conn.laddr.port
43
+ protocol = "TCP" if conn.type == socket.SOCK_STREAM else "UDP"
44
+ key = (port, protocol)
45
+ if key in seen:
46
+ continue
47
+
48
+ status = conn.status if conn.type == socket.SOCK_STREAM else "N/A"
49
+ pid = conn.pid or 0
50
+ seen[key] = PortInfo(
51
+ pid=pid,
52
+ name=_get_process_name(pid),
53
+ port=port,
54
+ protocol=protocol,
55
+ status=status,
56
+ )
57
+ except psutil.AccessDenied:
58
+ pass
59
+ return sorted(seen.values(), key=lambda x: x.port)
60
+
61
+
62
+ def get_current_username() -> str | None:
63
+ try:
64
+ return psutil.Process(os.getpid()).username()
65
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
66
+ return None
67
+
68
+
69
+ def get_user_pids() -> set[int]:
70
+ """Return PIDs of all processes running as the current user."""
71
+ username = get_current_username()
72
+ if not username:
73
+ return set()
74
+ pids: set[int] = set()
75
+ for proc in psutil.process_iter(["pid", "username"]):
76
+ try:
77
+ if proc.info["username"] == username:
78
+ pids.add(proc.info["pid"])
79
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
80
+ pass
81
+ return pids
82
+
83
+
84
+ def get_profiles_dir():
85
+ from pathlib import Path
86
+ d = Path.home() / ".portman" / "profiles"
87
+ d.mkdir(parents=True, exist_ok=True)
88
+ return d
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "portsmith"
7
+ version = "0.1.0"
8
+ description = "Local port management CLI — list, kill, watch, and snapshot listening ports"
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ authors = [{ name = "Atharva Kokane", email = "atharvashashankkokane@gmail.com" }]
12
+ requires-python = ">=3.10"
13
+ keywords = ["port", "cli", "network", "process", "devtools"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: System :: Networking",
25
+ "Topic :: Utilities",
26
+ ]
27
+ dependencies = [
28
+ "typer>=0.9.0",
29
+ "rich>=13.0.0",
30
+ "psutil>=5.9.0",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/atharvashashankk/portman"
35
+ Issues = "https://github.com/atharvashashankk/portman/issues"
36
+
37
+ [project.scripts]
38
+ portman = "portman.main:app"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["portman"]