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 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
- [![npm version](https://img.shields.io/npm/v/%40portmind%2Fcli.svg)](https://www.npmjs.com/package/@portmind/cli)
3
+ [![npm version](https://img.shields.io/npm/v/portmind-monorepo.svg)](https://www.npmjs.com/package/portmind-monorepo)
4
4
  [![node](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org)
5
5
  [![license](https://img.shields.io/badge/license-MIT-blue)](./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
- > The npm badge above will show "not found" until `@portmind/cli` is actually published. Nothing in this repo has been published yet.
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, IANA known-port descriptions, local history/"usual" detection, risk flags, `watch`/`free`/`explain`/`history`/`ssh`/`web`/`tui`/`config` commands, and the YAML config loader. Until those land, every `PortEntry.docker`, `.knownService`, and `.riskFlags` will be empty, and `.history.usual` is always `true`.
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 /opt/homebrew/var/postgresql@14
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
- Planned commands not yet implemented: `watch`, `free`, `explain`, `history`, `ssh <host> list|check`, `web`, `tui`, `config show|path`.
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
- Not implemented yet — there is no config loader, and `~/.portmind/config.yaml` / `.portmind.yaml` are not read. The schema below is the target design (resolution order: built-in defaults → `~/.portmind/config.yaml` → `.portmind.yaml` in the current directory, each layer overriding the previous):
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 equivalent TypeScript shape (`PortmindConfig`) and its defaults already exist in [packages/core/src/config.ts](./packages/core/src/config.ts) — only the file-loading/merge logic is missing.
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 — not started
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. **IANA enrichment** — fetch/cache the official IANA Service Name and Port Number Registry, populate `PortEntry.knownService`
137
- 2. **History and "usual" detection** — SQLite `observations`/`port_fingerprints` tables, `history <port>` command
138
- 3. **Docker cross-reference** — match `docker ps` output against scanned ports, populate `PortEntry.docker`
139
- 4. **Risk flags** — `bound_all_interfaces`, `unsigned_binary`, `no_known_service`, each independently configurable
140
- 5. **TUI** (`ink` or `blessed` — undecided) — live table with inline `explain`/`free`
141
- 6. **Web dashboard** — local-only HTTP server, `/api/ports`, static HTML/JS frontend
142
- 7. **AI `explain`** (opt-in) — provider abstraction, explicit field allowlist, response caching
143
- 8. **SSH remote support** — `ssh_hosts` config, same `PortEntry` shape with `host` set to the remote name
144
- 9. **Config system** — YAML loader/merge (defaults → user → project), `config show`/`config path`, audit logging for `free`/`explain`
145
- 10. **Polish** — full `--help` text, packaging for `npm install -g @portmind/cli`
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.1.0",
4
- "description": "portmind - local-first port scanning, enrichment and history tool",
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.1.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 { DEFAULT_CONFIG, scanPorts, type PortEntry } from "@portmind/core";
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.1.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
- let entries = await scanPorts({ includeUdp: DEFAULT_CONFIG.scan.includeUdp });
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
- console.error(`portmind list: scan failed - ${(error as Error).message}`);
48
- process.exitCode = 1;
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