@lowdep/portwatch 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rushabh Shah
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.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # portwatch
2
+
3
+ ![Zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen) ![Node](https://img.shields.io/badge/node-%3E%3D14-blue) ![License: MIT](https://img.shields.io/badge/license-MIT-green) ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)
4
+
5
+
6
+ Live terminal dashboard for monitoring your local development services.
7
+
8
+ Checks TCP ports and HTTP/HTTPS URLs on a configurable interval, shows latency, status, and when each service last changed state. Zero dependencies — uses only Node.js built-ins.
9
+
10
+ ---
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install -g portwatch
16
+ ```
17
+
18
+ Or without installing:
19
+
20
+ ```bash
21
+ npx portwatch
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Quick Start
27
+
28
+ ```bash
29
+ # 1. Create a config in your project
30
+ portwatch init
31
+
32
+ # 2. Edit .portwatch.json with your services
33
+ # 3. Start the dashboard
34
+ portwatch
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Example Output
40
+
41
+ ```
42
+ portwatch (every 5s · Ctrl+C to exit · config: .portwatch.json)
43
+
44
+ Service Target Status Latency Changed
45
+ ─────────────────────────────────────────────────────────────────────────
46
+ Frontend localhost:3000 ✓ UP 12ms 2m ago
47
+ Backend API localhost:4000 ✓ UP 8ms 5m ago
48
+ PostgreSQL localhost:5432 ✓ UP 2ms 10m ago
49
+ Redis localhost:6379 ✗ DOWN — 1m ago
50
+ Stripe Webhook http://localhost:8080 ✓ UP 45ms 30s ago
51
+
52
+ Last refresh: 14:32:05
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Config File (`.portwatch.json`)
58
+
59
+ ```json
60
+ {
61
+ "interval": 5,
62
+ "services": [
63
+ { "name": "Frontend", "target": "localhost:3000" },
64
+ { "name": "Backend API", "target": "localhost:4000" },
65
+ { "name": "PostgreSQL", "target": "localhost:5432" },
66
+ { "name": "Redis", "target": "localhost:6379" },
67
+ { "name": "Health check", "target": "http://localhost:4000/health" }
68
+ ]
69
+ }
70
+ ```
71
+
72
+ ### Target formats
73
+
74
+ | Format | What it checks |
75
+ |---|---|
76
+ | `localhost:3000` | TCP connection to port |
77
+ | `:5432` | TCP on localhost |
78
+ | `3000` | TCP on localhost |
79
+ | `http://localhost:4000` | HTTP GET, checks status < 500 |
80
+ | `https://api.mysite.com/health` | HTTPS GET |
81
+
82
+ ---
83
+
84
+ ## Usage
85
+
86
+ ```bash
87
+ portwatch # Uses .portwatch.json in current dir
88
+ portwatch init # Create starter .portwatch.json
89
+ portwatch ./myconfig.json # Use a specific config file
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Status Indicators
95
+
96
+ | Symbol | Meaning |
97
+ |---|---|
98
+ | `✓ UP` | Connected and responding |
99
+ | `⚡ SLOW` | Responding but latency > 1000ms |
100
+ | `✗ DOWN` | Connection refused or timed out |
101
+ | `checking…` | Initial check in progress |
102
+
103
+ ---
104
+
105
+ ## License
106
+
107
+ MIT
108
+
109
+ ---
110
+
111
+ ## Keywords
112
+
113
+ `port monitor` · `service health` · `dev dashboard` · `localhost monitor` · `tcp check` · `uptime monitor` · `watch ports` · `health dashboard` · `zero dependencies` · `cli`
114
+
115
+ ---
116
+
117
+ <div align="center">
118
+
119
+ **Built to solve, shared to help — Rushabh Shah 🛠️✨**
120
+
121
+ <sub>One of 40+ zero-dependency developer CLI tools — no <code>node_modules</code>, ever.</sub>
122
+
123
+ </div>
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const net = require('net');
5
+ const http = require('http');
6
+ const https = require('https');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+
11
+ const VERSION = '1.0.0';
12
+ const CFG_FILE = '.portwatch.json';
13
+
14
+ // ─── ANSI ─────────────────────────────────────────────────────────────────────
15
+ const isTTY = process.stdout.isTTY;
16
+ const c = (code, t) => isTTY ? `\x1b[${code}m${t}\x1b[0m` : t;
17
+ const bold = t => c('1', t);
18
+ const dim = t => c('2', t);
19
+ const red = t => c('31', t);
20
+ const green = t => c('32', t);
21
+ const yellow = t => c('33', t);
22
+ const cyan = t => c('36', t);
23
+ const up = '\x1b[F'; // cursor up one line
24
+
25
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
26
+ function timeAgo(ts) {
27
+ if (!ts) return dim('—');
28
+ const secs = Math.floor((Date.now() - ts) / 1000);
29
+ if (secs < 5) return dim('just now');
30
+ if (secs < 60) return dim(`${secs}s ago`);
31
+ if (secs < 3600) return dim(`${Math.floor(secs / 60)}m ago`);
32
+ return dim(`${Math.floor(secs / 3600)}h ago`);
33
+ }
34
+
35
+ function parseTarget(raw) {
36
+ // Accepts: "localhost:3000", "http://localhost:3000", ":5432", "3000"
37
+ if (typeof raw !== 'string' || !raw.trim()) return null;
38
+ raw = raw.trim();
39
+ if (/^https?:\/\//.test(raw)) {
40
+ const u = new URL(raw);
41
+ return { type: 'http', url: raw, host: u.hostname, port: parseInt(u.port) || (u.protocol === 'https:' ? 443 : 80) };
42
+ }
43
+ const m = raw.match(/^:?(\d+)$/) || raw.match(/^([^:]+):(\d+)$/);
44
+ if (!m) return null;
45
+ const host = m[2] ? m[1] : 'localhost';
46
+ const port = parseInt(m[2] || m[1]);
47
+ return { type: 'tcp', host, port };
48
+ }
49
+
50
+ // ─── Checkers ─────────────────────────────────────────────────────────────────
51
+ function checkTCP(host, port, timeoutMs = 3000) {
52
+ return new Promise(resolve => {
53
+ const start = Date.now();
54
+ const socket = net.createConnection({ host, port });
55
+ socket.setTimeout(timeoutMs);
56
+ socket.on('connect', () => {
57
+ socket.destroy();
58
+ resolve({ up: true, latency: Date.now() - start });
59
+ });
60
+ socket.on('error', () => { socket.destroy(); resolve({ up: false, latency: null }); });
61
+ socket.on('timeout', () => { socket.destroy(); resolve({ up: false, latency: null }); });
62
+ });
63
+ }
64
+
65
+ function checkHTTP(url, timeoutMs = 5000) {
66
+ return new Promise(resolve => {
67
+ const start = Date.now();
68
+ const lib = url.startsWith('https') ? https : http;
69
+ const req = lib.get(url, { timeout: timeoutMs }, res => {
70
+ res.resume();
71
+ const latency = Date.now() - start;
72
+ resolve({ up: res.statusCode < 500, latency, status: res.statusCode });
73
+ });
74
+ req.on('error', () => resolve({ up: false, latency: null }));
75
+ req.on('timeout', () => { req.destroy(); resolve({ up: false, latency: null }); });
76
+ });
77
+ }
78
+
79
+ async function checkService(svc) {
80
+ const target = svc._parsed;
81
+ let result;
82
+ if (target.type === 'http') {
83
+ result = await checkHTTP(target.url);
84
+ } else {
85
+ result = await checkTCP(target.host, target.port);
86
+ }
87
+ return result;
88
+ }
89
+
90
+ // ─── Config ───────────────────────────────────────────────────────────────────
91
+ function defaultConfig() {
92
+ return {
93
+ interval: 5,
94
+ services: [
95
+ { name: 'Frontend', target: 'localhost:3000' },
96
+ { name: 'Backend API', target: 'localhost:4000' },
97
+ { name: 'PostgreSQL', target: 'localhost:5432' },
98
+ { name: 'Redis', target: 'localhost:6379' },
99
+ ],
100
+ };
101
+ }
102
+
103
+ function loadConfig(cfgPath) {
104
+ try {
105
+ return JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+
111
+ // ─── Renderer ─────────────────────────────────────────────────────────────────
112
+ let prevLines = 0;
113
+
114
+ function clearPrev() {
115
+ if (!isTTY || prevLines === 0) return;
116
+ process.stdout.write(up.repeat(prevLines) + '\x1b[J');
117
+ }
118
+
119
+ function render(services, interval, cfgFile) {
120
+ const now = new Date().toLocaleTimeString();
121
+ const lines = [];
122
+
123
+ const nameW = Math.max(...services.map(s => s.name.length), 12);
124
+ const targetW = Math.max(...services.map(s => s.target.length), 12);
125
+
126
+ lines.push(`${bold('portwatch')} ${dim(`(every ${interval}s · Ctrl+C to exit · config: ${cfgFile})`)}`);
127
+ lines.push('');
128
+ lines.push(
129
+ ` ${'Service'.padEnd(nameW)} ${'Target'.padEnd(targetW)} ${'Status'.padEnd(10)} ${'Latency'.padStart(8)} ${'Changed'}`,
130
+ );
131
+ lines.push(dim(' ' + '─'.repeat(nameW + targetW + 40)));
132
+
133
+ for (const svc of services) {
134
+ const statusStr = svc.status === undefined
135
+ ? dim('checking…')
136
+ : svc.status
137
+ ? (svc.latency > 1000 ? yellow('⚡ SLOW') : green('✓ UP '))
138
+ : red('✗ DOWN');
139
+
140
+ const latencyStr = svc.latency != null
141
+ ? (svc.latency > 1000 ? yellow(`${svc.latency}ms`) : dim(`${svc.latency}ms`))
142
+ : dim(' —');
143
+
144
+ lines.push(
145
+ ` ${svc.name.padEnd(nameW)} ${dim(svc.target.padEnd(targetW))} ${statusStr.padEnd(10)} ${latencyStr.padStart(8)} ${timeAgo(svc.changedAt)}`,
146
+ );
147
+ }
148
+
149
+ lines.push('');
150
+ lines.push(dim(` Last refresh: ${now}`));
151
+
152
+ clearPrev();
153
+ process.stdout.write(lines.join('\n') + '\n');
154
+ prevLines = lines.length;
155
+ }
156
+
157
+ // ─── Main loop ────────────────────────────────────────────────────────────────
158
+ async function runLoop(services, interval, cfgFile) {
159
+ // Initial render
160
+ render(services, interval, cfgFile);
161
+
162
+ async function tick() {
163
+ await Promise.all(services.map(async svc => {
164
+ const result = await checkService(svc);
165
+ const wasUp = svc.status;
166
+ svc.status = result.up;
167
+ svc.latency = result.latency;
168
+ if (wasUp !== result.up) svc.changedAt = Date.now();
169
+ }));
170
+ render(services, interval, cfgFile);
171
+ }
172
+
173
+ // First real check immediately
174
+ await tick();
175
+
176
+ // Then repeat
177
+ const timer = setInterval(tick, interval * 1000);
178
+
179
+ process.on('SIGINT', () => { clearInterval(timer); console.log('\n'); process.exit(0); });
180
+ process.on('SIGTERM', () => { clearInterval(timer); console.log('\n'); process.exit(0); });
181
+ }
182
+
183
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
184
+ const args = process.argv.slice(2);
185
+ const flags = new Set(args.filter(a => a.startsWith('-')));
186
+ const positional = args.filter(a => !a.startsWith('-'));
187
+
188
+ if (flags.has('--version') || flags.has('-v')) {
189
+ console.log(`portwatch v${VERSION}`); process.exit(0);
190
+ }
191
+
192
+ if (flags.has('--help') || flags.has('-h')) {
193
+ console.log(`
194
+ ${bold('portwatch')} — Live terminal dashboard for dev service monitoring
195
+
196
+ ${bold('USAGE')}
197
+ portwatch [config-file]
198
+ portwatch init Create a .portwatch.json in the current directory
199
+
200
+ ${bold('OPTIONS')}
201
+ --version Show version
202
+ --help Show this help
203
+
204
+ ${bold('CONFIG')} (${CFG_FILE})
205
+ ${JSON.stringify(defaultConfig(), null, 2).split('\n').join('\n ')}
206
+
207
+ ${bold('TARGETS')}
208
+ "localhost:3000" TCP port check
209
+ ":5432" TCP on localhost
210
+ "http://localhost:4000" HTTP check (validates < 500 status)
211
+ "https://api.mysite.com" HTTPS check
212
+
213
+ ${bold('EXAMPLES')}
214
+ portwatch Look for .portwatch.json in current dir
215
+ portwatch init Create a starter .portwatch.json
216
+ portwatch ./myconfig.json Use a specific config file
217
+ `);
218
+ process.exit(0);
219
+ }
220
+
221
+ // init sub-command
222
+ if (positional[0] === 'init') {
223
+ const dest = path.join(process.cwd(), CFG_FILE);
224
+ if (fs.existsSync(dest)) {
225
+ console.log(yellow(`\n.portwatch.json already exists: ${dest}\n`));
226
+ } else {
227
+ fs.writeFileSync(dest, JSON.stringify(defaultConfig(), null, 2));
228
+ console.log(green(`\n✓ Created ${dest}\n`));
229
+ console.log(dim(' Edit it to add your services, then run: portwatch\n'));
230
+ }
231
+ process.exit(0);
232
+ }
233
+
234
+ // Find config
235
+ const cfgPath = positional[0]
236
+ ? path.resolve(positional[0])
237
+ : path.join(process.cwd(), CFG_FILE);
238
+
239
+ let cfg = loadConfig(cfgPath);
240
+ if (!cfg) {
241
+ console.log(yellow(`\nNo config found. Using defaults.`));
242
+ console.log(dim(` Run \`portwatch init\` to create ${CFG_FILE} in this directory.\n`));
243
+ cfg = defaultConfig();
244
+ }
245
+
246
+ const interval = cfg.interval || 5;
247
+
248
+ // Prepare service objects
249
+ const services = (cfg.services || []).map(svc => {
250
+ // Accept `target`, `url`, or `host`+`port` field forms
251
+ const targetStr = svc.target
252
+ || svc.url
253
+ || (svc.host && svc.port ? `${svc.host}:${svc.port}` : (svc.port ? `localhost:${svc.port}` : null));
254
+ const parsed = parseTarget(targetStr);
255
+ if (!parsed) {
256
+ console.error(red(`Invalid/missing target for service "${svc.name || '(unnamed)'}" — skipping`));
257
+ return null;
258
+ }
259
+ return {
260
+ name: svc.name || targetStr,
261
+ target: targetStr,
262
+ _parsed: parsed,
263
+ status: undefined,
264
+ latency: null,
265
+ changedAt: null,
266
+ };
267
+ }).filter(Boolean);
268
+
269
+ if (!services.length) {
270
+ console.error(red('\nNo valid services configured.\n'));
271
+ process.exit(1);
272
+ }
273
+
274
+ runLoop(services, interval, path.relative(process.cwd(), cfgPath) || CFG_FILE).catch(e => {
275
+ console.error(red(`\nError: ${e.message}\n`));
276
+ process.exit(1);
277
+ });
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@lowdep/portwatch",
3
+ "version": "1.0.0",
4
+ "description": "Live terminal dashboard monitoring local dev services — TCP ports and HTTP URLs, zero dependencies",
5
+ "bin": {
6
+ "portwatch": "bin/portwatch.js"
7
+ },
8
+ "keywords": [
9
+ "port",
10
+ "monitor",
11
+ "dashboard",
12
+ "developer-tools",
13
+ "cli",
14
+ "tcp",
15
+ "http",
16
+ "health-check",
17
+ "devops",
18
+ "port monitor",
19
+ "service health",
20
+ "dev dashboard",
21
+ "localhost monitor",
22
+ "tcp check",
23
+ "uptime monitor",
24
+ "watch ports",
25
+ "health dashboard",
26
+ "zero dependencies"
27
+ ],
28
+ "author": "Rushabh Shah",
29
+ "license": "MIT",
30
+ "engines": {
31
+ "node": ">=14"
32
+ },
33
+ "files": [
34
+ "bin/"
35
+ ],
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/Rushabh5000/portwatch.git"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/Rushabh5000/portwatch/issues"
42
+ },
43
+ "homepage": "https://github.com/Rushabh5000/portwatch#readme",
44
+ "publishConfig": {
45
+ "access": "public"
46
+ }
47
+ }