devenv-inspector 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 CHANGED
@@ -3,10 +3,10 @@
3
3
 
4
4
  <h1>DevEnv Inspector</h1>
5
5
 
6
- <p>A unified desktop GUI for inspecting and managing your development runtimes and global packages — no terminal required.</p>
6
+ <p>A unified desktop GUI for inspecting runtimes, managing global packages, and monitoring active local ports — no terminal required.</p>
7
7
 
8
8
  <p>
9
- <img src="https://img.shields.io/badge/version-0.1.0-5a7af5?style=for-the-badge" alt="Version" />
9
+ <img src="https://img.shields.io/badge/version-0.3.0-5a7af5?style=for-the-badge" alt="Version" />
10
10
  <img src="https://img.shields.io/badge/platform-macOS-lightgrey?style=for-the-badge&logo=apple" alt="Platform" />
11
11
  <img src="https://img.shields.io/badge/license-MIT-44c98b?style=for-the-badge" alt="License" />
12
12
  <img src="https://img.shields.io/badge/open%20source-%E2%9D%A4-e05454?style=for-the-badge" alt="Open Source" />
@@ -32,7 +32,7 @@
32
32
 
33
33
  ## What is this?
34
34
 
35
- Developers who work across Python, Node.js, and Conda constantly switch between terminal commands just to see what's installed. DevEnv Inspector puts it all in one window version badges, a searchable package list, and one-click uninstallation.
35
+ Developers who work across Python, Node.js, and Conda constantly switch between terminal commands just to see what's installed — and what's running. DevEnv Inspector puts it all in one window: version badges, a searchable package list, one-click uninstallation, and a live view of every port in use on your machine.
36
36
 
37
37
  <div align="center">
38
38
  <img src="media/screenshot.png" alt="DevEnv Inspector Screenshot" width="780" />
@@ -42,11 +42,14 @@ Developers who work across Python, Node.js, and Conda constantly switch between
42
42
 
43
43
  ## Features
44
44
 
45
- - **Runtime detection** — instantly shows installed versions of Python, Conda, and Node.js
46
- - **Unified package table** — all pip, conda, and npm global packages in one searchable list
45
+ - **Runtime detection** — instantly shows installed versions of Python, Conda, Node.js, npm, Yarn, and pnpm
46
+ - **Unified package table** — all pip, conda, npm, yarn, and pnpm global packages in one searchable list
47
47
  - **Safe uninstallation** — confirmation dialog before any package is removed
48
- - **Filter by manager** — quickly scope the list to pip / conda / npm
49
- - **Graceful fallbacks** — missing runtimes show "Not Installed" and hide irrelevant packages
48
+ - **Filter by manager** — dynamically shows only the managers that are installed on your machine
49
+ - **Active Ports tab** — see every TCP/UDP port in use: port number, protocol, PID, and process name
50
+ - **Kill process** — terminate any port's process with a single click and a confirmation dialog
51
+ - **Graceful fallbacks** — missing runtimes show "Not Installed" and hide irrelevant filter tabs
52
+ - **Plugin system** — add support for new package managers in a single file, zero changes to core code
50
53
  - **No internet required** — everything runs locally against your machine
51
54
 
52
55
  ---
@@ -63,8 +66,10 @@ npm install -g devenv-inspector-cli
63
66
  devenv list # show all runtimes
64
67
  devenv packages # list all global packages
65
68
  devenv packages --runtime pip # filter by manager
69
+ devenv packages --runtime pnpm # pnpm only
66
70
  devenv uninstall numpy --runtime conda
67
- devenv info python
71
+ devenv info pnpm
72
+ devenv ports # show all active local ports
68
73
  ```
69
74
 
70
75
  > Source lives in [`cli/`](./cli) — same repo, zero Electron. Docker support included.
@@ -126,52 +131,135 @@ npm run package
126
131
  ```
127
132
 
128
133
  This builds the source and produces a `.dmg` installer in `dist/`:
129
- - `DevEnv Inspector-0.1.0-arm64.dmg` — Apple Silicon
130
- - `DevEnv Inspector-0.1.0.dmg` — Intel
134
+ - `DevEnv Inspector-0.3.0-arm64.dmg` — Apple Silicon
135
+ - `DevEnv Inspector-0.3.0.dmg` — Intel
131
136
 
132
137
  ---
133
138
 
134
139
  ## How It Works
135
140
 
136
141
  ```
137
- ┌─────────────────────────────────────────────┐
138
- Renderer Process
139
- │ React UI — table, filters, dialogs
140
- └──────────────────┬──────────────────────────┘
141
- │ IPC (contextBridge)
142
- ┌──────────────────▼──────────────────────────┐
143
- │ Main Process │
144
- detectors.js → python3 / conda / node
145
- parsers.js pip / conda / npm --json
146
- ipcHandlers.js uninstall routing
147
- shell.js login shell executor
148
- └─────────────────────────────────────────────┘
142
+ ┌─────────────────────────────────────────────────────────┐
143
+ Renderer Process
144
+ │ React UI — runtime cards, Packages tab, Ports tab
145
+ │ Tabs derived dynamically from registry metadata │
146
+ └──────────────────────┬──────────────────────────────────┘
147
+ │ IPC (contextBridge)
148
+ ┌──────────────────────▼──────────────────────────────────┐
149
+ Main Process
150
+ runtimes/builtins.js registerRuntime() × 6
151
+ registry.js getRegisteredRuntimes()
152
+ detectors.js registry loop → versions
153
+ │ parsers.js → registry loop → packages │
154
+ │ ports.js → lsof → TCP/UDP port list │
155
+ │ ipcHandlers.js → uninstall + kill-port routing │
156
+ │ shell.js → login shell executor │
157
+ └─────────────────────────────────────────────────────────┘
149
158
  ```
150
159
 
151
160
  Every command runs through the user's login shell (`zsh -i -l -c`) so that conda, pyenv, nvm, and other shell-managed tools are always found — both in dev mode and in the packaged `.app`.
152
161
 
153
162
  ---
154
163
 
155
- ## Supported Runtimes (v0.1.0)
164
+ ## Supported Runtimes (v0.3.0)
156
165
 
157
- | Runtime | Packages | Uninstall |
166
+ | Runtime | Detect | Packages | Uninstall |
167
+ |---|---|---|---|
168
+ | Python | `python3 --version` | `python3 -m pip list --format=json` | `python3 -m pip uninstall -y` |
169
+ | Anaconda | `conda --version` | `conda list --json` | `conda remove -y` |
170
+ | Node.js | `node --version` | — | — |
171
+ | npm | `npm --version` | `npm list -g --depth=0 --json` | `npm uninstall -g` |
172
+ | Yarn | `yarn --version` | `yarn global list --json` | `yarn global remove` |
173
+ | pnpm | `pnpm --version` | `pnpm list -g --json` | `pnpm remove -g` |
174
+
175
+ ---
176
+
177
+ ## Active Local Ports
178
+
179
+ The **Active Ports** tab gives you an instant overview of every listening port on your machine — the same information as `lsof`, presented without the terminal.
180
+
181
+ | Column | Description |
182
+ |---|---|
183
+ | **Port** | The open port number |
184
+ | **Protocol** | TCP or UDP |
185
+ | **PID** | Process ID using the port |
186
+ | **Process** | Name of the program / service |
187
+
188
+ ### In the GUI
189
+
190
+ - Switch to the **Active Ports** tab next to Packages — a live count badge shows how many ports are open
191
+ - Search by port number, process name, or PID
192
+ - Filter by **TCP** / **UDP**
193
+ - Click **Kill** on any row → confirmation dialog → sends `SIGTERM` to that process
194
+
195
+ ### How ports are detected
196
+
197
+ `lsof -F pcn` is used with structured field output — no text parsing of headers, injection-safe. TCP ports are filtered to `LISTEN` state only. IPv4/IPv6 duplicates are deduplicated automatically.
198
+
199
+ ---
200
+
201
+ ## Plugin System
202
+
203
+ v0.3.0 ships a runtime registry that makes adding new package managers trivial. Each built-in (pip, conda, npm, yarn, pnpm) is now a self-describing plugin object registered via `registerRuntime()`. The core detectors, parsers, IPC handlers, and renderer UI are all driven by the registry — they contain zero hardcoded manager names.
204
+
205
+ ### Adding a community runtime (e.g. Bun)
206
+
207
+ Create one file — `src/main/runtimes/bun.js`:
208
+
209
+ ```js
210
+ import { registerRuntime } from '../registry.js'
211
+ import { runInShell } from '../shell.js'
212
+
213
+ registerRuntime({
214
+ name: 'bun',
215
+ label: 'Bun',
216
+ color: '#fbf0df',
217
+ detect: () => runInShell('bun', ['--version'], { timeout: 10000 }).catch(() => null),
218
+ parseVersion: (o) => o.trim().replace(/^bun\s+v?/i, ''),
219
+ list: async () => { /* parse: bun pm ls --global */ },
220
+ uninstall: (pkg) => ['bun', ['remove', '-g', pkg]]
221
+ })
222
+ ```
223
+
224
+ Then add **one import line** in `src/main/index.js`:
225
+
226
+ ```js
227
+ import './runtimes/bun.js'
228
+ ```
229
+
230
+ That's it. The runtime card, filter tab, package listing, and uninstall button all appear automatically — **zero changes to core code**.
231
+
232
+ ### Plugin shape reference
233
+
234
+ | Field | Type | Description |
158
235
  |---|---|---|
159
- | Python | `python3 -m pip list` | `python3 -m pip uninstall -y` |
160
- | Anaconda | `conda list --json` | `conda remove -y` |
161
- | Node.js | `npm list -g --depth=0` | `npm uninstall -g` |
162
- | Yarn | `yarn global list --json` | `yarn global remove` |
163
- | pnpm | `pnpm list -g --json` | `pnpm remove -g` |
236
+ | `name` | `string` | Unique key (also used as `manager` in package rows) |
237
+ | `label` | `string` | Display name in UI and CLI |
238
+ | `color` | `string` | Hex color for the manager badge |
239
+ | `detect` | `async () => string \| null` | Returns raw stdout, or `null` if not installed |
240
+ | `parseVersion` | `(output) => string` | Cleans `detect()` output to a version string |
241
+ | `list` | `async () => [{name, version}] \| null` | `null` = no packages for this runtime |
242
+ | `uninstall` | `(pkg) => [cmd, args] \| null` | `null` = uninstall not supported |
164
243
 
165
244
  ---
166
245
 
167
246
  ## Roadmap
168
247
 
169
- - [ ] nvm / pyenv support
248
+ ### Shipped
249
+ - [x] Python, Conda, Node.js, npm, Yarn, pnpm support
250
+ - [x] Searchable + filterable global package table
251
+ - [x] One-click uninstall with confirmation
252
+ - [x] CLI companion (`devenv-inspector-cli` on npm)
253
+ - [x] Docker support for the CLI
254
+ - [x] **Plugin system** — add any runtime in a single file
255
+ - [x] **Active Local Ports** — live TCP/UDP port viewer with process names and kill support
256
+
257
+ ### Upcoming
258
+ - [ ] nvm / pyenv version manager support
170
259
  - [ ] Virtual environment detection
171
- - [ ] Package update detection
260
+ - [ ] Package update detection (`outdated` view)
172
261
  - [ ] Dependency graph visualization
173
262
  - [ ] Dark / light mode toggle
174
- - [ ] Plugin system for new package managers
175
263
  - [ ] Windows & Linux support
176
264
 
177
265
  ---
package/cli/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  <div align="center">
2
2
  <h1>devenv-inspector-cli</h1>
3
- <p>Inspect runtimes and manage global packages from your terminal — no GUI required.</p>
3
+ <p>Inspect runtimes, manage global packages, and monitor active local ports from your terminal — no GUI required.</p>
4
4
 
5
5
  <p>
6
- <img src="https://img.shields.io/badge/version-0.1.0-5a7af5?style=for-the-badge" alt="Version" />
6
+ <img src="https://img.shields.io/badge/version-0.3.0-5a7af5?style=for-the-badge" alt="Version" />
7
7
  <img src="https://img.shields.io/badge/platform-macOS-lightgrey?style=for-the-badge&logo=apple" alt="Platform" />
8
8
  <img src="https://img.shields.io/badge/license-MIT-44c98b?style=for-the-badge" alt="License" />
9
9
  <img src="https://img.shields.io/npm/v/devenv-inspector-cli?style=for-the-badge&logo=npm&color=cc3534" alt="npm" />
@@ -31,6 +31,7 @@ No Node.js or Python required — just Docker:
31
31
  docker run --rm ghcr.io/ali-aldahmani/devenv-inspector-cli list
32
32
  docker run --rm ghcr.io/ali-aldahmani/devenv-inspector-cli packages
33
33
  docker run --rm ghcr.io/ali-aldahmani/devenv-inspector-cli info python
34
+ docker run --rm ghcr.io/ali-aldahmani/devenv-inspector-cli ports
34
35
 
35
36
  # Or build locally from source
36
37
  git clone https://github.com/Ali-Aldahmani/devenv-inspector.git
@@ -57,6 +58,8 @@ $ devenv list
57
58
  Conda 26.1.0
58
59
  Node.js 24.13.0
59
60
  npm 11.6.2
61
+ Yarn 1.22.22
62
+ pnpm 9.15.4
60
63
  ```
61
64
 
62
65
  ---
@@ -69,6 +72,8 @@ devenv packages # all packages
69
72
  devenv packages --runtime pip # pip only
70
73
  devenv packages --runtime conda # conda only
71
74
  devenv packages --runtime npm # npm only
75
+ devenv packages --runtime yarn # yarn only
76
+ devenv packages --runtime pnpm # pnpm only
72
77
  ```
73
78
 
74
79
  ```
@@ -93,6 +98,8 @@ Uninstall a package with a confirmation prompt.
93
98
  devenv uninstall requests --runtime pip
94
99
  devenv uninstall numpy --runtime conda
95
100
  devenv uninstall electron --runtime npm
101
+ devenv uninstall create-react-app --runtime yarn
102
+ devenv uninstall typescript --runtime pnpm
96
103
  ```
97
104
 
98
105
  ```
@@ -112,6 +119,8 @@ devenv info python
112
119
  devenv info conda
113
120
  devenv info node
114
121
  devenv info npm
122
+ devenv info yarn
123
+ devenv info pnpm
115
124
  ```
116
125
 
117
126
  ```
@@ -126,12 +135,102 @@ $ devenv info python
126
135
 
127
136
  ---
128
137
 
138
+ ### `devenv ports`
139
+ Show all active local listening ports (TCP + UDP) with their process details.
140
+
141
+ ```
142
+ $ devenv ports
143
+
144
+ DevEnv Inspector — Active Local Ports
145
+
146
+ ┌────────┬──────────┬───────┬────────────┐
147
+ │ Port │ Protocol │ PID │ Process │
148
+ ├────────┼──────────┼───────┼────────────┤
149
+ │ 3000 │ TCP │ 1234 │ node │
150
+ │ 5432 │ TCP │ 2345 │ postgres │
151
+ │ 8000 │ TCP │ 3456 │ python3 │
152
+ │ 8080 │ TCP │ 4567 │ node │
153
+ └────────┴──────────┴───────┴────────────┘
154
+
155
+ 4 active ports
156
+ ```
157
+
158
+ TCP ports are colored **blue**, UDP ports **orange**. Only `LISTEN` state TCP sockets and bound UDP sockets are shown — established connections are excluded.
159
+
160
+ ---
161
+
129
162
  ## How It Works
130
163
 
131
164
  All commands run through your login shell (`zsh -i -l -c`) — the same approach as the GUI app — so conda, pyenv, nvm, and other shell-managed tools are always found.
132
165
 
133
166
  Package names are validated against `/^[a-zA-Z0-9._\-@/]+$/` before any shell call to prevent injection.
134
167
 
168
+ Port detection uses `lsof -F pcn` with structured field output — no fragile header parsing. IPv4/IPv6 duplicates are deduplicated automatically.
169
+
170
+ Runtimes are powered by a **plugin registry** — each manager is a self-describing object registered via `registerRuntime()`. The CLI commands contain zero hardcoded manager names; everything is derived from the registry at runtime.
171
+
172
+ ---
173
+
174
+ ## Plugin System
175
+
176
+ Adding support for a new package manager requires **one file** and **one import line**.
177
+
178
+ Create `cli/src/runtimes/bun.js`:
179
+
180
+ ```js
181
+ import { registerRuntime } from '../registry.js'
182
+ import { runInShell } from '../shell.js'
183
+
184
+ registerRuntime({
185
+ name: 'bun',
186
+ label: 'Bun',
187
+ color: '#fbf0df',
188
+ detect: () => runInShell('bun', ['--version'], { timeout: 10000 }).catch(() => null),
189
+ parseVersion: (o) => o.trim().replace(/^bun\s+v?/i, ''),
190
+ list: async () => { /* parse: bun pm ls --global */ },
191
+ uninstall: (pkg) => ['bun', ['remove', '-g', pkg]]
192
+ })
193
+ ```
194
+
195
+ Then add one line in `cli/bin/devenv.js`:
196
+
197
+ ```js
198
+ import '../src/runtimes/bun.js'
199
+ ```
200
+
201
+ Bun immediately appears in `devenv list`, `devenv packages --runtime bun`, `devenv uninstall`, and `devenv info bun` — **zero changes to CLI command files**.
202
+
203
+ ### Plugin shape reference
204
+
205
+ | Field | Type | Description |
206
+ |---|---|---|
207
+ | `name` | `string` | Unique key used as `--runtime` value |
208
+ | `label` | `string` | Display name in output |
209
+ | `color` | `string` | Hex color for colored output |
210
+ | `detect` | `async () => string \| null` | Returns raw stdout, or `null` if not installed |
211
+ | `parseVersion` | `(output) => string` | Cleans `detect()` output to a version string |
212
+ | `list` | `async () => [{name, version}] \| null` | `null` = no packages for this runtime |
213
+ | `uninstall` | `(pkg) => [cmd, args] \| null` | `null` = uninstall not supported |
214
+
215
+ ---
216
+
217
+ ## Roadmap
218
+
219
+ ### Shipped
220
+ - [x] Python, Conda, Node.js, npm, Yarn, pnpm support
221
+ - [x] Colored, tabular package output with `cli-table3`
222
+ - [x] Confirmation prompt before uninstall
223
+ - [x] `devenv info` command for per-runtime details
224
+ - [x] Docker support
225
+ - [x] **Plugin system** — add any runtime in a single file
226
+ - [x] **`devenv ports`** — active TCP/UDP port viewer with process names
227
+
228
+ ### Upcoming
229
+ - [ ] nvm / pyenv version manager support
230
+ - [ ] `devenv packages --outdated` — highlight packages with available updates
231
+ - [ ] JSON output flag (`--json`) for scripting
232
+ - [ ] Windows & Linux support
233
+
135
234
  ---
136
235
 
137
236
  ## License
package/cli/bin/devenv.js CHANGED
@@ -1,16 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ import '../src/runtimes/builtins.js'
2
3
  import { Command } from 'commander'
3
4
  import { listCommand } from '../src/commands/list.js'
4
5
  import { packagesCommand } from '../src/commands/packages.js'
5
6
  import { uninstallCommand } from '../src/commands/uninstall.js'
6
7
  import { infoCommand } from '../src/commands/info.js'
8
+ import { portsCommand } from '../src/commands/ports.js'
7
9
 
8
10
  const program = new Command()
9
11
 
10
12
  program
11
13
  .name('devenv')
12
14
  .description('Inspect and manage your development runtimes and global packages')
13
- .version('0.1.0')
15
+ .version('0.3.0')
14
16
 
15
17
  program
16
18
  .command('list')
@@ -34,4 +36,9 @@ program
34
36
  .description('Show detailed info for a runtime (python, conda, node, npm)')
35
37
  .action(infoCommand)
36
38
 
39
+ program
40
+ .command('ports')
41
+ .description('Show all active local listening ports and their processes')
42
+ .action(portsCommand)
43
+
37
44
  await program.parseAsync(process.argv)
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devenv-inspector-cli",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "CLI companion to DevEnv Inspector — inspect runtimes and manage global packages from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,51 +1,44 @@
1
1
  import chalk from 'chalk'
2
2
  import { detectRuntimes } from '../detectors.js'
3
3
  import { getAllPackages } from '../parsers.js'
4
-
5
- const RUNTIME_META = {
6
- python: { label: 'Python', manager: 'pip', packageLabel: 'pip packages' },
7
- conda: { label: 'Conda', manager: 'conda', packageLabel: 'conda packages' },
8
- node: { label: 'Node.js', manager: null, packageLabel: null },
9
- npm: { label: 'npm', manager: 'npm', packageLabel: 'global npm packages' },
10
- yarn: { label: 'Yarn', manager: 'yarn', packageLabel: 'global yarn packages' },
11
- pnpm: { label: 'pnpm', manager: 'pnpm', packageLabel: 'global pnpm packages' }
12
- }
4
+ import { getRegisteredRuntimes, getRuntime } from '../registry.js'
13
5
 
14
6
  export async function infoCommand(runtime) {
15
7
  const key = runtime.toLowerCase()
16
- const meta = RUNTIME_META[key]
8
+ const rt = getRuntime(key)
17
9
 
18
- if (!meta) {
19
- console.error(chalk.red(`\n Unknown runtime "${runtime}". Valid values: python, conda, node, npm, yarn, pnpm\n`))
10
+ if (!rt) {
11
+ const valid = getRegisteredRuntimes().map((r) => r.name).join(', ')
12
+ console.error(chalk.red(`\n Unknown runtime "${runtime}". Valid values: ${valid}\n`))
20
13
  process.exit(1)
21
14
  }
22
15
 
23
- if (process.stdout.isTTY) process.stdout.write(` Loading info for ${meta.label}...`)
16
+ if (process.stdout.isTTY) process.stdout.write(` Loading info for ${rt.label}...`)
24
17
 
25
18
  const runtimes = await detectRuntimes()
26
19
 
27
20
  if (process.stdout.isTTY) process.stdout.write('\r' + ' '.repeat(40) + '\r')
28
21
 
29
- const rt = runtimes[key]
22
+ const info = runtimes[key]
30
23
 
31
- console.log(chalk.bold(`\n ${meta.label}\n`))
32
- console.log(` Status ${rt?.installed ? chalk.green('Installed') : chalk.dim('Not Installed')}`)
24
+ console.log(chalk.bold(`\n ${rt.label}\n`))
25
+ console.log(` Status ${info?.installed ? chalk.green('Installed') : chalk.dim('Not Installed')}`)
33
26
 
34
- if (!rt?.installed) {
27
+ if (!info?.installed) {
35
28
  console.log()
36
29
  return
37
30
  }
38
31
 
39
- console.log(` Version ${chalk.cyan(rt.version)}`)
32
+ console.log(` Version ${chalk.cyan(info.version)}`)
40
33
 
41
- // Show package count if this runtime has packages
42
- if (meta.manager) {
34
+ // Show package count if this runtime supports package listing
35
+ if (rt.list) {
43
36
  if (process.stdout.isTTY) process.stdout.write(' Counting packages...')
44
37
  const allPkgs = await getAllPackages(runtimes)
45
38
  if (process.stdout.isTTY) process.stdout.write('\r' + ' '.repeat(30) + '\r')
46
39
 
47
- const count = allPkgs.filter((p) => p.manager === meta.manager).length
48
- console.log(` Packages ${chalk.yellow(count)} ${meta.packageLabel}`)
40
+ const count = allPkgs.filter((p) => p.manager === key).length
41
+ console.log(` Packages ${chalk.yellow(count)} global ${rt.label} packages`)
49
42
  }
50
43
 
51
44
  console.log()
@@ -1,13 +1,6 @@
1
1
  import chalk from 'chalk'
2
2
  import { detectRuntimes } from '../detectors.js'
3
3
 
4
- const RUNTIMES = [
5
- { key: 'python', label: 'Python' },
6
- { key: 'conda', label: 'Conda' },
7
- { key: 'node', label: 'Node.js' },
8
- { key: 'npm', label: 'npm' }
9
- ]
10
-
11
4
  export async function listCommand() {
12
5
  console.log(chalk.bold('\n DevEnv Inspector — Runtimes\n'))
13
6
 
@@ -18,11 +11,10 @@ export async function listCommand() {
18
11
 
19
12
  if (spinner) process.stdout.write('\r' + ' '.repeat(30) + '\r')
20
13
 
21
- for (const { key, label } of RUNTIMES) {
22
- const rt = runtimes[key]
23
- const name = chalk.white.bold(label.padEnd(10))
24
- if (rt?.installed) {
25
- console.log(` ${name} ${chalk.green(rt.version)}`)
14
+ for (const [, info] of Object.entries(runtimes)) {
15
+ const name = chalk.white.bold(info.label.padEnd(10))
16
+ if (info.installed) {
17
+ console.log(` ${name} ${chalk.green(info.version)}`)
26
18
  } else {
27
19
  console.log(` ${name} ${chalk.dim('Not Installed')}`)
28
20
  }
@@ -2,22 +2,15 @@ import chalk from 'chalk'
2
2
  import Table from 'cli-table3'
3
3
  import { detectRuntimes } from '../detectors.js'
4
4
  import { getAllPackages } from '../parsers.js'
5
-
6
- const VALID_RUNTIMES = ['pip', 'conda', 'npm', 'yarn', 'pnpm']
7
-
8
- const MANAGER_COLORS = {
9
- pip: (s) => chalk.hex('#5a7af5')(s),
10
- conda: (s) => chalk.hex('#44c98b')(s),
11
- npm: (s) => chalk.hex('#e05454')(s),
12
- yarn: (s) => chalk.hex('#2c8ebb')(s),
13
- pnpm: (s) => chalk.hex('#f69220')(s)
14
- }
5
+ import { getRegisteredRuntimes, getRuntime } from '../registry.js'
15
6
 
16
7
  export async function packagesCommand(options) {
17
8
  const filter = options.runtime?.toLowerCase()
18
9
 
19
- if (filter && !VALID_RUNTIMES.includes(filter)) {
20
- console.error(chalk.red(`\n Unknown runtime "${filter}". Valid values: pip, conda, npm, yarn, pnpm\n`))
10
+ const validRuntimes = getRegisteredRuntimes().filter((rt) => rt.list).map((rt) => rt.name)
11
+
12
+ if (filter && !validRuntimes.includes(filter)) {
13
+ console.error(chalk.red(`\n Unknown runtime "${filter}". Valid values: ${validRuntimes.join(', ')}\n`))
21
14
  process.exit(1)
22
15
  }
23
16
 
@@ -54,7 +47,8 @@ export async function packagesCommand(options) {
54
47
  })
55
48
 
56
49
  for (const pkg of packages) {
57
- const colorFn = MANAGER_COLORS[pkg.manager] || ((s) => s)
50
+ const color = getRuntime(pkg.manager)?.color ?? '#ffffff'
51
+ const colorFn = (s) => chalk.hex(color)(s)
58
52
  table.push([
59
53
  pkg.name,
60
54
  chalk.dim(pkg.version),
@@ -0,0 +1,47 @@
1
+ import chalk from 'chalk'
2
+ import Table from 'cli-table3'
3
+ import { getActivePorts } from '../ports.js'
4
+
5
+ export async function portsCommand() {
6
+ console.log(chalk.bold('\n DevEnv Inspector — Active Local Ports\n'))
7
+
8
+ const spinner = process.stdout.isTTY
9
+ if (spinner) process.stdout.write(' Scanning ports...')
10
+
11
+ const ports = await getActivePorts()
12
+
13
+ if (spinner) process.stdout.write('\r' + ' '.repeat(30) + '\r')
14
+
15
+ if (ports.length === 0) {
16
+ console.log(chalk.dim(' No active listening ports found.\n'))
17
+ return
18
+ }
19
+
20
+ const table = new Table({
21
+ head: [
22
+ chalk.white.bold('Port'),
23
+ chalk.white.bold('Protocol'),
24
+ chalk.white.bold('PID'),
25
+ chalk.white.bold('Process')
26
+ ],
27
+ style: { head: [], border: [] },
28
+ colWidths: [10, 12, 10, 30]
29
+ })
30
+
31
+ for (const p of ports) {
32
+ const proto =
33
+ p.protocol === 'TCP'
34
+ ? chalk.hex('#4b9eff')(p.protocol)
35
+ : chalk.hex('#f69220')(p.protocol)
36
+
37
+ table.push([
38
+ chalk.white.bold(String(p.port)),
39
+ proto,
40
+ chalk.dim(String(p.pid)),
41
+ chalk.white(p.process)
42
+ ])
43
+ }
44
+
45
+ console.log(table.toString())
46
+ console.log(chalk.dim(`\n ${ports.length} active port${ports.length !== 1 ? 's' : ''}\n`))
47
+ }
@@ -1,24 +1,16 @@
1
1
  import chalk from 'chalk'
2
- import { createRequire } from 'module'
3
2
  import { runInShell } from '../shell.js'
3
+ import { getRegisteredRuntimes, getRuntime } from '../registry.js'
4
4
 
5
5
  const PACKAGE_NAME_RE = /^[a-zA-Z0-9._\-@/]+$/
6
6
 
7
- const VALID_MANAGERS = ['pip', 'conda', 'npm', 'yarn', 'pnpm']
8
-
9
- const UNINSTALL_COMMANDS = {
10
- pip: ['python3', ['-m', 'pip', 'uninstall', '-y']],
11
- conda: ['conda', ['remove', '-y']],
12
- npm: ['npm', ['uninstall', '-g']],
13
- yarn: ['yarn', ['global', 'remove']],
14
- pnpm: ['pnpm', ['remove', '-g']]
15
- }
16
-
17
7
  export async function uninstallCommand(pkg, options) {
18
8
  const manager = options.runtime?.toLowerCase()
19
9
 
20
- if (!VALID_MANAGERS.includes(manager)) {
21
- console.error(chalk.red(`\n Unknown runtime "${manager}". Valid values: pip, conda, npm, yarn, pnpm\n`))
10
+ const validManagers = getRegisteredRuntimes().filter((rt) => rt.uninstall).map((rt) => rt.name)
11
+
12
+ if (!validManagers.includes(manager)) {
13
+ console.error(chalk.red(`\n Unknown runtime "${manager}". Valid values: ${validManagers.join(', ')}\n`))
22
14
  process.exit(1)
23
15
  }
24
16
 
@@ -46,8 +38,7 @@ export async function uninstallCommand(pkg, options) {
46
38
 
47
39
  console.log(chalk.dim(`\n Running ${manager} uninstall ${pkg}...`))
48
40
 
49
- const [cmd, baseArgs] = UNINSTALL_COMMANDS[manager]
50
- const args = [...baseArgs, pkg]
41
+ const [cmd, args] = getRuntime(manager).uninstall(pkg)
51
42
 
52
43
  try {
53
44
  await runInShell(cmd, args, { timeout: 60000 })