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 CHANGED
@@ -1,9 +1,9 @@
1
- # 🔍 portly-cli
1
+ # 🔍 portly
2
2
 
3
- > A pretty CLI tool to explore and manage open ports. Like `lsof`, but with style.
3
+ > A CLI tool to explore and manage open ports. Like `lsof`, but with style.
4
4
 
5
- ![Terminal](https://img.shields.io/badge/Terminal-macOS-green)
6
- ![Node](https://img.shields.io/badge/Node-16+-yellow)
5
+ ![Terminal](https://img.shields.io/badge/Terminal-macOS-green) ![Terminal](https://img.shields.io/badge/Terminal-Linux-green) ![Terminal](https://img.shields.io/badge/Terminal-Windows-blue)
6
+ ![npm](https://img.shields.io/badge/npm-yellow)
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
- | [Y] KILL [N] CANCEL |
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 Linux
146
+ - macOS, Linux, or Windows
158
147
  - Node.js 16 or higher
159
- - `lsof` command (standard on macOS/Linux)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portly-cli",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "A pretty CLI tool to explore and manage open ports. Like lsof, but with style.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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: true,
23
+ warnings: false,
24
+ terminal: process.env.TERM || 'xterm',
21
25
  });
22
26
 
23
27
  // Create main screen (binds keys internally)
@@ -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
- exec(`kill ${pid}`, (err) => {
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
- exec(`echo ${port} | pbcopy`, () => {
517
- this.statusBar.setContent(`Copied port ${port} to clipboard!`);
518
- this.renderStatusBar();
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
- exec(`echo ${pid} | pbcopy`, () => {
526
- this.statusBar.setContent(`Copied PID ${pid} to clipboard!`);
527
- this.renderStatusBar();
528
- });
547
+ copyToClipboard(pid);
548
+ this.statusBar.setContent(`Copied PID ${pid} to clipboard!`);
549
+ this.renderStatusBar();
529
550
  }
530
551
  }
531
552
 
@@ -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
- async function scanPorts() {
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.