portmind-monorepo 0.1.0 → 0.3.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/CHANGELOG.md +29 -0
- package/README.md +43 -19
- package/package.json +21 -2
- package/packages/cli/package.json +2 -1
- package/packages/cli/src/index.ts +74 -5
- package/packages/core/data/iana-cache.json +1 -0
- package/packages/core/package.json +5 -3
- package/packages/core/src/configLoader.ts +140 -0
- package/packages/core/src/index.ts +9 -0
- package/packages/core/src/knownPorts/lookup.ts +99 -0
- package/packages/core/src/knownPorts/parseIana.ts +65 -0
- package/packages/core/src/scan/index.ts +18 -3
- package/packages/core/tests/configLoader.test.ts +54 -0
- package/packages/core/tests/lookup.test.ts +87 -0
- package/packages/core/tests/parseIana.test.ts +47 -0
- package/packages/web/package.json +44 -0
- package/packages/web/src/index.ts +1 -0
- package/packages/web/src/server.ts +51 -0
- package/packages/web/static/index.html +268 -0
- package/packages/web/tests/server.test.ts +37 -0
- package/packages/web/tsconfig.json +9 -0
- package/packages/web/vitest.config.ts +8 -0
- package/portmind.config.example.yaml +51 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.0] - 2026-09-06
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `@portmind/core`: real IANA known-port lookup - CSV fetched and verified directly from `iana.org`, parsed into 11,394 real entries, bundled as `packages/core/data/iana-cache.json` so lookups work fully offline. `PortEntry.knownService` is now populated for any port with an official registration (e.g. `5432` -> "PostgreSQL Database", `3306` -> "MySQL").
|
|
13
|
+
- `@portmind/core`: real config file loader (`configLoader.ts`) - reads and deep-merges `~/.portmind/config.yaml` (user) and `.portmind.yaml` (project) over the built-in defaults. A config file that exists but fails to parse throws `ConfigError`, mapped to CLI exit code `2`. `known_ports.source: custom|both` + `custom_db_path` let a JSON file override or extend IANA (e.g. for internal services or dev-convention ports like Redis/MongoDB that IANA doesn't register).
|
|
14
|
+
- `@portmind/cli`: `portmind config show` (prints the fully resolved config as JSON) and `portmind config path` (prints both config file locations and whether each exists).
|
|
15
|
+
- `portmind.config.example.yaml` at repo root - copy-and-edit starting point for either config location.
|
|
16
|
+
- `scanPorts()` in `core` now takes a `knownPorts` config parameter (defaulting to `DEFAULT_CONFIG.knownPorts`), and both CLI `list` and the web server's `/api/ports` load real config via `loadConfig()` instead of using the hardcoded default - this is the actual wiring point through which config file changes take effect.
|
|
17
|
+
- Vitest coverage: IANA CSV parsing (quoted fields, Reserved/Unassigned filtering, non-tcp/udp transports), known-port lookup (iana/custom/both source modes, missing custom file handling), and config loading (defaults-only, project override merge, malformed-YAML error).
|
|
18
|
+
|
|
19
|
+
### Known limitations
|
|
20
|
+
- `known_ports.refresh_days` is not enforced - the bundled IANA cache is a one-time snapshot (fetched 2026-09-06), not auto-refreshed.
|
|
21
|
+
- `history.retention_days` and `risk_rules.*` are defined in config but not yet consumed - history and risk flags aren't implemented.
|
|
22
|
+
- `logging.audit_log` is defined but nothing writes to it yet - there's no `free`/`explain` command to audit.
|
|
23
|
+
|
|
24
|
+
## [0.2.0] - 2026-09-06
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- `@portmind/web`: local-only web dashboard (Phase 8) - plain Node `http` server bound to `127.0.0.1`, `GET /api/ports` returning the same `PortEntry[]` JSON as `portmind list --json`, and a static vanilla HTML/JS page (no framework, no build step) with a sortable/filterable table and a row-click detail panel.
|
|
28
|
+
- `@portmind/cli`: `portmind web` command (`--port`, `--no-open`) that starts the dashboard and opens the default browser.
|
|
29
|
+
- Vitest coverage for the web server: page route, API route, and 404 handling.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- Root `package.json` (published to npm as `portmind-monorepo`) was missing `license` and `keywords` - added, along with matching metadata already present on the scoped packages.
|
|
33
|
+
|
|
34
|
+
### Known limitations
|
|
35
|
+
- The web dashboard's Docker-only/unusual filters have nothing to filter yet, since Docker cross-reference, history, and risk flags aren't implemented. Its "Explain with AI" button is a disabled placeholder pending Phase 9 (AI `explain`).
|
|
36
|
+
- `portmind-monorepo` on npm still has no `bin` field - it is not an installable CLI. The real CLI package, `@portmind/cli`, has not been published.
|
|
37
|
+
|
|
9
38
|
## [0.1.0] - 2026-09-06
|
|
10
39
|
|
|
11
40
|
### Added
|
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# portmind
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/portmind-monorepo)
|
|
4
4
|
[](https://nodejs.org)
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
|
|
7
|
-
Local-first CLI that scans listening ports on your machine, enriches them with process/Docker/IANA detail, remembers what *normally* runs on each port, and flags what's unusual — no cloud, no telemetry, no background AI calls.
|
|
7
|
+
Local-first CLI + web dashboard that scans listening ports on your machine, enriches them with process/Docker/IANA detail, remembers what *normally* runs on each port, and flags what's unusual — no cloud, no telemetry, no background AI calls.
|
|
8
8
|
|
|
9
|
-
>
|
|
9
|
+
> `portmind-monorepo` is published on npm (see the badge above), but it has no `bin` field — installing it does not give you a `portmind` command. `@portmind/cli`, the package that actually would, is not published yet.
|
|
10
10
|
|
|
11
11
|
## Status
|
|
12
12
|
|
|
@@ -15,8 +15,12 @@ Local-first CLI that scans listening ports on your machine, enriches them with p
|
|
|
15
15
|
- Each result is enriched with process command line, working directory, and start time (`ps` + `lsof -d cwd`)
|
|
16
16
|
- `--range <min-max>`, `--docker-only`, `--unusual`, and `--json` filters/output modes
|
|
17
17
|
- Table and JSON output share one `PortEntry` type, defined once in `@portmind/core`
|
|
18
|
+
- `portmind web` starts a local-only dashboard (`127.0.0.1`, plain HTML/JS, no framework, no build step) serving the same data as `portmind list --json` over `GET /api/ports`, with a sortable/filterable table and a row-click detail panel
|
|
19
|
+
- Real known-port descriptions via the official IANA Service Name and Port Number Registry (11,394 entries bundled, fetched and verified from `iana.org` — e.g. port 5432 shows "PostgreSQL Database", not a placeholder)
|
|
20
|
+
- A real, working config file loader — `~/.portmind/config.yaml` (user) and `.portmind.yaml` (project) are both read and merged over the defaults; `portmind config show`/`config path` expose the resolved result and file locations
|
|
21
|
+
- `known_ports.source: custom` or `both` in config lets you point at your own JSON file to override or add service descriptions IANA doesn't have (internal services, dev conventions like Redis/MongoDB that aren't officially IANA-registered)
|
|
18
22
|
|
|
19
|
-
**Not implemented yet** (see [Next steps](#next-steps)): Docker cross-reference,
|
|
23
|
+
**Not implemented yet** (see [Next steps](#next-steps)): Docker cross-reference, local history/"usual" detection, risk flags, `watch`/`free`/`explain`/`history`/`ssh`/`tui` commands. Until those land, every `PortEntry.docker` and `.riskFlags` will be empty, and `.history.usual` is always `true` — so the web dashboard's Docker/unusual filters currently have nothing to filter.
|
|
20
24
|
|
|
21
25
|
See [CHANGELOG.md](./CHANGELOG.md) for a dated record of what shipped when.
|
|
22
26
|
|
|
@@ -55,12 +59,31 @@ Example output:
|
|
|
55
59
|
```
|
|
56
60
|
PORT PROTO PROCESS PID DOCKER USUAL NOTE
|
|
57
61
|
3000 tcp node 41822 - yes /Users/you/Projects/react-dashboard
|
|
58
|
-
5432 tcp postgres 1758 - yes
|
|
62
|
+
5432 tcp postgres 1758 - yes PostgreSQL Database
|
|
59
63
|
```
|
|
60
64
|
|
|
61
65
|
**Exit codes:** `0` success, `1` scan error, `2` config error.
|
|
62
66
|
|
|
63
|
-
|
|
67
|
+
### Web dashboard
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
portmind web # starts on http://127.0.0.1:4400 and opens your browser
|
|
71
|
+
portmind web --port 4401 # use a different port for the dashboard itself
|
|
72
|
+
portmind web --no-open # don't open the browser automatically
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Why a plain server + vanilla JS instead of a framework: the dashboard is two routes (the page, and `/api/ports`), it never leaves your machine, and there's no build pipeline to maintain — consistent with the "local-first, no telemetry" design of the rest of the tool. The page polls `/api/ports` on load and on manual refresh (or a 5-second auto-refresh you opt into); clicking a row expands full detail including an "Explain with AI" button that's currently a disabled placeholder, since AI `explain` (Phase 9) isn't built yet.
|
|
76
|
+
|
|
77
|
+
### Config
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
portmind config show # print the fully resolved config (defaults + user + project merged), as JSON
|
|
81
|
+
portmind config path # print ~/.portmind/config.yaml and ./.portmind.yaml, and whether each exists
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Copy [portmind.config.example.yaml](./portmind.config.example.yaml) to `~/.portmind/config.yaml` or `./.portmind.yaml` to change behavior — only the keys you include override the defaults, anything omitted falls back. Malformed YAML in a config file that exists is a hard error (exit code `2`), not silently ignored.
|
|
85
|
+
|
|
86
|
+
Planned commands not yet implemented: `watch`, `free`, `explain`, `history`, `ssh <host> list|check`, `tui`.
|
|
64
87
|
|
|
65
88
|
## Storage
|
|
66
89
|
|
|
@@ -68,7 +91,7 @@ History will be stored locally in a SQLite database at `~/.portmind/portmind.db`
|
|
|
68
91
|
|
|
69
92
|
## Configuration
|
|
70
93
|
|
|
71
|
-
|
|
94
|
+
Real and working. Resolution order (later overrides earlier, and only the keys you actually set are overridden - everything else falls back): built-in defaults → `~/.portmind/config.yaml` (user) → `.portmind.yaml` in the current directory (project). See [portmind.config.example.yaml](./portmind.config.example.yaml) for a copy-and-edit starting point.
|
|
72
95
|
|
|
73
96
|
```yaml
|
|
74
97
|
scan:
|
|
@@ -114,7 +137,7 @@ logging:
|
|
|
114
137
|
audit_log: false
|
|
115
138
|
```
|
|
116
139
|
|
|
117
|
-
The
|
|
140
|
+
The TypeScript shape (`PortmindConfig`), its defaults, and the loader/merge logic live in [packages/core/src/config.ts](./packages/core/src/config.ts) and [packages/core/src/configLoader.ts](./packages/core/src/configLoader.ts). Not yet wired to config: `history.retention_days` (history isn't implemented), `risk_rules.*` (risk flags aren't implemented), `known_ports.refresh_days` (the bundled IANA cache doesn't auto-refresh yet - refreshing it means re-fetching the CSV and regenerating `packages/core/data/iana-cache.json`, which isn't automated).
|
|
118
141
|
|
|
119
142
|
## Architecture
|
|
120
143
|
|
|
@@ -123,7 +146,7 @@ packages/
|
|
|
123
146
|
├── core/ # scanning, enrichment, data model, config — no UI, no AI dependency
|
|
124
147
|
├── cli/ # table/JSON output over @portmind/core (implemented)
|
|
125
148
|
├── tui/ # live terminal dashboard — not started
|
|
126
|
-
├── web/ # local HTML dashboard
|
|
149
|
+
├── web/ # local HTML dashboard (implemented: /api/ports + static page)
|
|
127
150
|
└── ai/ # optional AI deep-search plugin — not started
|
|
128
151
|
```
|
|
129
152
|
|
|
@@ -133,16 +156,17 @@ packages/
|
|
|
133
156
|
|
|
134
157
|
Remaining phases, in build order:
|
|
135
158
|
|
|
136
|
-
1. **
|
|
137
|
-
2. **
|
|
138
|
-
3. **
|
|
139
|
-
4. **
|
|
140
|
-
5. **
|
|
141
|
-
6. **
|
|
142
|
-
7. **
|
|
143
|
-
8. **
|
|
144
|
-
9. **
|
|
145
|
-
|
|
159
|
+
1. **History and "usual" detection** — SQLite `observations`/`port_fingerprints` tables, `history <port>` command
|
|
160
|
+
2. **Docker cross-reference** — match `docker ps` output against scanned ports, populate `PortEntry.docker`
|
|
161
|
+
3. **Risk flags** — `bound_all_interfaces`, `unsigned_binary`, `no_known_service`, each independently configurable
|
|
162
|
+
4. **TUI** (`ink` or `blessed` — undecided) — live table with inline `explain`/`free`
|
|
163
|
+
5. **AI `explain`** (opt-in) — provider abstraction, explicit field allowlist, response caching (the web dashboard's "Explain with AI" button is wired up but disabled until this exists)
|
|
164
|
+
6. **SSH remote support** — `ssh_hosts` config, same `PortEntry` shape with `host` set to the remote name
|
|
165
|
+
7. **Audit logging** — log every `free`/`explain` action per `logging.audit_log`
|
|
166
|
+
8. **IANA cache auto-refresh** — automate re-fetching the CSV on `known_ports.refresh_days`, rather than the current bundled-once snapshot
|
|
167
|
+
9. **Polish** — full `--help` text, packaging for `npm install -g @portmind/cli`
|
|
168
|
+
|
|
169
|
+
Web dashboard, IANA enrichment, and the config file loader (were phases 6, 1, and 11 in the original spec numbering) are done — see [Status](#status).
|
|
146
170
|
|
|
147
171
|
Two decisions still open: TUI library (`ink` vs `blessed`), and whether `free` on a Docker-backed port needs anything beyond the interactive stop/kill/cancel prompt already agreed on.
|
|
148
172
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portmind-monorepo",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Local-first CLI + web dashboard that scans listening ports, enriches them with process/Docker/IANA detail, and remembers what normally runs where.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"port",
|
|
8
|
+
"port-scanner",
|
|
9
|
+
"lsof",
|
|
10
|
+
"docker",
|
|
11
|
+
"devtools",
|
|
12
|
+
"dev-server",
|
|
13
|
+
"port-conflict",
|
|
14
|
+
"eaddrinuse"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/psandis/portmind#readme",
|
|
17
|
+
"bugs": "https://github.com/psandis/portmind/issues",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/psandis/portmind.git"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "Petri Sandholm",
|
|
5
24
|
"packageManager": "pnpm@9.0.0",
|
|
6
25
|
"engines": {
|
|
7
26
|
"node": ">=22"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portmind/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Local-first CLI that scans listening ports, enriches them with process/Docker/IANA detail, and remembers what normally runs where.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@portmind/core": "workspace:*",
|
|
44
|
+
"@portmind/web": "workspace:*",
|
|
44
45
|
"commander": "^15.0.0"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
import {
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { ConfigError, loadConfig, resolveConfigPaths, scanPorts, type PortEntry } from "@portmind/core";
|
|
5
|
+
import { createWebServer } from "@portmind/web";
|
|
4
6
|
import { renderTable } from "./renderTable.js";
|
|
5
7
|
|
|
6
8
|
const program = new Command();
|
|
7
9
|
|
|
8
|
-
program.name("portmind").description("Local-first port scanning, enrichment and history tool").version("0.
|
|
10
|
+
program.name("portmind").description("Local-first port scanning, enrichment and history tool").version("0.3.0");
|
|
9
11
|
|
|
10
12
|
interface ListOptions {
|
|
11
13
|
range?: string;
|
|
@@ -24,7 +26,8 @@ program
|
|
|
24
26
|
.action(async (options: ListOptions) => {
|
|
25
27
|
try {
|
|
26
28
|
const range = parseRange(options.range);
|
|
27
|
-
|
|
29
|
+
const config = await loadConfig();
|
|
30
|
+
let entries = await scanPorts({ includeUdp: config.scan.includeUdp }, config.knownPorts);
|
|
28
31
|
|
|
29
32
|
if (range) {
|
|
30
33
|
entries = entries.filter((e) => e.port >= range.min && e.port <= range.max);
|
|
@@ -44,8 +47,74 @@ program
|
|
|
44
47
|
console.log(renderTable(entries));
|
|
45
48
|
}
|
|
46
49
|
} catch (error) {
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
if (error instanceof ConfigError) {
|
|
51
|
+
console.error(`portmind list: config error - ${error.message}`);
|
|
52
|
+
process.exitCode = 2;
|
|
53
|
+
} else {
|
|
54
|
+
console.error(`portmind list: scan failed - ${(error as Error).message}`);
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
interface WebOptions {
|
|
61
|
+
port: string;
|
|
62
|
+
open: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
program
|
|
66
|
+
.command("web")
|
|
67
|
+
.description("Start the local web dashboard")
|
|
68
|
+
.option("--port <port>", "port for the dashboard itself", "4400")
|
|
69
|
+
.option("--no-open", "don't open the browser automatically")
|
|
70
|
+
.action((options: WebOptions) => {
|
|
71
|
+
const port = Number.parseInt(options.port, 10);
|
|
72
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
73
|
+
console.error(`portmind web: invalid --port "${options.port}"`);
|
|
74
|
+
process.exitCode = 2;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
createWebServer({ port });
|
|
79
|
+
const url = `http://127.0.0.1:${port}`;
|
|
80
|
+
console.log(`portmind web: dashboard running at ${url}`);
|
|
81
|
+
|
|
82
|
+
if (options.open) {
|
|
83
|
+
const opener = process.platform === "darwin" ? "open" : "xdg-open";
|
|
84
|
+
execFile(opener, [url], (error) => {
|
|
85
|
+
if (error) {
|
|
86
|
+
console.error(`portmind web: could not open browser automatically (${error.message})`);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const config = program.command("config").description("Inspect resolved portmind configuration");
|
|
93
|
+
|
|
94
|
+
config
|
|
95
|
+
.command("show")
|
|
96
|
+
.description("Print the resolved config (defaults + user + project merged)")
|
|
97
|
+
.action(async () => {
|
|
98
|
+
try {
|
|
99
|
+
const resolved = await loadConfig();
|
|
100
|
+
console.log(JSON.stringify(resolved, null, 2));
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error(`portmind config show: ${(error as Error).message}`);
|
|
103
|
+
process.exitCode = 2;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
config
|
|
108
|
+
.command("path")
|
|
109
|
+
.description("Print the path(s) to the active config file(s) and whether they exist")
|
|
110
|
+
.action(async () => {
|
|
111
|
+
try {
|
|
112
|
+
const paths = await resolveConfigPaths();
|
|
113
|
+
console.log(`user: ${paths.user}${paths.userExists ? "" : " (not found)"}`);
|
|
114
|
+
console.log(`project: ${paths.project}${paths.projectExists ? "" : " (not found)"}`);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error(`portmind config path: ${(error as Error).message}`);
|
|
117
|
+
process.exitCode = 2;
|
|
49
118
|
}
|
|
50
119
|
});
|
|
51
120
|
|