portly-cli 1.0.2 → 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
@@ -2,8 +2,8 @@
2
2
 
3
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
 
@@ -143,10 +143,10 @@ When auto-refresh is enabled (`[AUTO-ON]`), portly automatically scans every 2 s
143
143
 
144
144
  ## 🛠️ Requirements
145
145
 
146
- - macOS or Linux
146
+ - macOS, Linux, or Windows
147
147
  - Node.js 16 or higher
148
- - `lsof` command (standard on macOS/Linux)
149
- - Linux only: `xclip` or `xsel` for copy-to-clipboard feature
148
+ - `lsof` command (macOS/Linux, standard on those platforms)
149
+ - `netstat` and `tasklist` commands (Windows, built-in)
150
150
 
151
151
  ---
152
152
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portly-cli",
3
- "version": "1.0.2",
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
File without changes
@@ -5,9 +5,18 @@ const { CATEGORY_LABELS } = require('../utils/portScanner');
5
5
  function copyToClipboard(text) {
6
6
  const exec = require('child_process').exec;
7
7
  const isMac = process.platform === 'darwin';
8
- const cmd = isMac
9
- ? `echo "${text}" | pbcopy`
10
- : `echo "${text}" | xclip -selection clipboard 2>/dev/null || echo "${text}" | xsel --clipboard 2>/dev/null || echo "Clipboard not available (install xclip)"`;
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
+ }
11
20
  exec(cmd, () => {});
12
21
  }
13
22
 
@@ -505,7 +514,13 @@ class MainScreen {
505
514
 
506
515
  killProcess(pid) {
507
516
  const exec = require('child_process').exec;
508
- 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) => {
509
524
  if (err) {
510
525
  this.statusBar.setContent(`Failed to kill PID ${pid}: ${err.message}`);
511
526
  } else {
@@ -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 };