portly-cli 1.0.1 → 1.1.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 +10 -37
- package/package.json +1 -1
- package/src/index.js +6 -2
- package/src/ui/mainScreen.js +32 -11
- package/src/utils/portScanner.js +119 -1
- package/debug.log +0 -1
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# 🔍 portly
|
|
1
|
+
# 🔍 portly
|
|
2
2
|
|
|
3
|
-
> A
|
|
3
|
+
> A CLI tool to explore and manage open ports. Like `lsof`, but with style.
|
|
4
4
|
|
|
5
|
-

|
|
6
|
-
  
|
|
6
|
+

|
|
7
7
|
|
|
8
8
|
## 🚀 Quick Start
|
|
9
9
|
|
|
@@ -17,17 +17,6 @@ npm install -g portly-cli
|
|
|
17
17
|
portly
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Or run locally:
|
|
21
|
-
```bash
|
|
22
|
-
# Navigate to the portly directory
|
|
23
|
-
cd portly
|
|
24
|
-
|
|
25
|
-
# Run the app
|
|
26
|
-
node src/index.js
|
|
27
|
-
# or
|
|
28
|
-
npm start
|
|
29
|
-
```
|
|
30
|
-
|
|
31
20
|
---
|
|
32
21
|
|
|
33
22
|
## ⌨️ Keyboard Shortcuts
|
|
@@ -131,13 +120,13 @@ When you press `K` on a port, a confirmation dialog appears:
|
|
|
131
120
|
+----------------------------------+
|
|
132
121
|
| |
|
|
133
122
|
| Process: ControlCe |
|
|
134
|
-
| PID: 602
|
|
123
|
+
| PID: 602 |
|
|
135
124
|
| Port: 5000 |
|
|
136
125
|
| |
|
|
137
126
|
| Are you sure you want to kill |
|
|
138
127
|
| this process? |
|
|
139
128
|
| |
|
|
140
|
-
|
|
|
129
|
+
| [Y] KILL [N]/[ESC] CANCEL |
|
|
141
130
|
| |
|
|
142
131
|
+----------------------------------+
|
|
143
132
|
```
|
|
@@ -154,9 +143,10 @@ When auto-refresh is enabled (`[AUTO-ON]`), portly automatically scans every 2 s
|
|
|
154
143
|
|
|
155
144
|
## 🛠️ Requirements
|
|
156
145
|
|
|
157
|
-
- macOS or
|
|
146
|
+
- macOS, Linux, or Windows
|
|
158
147
|
- Node.js 16 or higher
|
|
159
|
-
- `lsof` command (standard on
|
|
148
|
+
- `lsof` command (macOS/Linux, standard on those platforms)
|
|
149
|
+
- `netstat` and `tasklist` commands (Windows, built-in)
|
|
160
150
|
|
|
161
151
|
---
|
|
162
152
|
|
|
@@ -166,27 +156,10 @@ When auto-refresh is enabled (`[AUTO-ON]`), portly automatically scans every 2 s
|
|
|
166
156
|
# Install globally via npm
|
|
167
157
|
npm install -g portly-cli
|
|
168
158
|
|
|
169
|
-
# Run
|
|
159
|
+
# Run the app
|
|
170
160
|
portly
|
|
171
161
|
```
|
|
172
162
|
|
|
173
|
-
### For Development
|
|
174
|
-
|
|
175
|
-
```bash
|
|
176
|
-
# Clone the repo
|
|
177
|
-
git clone <your-repo-url>
|
|
178
|
-
cd portly
|
|
179
|
-
|
|
180
|
-
# Install dependencies
|
|
181
|
-
npm install
|
|
182
|
-
|
|
183
|
-
# Run
|
|
184
|
-
npm start
|
|
185
|
-
|
|
186
|
-
# Or with file watching (auto-reload on changes)
|
|
187
|
-
npm run dev
|
|
188
|
-
```
|
|
189
|
-
|
|
190
163
|
---
|
|
191
164
|
|
|
192
165
|
## 🎨 Port States
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -4,6 +4,9 @@ const blessed = require('blessed');
|
|
|
4
4
|
const MainScreen = require('./ui/mainScreen');
|
|
5
5
|
const { scanPorts } = require('./utils/portScanner');
|
|
6
6
|
|
|
7
|
+
// Disable warnings on startup
|
|
8
|
+
process.emit('warning', { name: 'DeprecationWarning', code: 'DEP', suppress: true });
|
|
9
|
+
|
|
7
10
|
class Portly {
|
|
8
11
|
constructor() {
|
|
9
12
|
this.screen = null;
|
|
@@ -13,11 +16,12 @@ class Portly {
|
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
init() {
|
|
16
|
-
// Create the screen
|
|
19
|
+
// Create the screen with fallback terminal
|
|
17
20
|
this.screen = blessed.screen({
|
|
18
21
|
smartCSR: true,
|
|
19
22
|
title: 'portly - port explorer',
|
|
20
|
-
warnings:
|
|
23
|
+
warnings: false,
|
|
24
|
+
terminal: process.env.TERM || 'xterm',
|
|
21
25
|
});
|
|
22
26
|
|
|
23
27
|
// Create main screen (binds keys internally)
|
package/src/ui/mainScreen.js
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
const blessed = require('blessed');
|
|
2
2
|
const { CATEGORY_LABELS } = require('../utils/portScanner');
|
|
3
3
|
|
|
4
|
+
// Cross-platform clipboard copy
|
|
5
|
+
function copyToClipboard(text) {
|
|
6
|
+
const exec = require('child_process').exec;
|
|
7
|
+
const isMac = process.platform === 'darwin';
|
|
8
|
+
const isWindows = process.platform === 'win32';
|
|
9
|
+
|
|
10
|
+
let cmd;
|
|
11
|
+
if (isWindows) {
|
|
12
|
+
// Windows: use clip command
|
|
13
|
+
cmd = `echo ${text}| clip`;
|
|
14
|
+
} else if (isMac) {
|
|
15
|
+
cmd = `echo "${text}" | pbcopy`;
|
|
16
|
+
} else {
|
|
17
|
+
// Linux
|
|
18
|
+
cmd = `echo "${text}" | xclip -selection clipboard 2>/dev/null || echo "${text}" | xsel --clipboard 2>/dev/null || echo "Clipboard not available (install xclip)"`;
|
|
19
|
+
}
|
|
20
|
+
exec(cmd, () => {});
|
|
21
|
+
}
|
|
22
|
+
|
|
4
23
|
const STATUS_ICONS = {
|
|
5
24
|
LISTEN: '[L]',
|
|
6
25
|
ESTABLISHED: '[E]',
|
|
@@ -495,7 +514,13 @@ class MainScreen {
|
|
|
495
514
|
|
|
496
515
|
killProcess(pid) {
|
|
497
516
|
const exec = require('child_process').exec;
|
|
498
|
-
|
|
517
|
+
const isWindows = process.platform === 'win32';
|
|
518
|
+
|
|
519
|
+
const killCmd = isWindows
|
|
520
|
+
? `taskkill /F /PID ${pid}`
|
|
521
|
+
: `kill ${pid}`;
|
|
522
|
+
|
|
523
|
+
exec(killCmd, (err) => {
|
|
499
524
|
if (err) {
|
|
500
525
|
this.statusBar.setContent(`Failed to kill PID ${pid}: ${err.message}`);
|
|
501
526
|
} else {
|
|
@@ -511,21 +536,17 @@ class MainScreen {
|
|
|
511
536
|
}
|
|
512
537
|
|
|
513
538
|
copyPort() {
|
|
514
|
-
const exec = require('child_process').exec;
|
|
515
539
|
const port = this.selectedPort.port;
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
});
|
|
540
|
+
copyToClipboard(port);
|
|
541
|
+
this.statusBar.setContent(`Copied port ${port} to clipboard!`);
|
|
542
|
+
this.renderStatusBar();
|
|
520
543
|
}
|
|
521
544
|
|
|
522
545
|
copyPid() {
|
|
523
|
-
const exec = require('child_process').exec;
|
|
524
546
|
const pid = this.selectedPort.pid;
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
});
|
|
547
|
+
copyToClipboard(pid);
|
|
548
|
+
this.statusBar.setContent(`Copied PID ${pid} to clipboard!`);
|
|
549
|
+
this.renderStatusBar();
|
|
529
550
|
}
|
|
530
551
|
}
|
|
531
552
|
|
package/src/utils/portScanner.js
CHANGED
|
@@ -22,6 +22,7 @@ function getProtocolLabel(protocol) {
|
|
|
22
22
|
function getStateLabel(state) {
|
|
23
23
|
const states = {
|
|
24
24
|
LISTEN: '🟢',
|
|
25
|
+
LISTENING: '🟢',
|
|
25
26
|
ESTABLISHED: '🟡',
|
|
26
27
|
TIME_WAIT: '⏳',
|
|
27
28
|
CLOSE_WAIT: '⏳',
|
|
@@ -34,7 +35,11 @@ function getStateLabel(state) {
|
|
|
34
35
|
return states[state] || '⚪';
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
function isWindows() {
|
|
39
|
+
return process.platform === 'win32';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function scanPortsMacLinux() {
|
|
38
43
|
const ports = [];
|
|
39
44
|
|
|
40
45
|
try {
|
|
@@ -108,4 +113,117 @@ async function scanPorts() {
|
|
|
108
113
|
return Array.from(seen.values()).sort((a, b) => a.port - b.port);
|
|
109
114
|
}
|
|
110
115
|
|
|
116
|
+
async function scanPortsWindows() {
|
|
117
|
+
const ports = [];
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// Get netstat output - parse in Node.js to handle locale differences
|
|
121
|
+
const { stdout: netstatOut } = await execAsync(
|
|
122
|
+
'netstat -ano',
|
|
123
|
+
{ maxBuffer: 1024 * 1024 * 10, encoding: 'utf8' }
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Get process list (name and PID)
|
|
127
|
+
const { stdout: tasklistOut } = await execAsync(
|
|
128
|
+
'tasklist /FO CSV /NH',
|
|
129
|
+
{ maxBuffer: 1024 * 1024 * 10, encoding: 'utf8' }
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Parse tasklist into a map: PID -> Command
|
|
133
|
+
const pidToCmd = new Map();
|
|
134
|
+
const tasklistLines = tasklistOut.split('\n');
|
|
135
|
+
for (const line of tasklistLines) {
|
|
136
|
+
const match = line.match(/"([^"]+)","(\d+)"/);
|
|
137
|
+
if (match) {
|
|
138
|
+
pidToCmd.set(match[2], match[1]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const lines = netstatOut.split('\n');
|
|
143
|
+
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
if (!line.trim()) continue;
|
|
146
|
+
|
|
147
|
+
// Skip header lines and non-TCP lines
|
|
148
|
+
if (!line.includes('TCP') || line.includes('Proto') || line.startsWith('Aktive') || line.startsWith('Active')) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const parts = line.trim().split(/\s+/);
|
|
153
|
+
if (parts.length < 5) continue;
|
|
154
|
+
|
|
155
|
+
const protocol = parts[0]; // TCP
|
|
156
|
+
const localAddr = parts[1]; // 0.0.0.0:135 or [::]:5000
|
|
157
|
+
const pid = parseInt(parts[4], 10);
|
|
158
|
+
|
|
159
|
+
// Check if it's a listening state by foreign address (locale-independent)
|
|
160
|
+
const foreignAddr = parts[2];
|
|
161
|
+
const isListening = /^(0\.0\.0\.0:0|\[?::\]?:0)$/.test(foreignAddr);
|
|
162
|
+
if (!isListening) continue;
|
|
163
|
+
|
|
164
|
+
if (isNaN(pid)) continue;
|
|
165
|
+
|
|
166
|
+
// Parse port from local address
|
|
167
|
+
let port;
|
|
168
|
+
let localAddress;
|
|
169
|
+
|
|
170
|
+
if (localAddr.startsWith('[')) {
|
|
171
|
+
// IPv6 format: [::]:5000
|
|
172
|
+
const match = localAddr.match(/\[([^\]]+)\]:(\d+)/);
|
|
173
|
+
if (!match) continue;
|
|
174
|
+
localAddress = match[1];
|
|
175
|
+
port = parseInt(match[2], 10);
|
|
176
|
+
} else {
|
|
177
|
+
// IPv4 format: 0.0.0.0:135
|
|
178
|
+
const colonIdx = localAddr.lastIndexOf(':');
|
|
179
|
+
if (colonIdx === -1) continue;
|
|
180
|
+
localAddress = localAddr.substring(0, colonIdx);
|
|
181
|
+
port = parseInt(localAddr.substring(colonIdx + 1), 10);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (isNaN(port)) continue;
|
|
185
|
+
|
|
186
|
+
const command = pidToCmd.get(String(pid)) || 'Unknown';
|
|
187
|
+
|
|
188
|
+
// Determine protocol type (TCP4 or TCP6)
|
|
189
|
+
const protocolFull = localAddr.startsWith('[') ? 'TCP6' : 'TCP';
|
|
190
|
+
|
|
191
|
+
ports.push({
|
|
192
|
+
command,
|
|
193
|
+
pid,
|
|
194
|
+
user: 'N/A', // Windows netstat doesn't provide user info like lsof on Unix
|
|
195
|
+
port,
|
|
196
|
+
protocol: protocol === 'TCP' ? 'TCP' : protocol,
|
|
197
|
+
protocolFull,
|
|
198
|
+
state: 'LISTEN',
|
|
199
|
+
localAddress: localAddress === '0.0.0.0' || localAddress === '::' || localAddress === '[::]' ? '*' : localAddress,
|
|
200
|
+
remoteAddress: '-',
|
|
201
|
+
category: getCategory(port),
|
|
202
|
+
stateIcon: getStateLabel('LISTEN'),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.error('Error running netstat:', err.message);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Deduplicate by port+protocol
|
|
210
|
+
const seen = new Map();
|
|
211
|
+
for (const p of ports) {
|
|
212
|
+
const key = `${p.port}-${p.protocol}`;
|
|
213
|
+
if (!seen.has(key)) {
|
|
214
|
+
seen.set(key, p);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Sort by port
|
|
219
|
+
return Array.from(seen.values()).sort((a, b) => a.port - b.port);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function scanPorts() {
|
|
223
|
+
if (isWindows()) {
|
|
224
|
+
return scanPortsWindows();
|
|
225
|
+
}
|
|
226
|
+
return scanPortsMacLinux();
|
|
227
|
+
}
|
|
228
|
+
|
|
111
229
|
module.exports = { scanPorts, getCategory, getProtocolLabel, CATEGORY_LABELS };
|
package/debug.log
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
This file is just a marker.
|