@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 +21 -0
- package/README.md +123 -0
- package/bin/portwatch.js +277 -0
- package/package.json +47 -0
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
|
+
   
|
|
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>
|
package/bin/portwatch.js
ADDED
|
@@ -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
|
+
}
|