portmind-monorepo 0.1.0 → 0.3.1
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 +42 -0
- package/README.md +53 -20
- package/package.json +21 -2
- package/packages/cli/package.json +2 -1
- package/packages/cli/src/index.ts +116 -5
- package/packages/core/data/iana-cache.json +1 -0
- package/packages/core/package.json +5 -3
- package/packages/core/src/ai/allowlist.ts +38 -0
- package/packages/core/src/ai/cache.ts +54 -0
- package/packages/core/src/ai/explain.ts +47 -0
- package/packages/core/src/ai/provider.ts +91 -0
- package/packages/core/src/ai/sanitize.ts +19 -0
- package/packages/core/src/configLoader.ts +140 -0
- package/packages/core/src/index.ts +13 -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/aiCache.test.ts +38 -0
- package/packages/core/tests/allowlist.test.ts +63 -0
- package/packages/core/tests/configLoader.test.ts +54 -0
- package/packages/core/tests/explain.test.ts +119 -0
- package/packages/core/tests/lookup.test.ts +87 -0
- package/packages/core/tests/parseIana.test.ts +47 -0
- package/packages/core/tests/provider.test.ts +72 -0
- package/packages/core/tests/sanitize.test.ts +25 -0
- package/packages/web/package.json +44 -0
- package/packages/web/src/index.ts +1 -0
- package/packages/web/src/server.ts +85 -0
- package/packages/web/static/index.html +354 -0
- package/packages/web/tests/server.test.ts +48 -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,48 @@ 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.1] - 2026-09-06
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `@portmind/core`: AI `explain` feature - provider abstraction for Anthropic (`/v1/messages`) and OpenAI (`/v1/chat/completions`), selected via `ai.provider` config with no code changes needed to switch; explicit field allowlist enforcement (`buildExplainPayload`, only sends fields named in `ai.fields_sent`); cmdline secret/token redaction (`sanitizeCmdline`) before anything is sent; SQLite response cache (`ai_explanations` table) keyed by `(process_name, port)` so repeat queries don't re-call the API.
|
|
13
|
+
- `@portmind/cli`: `portmind explain <port>` - opt-in, requires `ai.enabled: true` and an API key in `ANTHROPIC_API_KEY`/`OPENAI_API_KEY`; fails with a clear config error (exit code `2`) rather than doing nothing if unconfigured.
|
|
14
|
+
- `@portmind/web`: `POST /api/explain` endpoint; the dashboard's "Explain with AI" button now makes a real request instead of showing a disabled placeholder.
|
|
15
|
+
- Web dashboard UX fixes: sticky header (title, filter toolbar, and column labels stay pinned while scrolling a long port list) and the detail panel is now a fixed slide-in panel from the right edge with a dimmed overlay, appearing instantly regardless of scroll position, instead of a block at the bottom of the page requiring a scroll to find. The panel now shows "Service name" and "Description" as two separate labeled fields.
|
|
16
|
+
- Vitest coverage: cmdline sanitization patterns, allowlist field selection, provider request-shape verification (mocked `fetch`, exact URL/headers/body per each provider's documented API), cache get/set/overwrite, and the full `explainPort` orchestration (disabled, missing key, cache hit, cache miss - all mocked).
|
|
17
|
+
|
|
18
|
+
### Known limitations
|
|
19
|
+
- **AI explain has not been tested against a real API key.** Every test mocks the HTTP layer; the request shapes match each provider's documented API as of this writing, but end-to-end correctness against the actual Anthropic/OpenAI services is unverified.
|
|
20
|
+
- The table's DESCRIPTION column and detail panel's "Description" field only populate for ports IANA has actually registered - most macOS system/app processes (e.g. rapportd, ControlCenter, Spotify) aren't registered services, so they correctly show "-" or, for genuinely coincidental port-number matches, an unrelated IANA description for that number.
|
|
21
|
+
|
|
22
|
+
## [0.3.0] - 2026-09-06
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- `@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").
|
|
26
|
+
- `@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).
|
|
27
|
+
- `@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).
|
|
28
|
+
- `portmind.config.example.yaml` at repo root - copy-and-edit starting point for either config location.
|
|
29
|
+
- `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.
|
|
30
|
+
- 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).
|
|
31
|
+
|
|
32
|
+
### Known limitations
|
|
33
|
+
- `known_ports.refresh_days` is not enforced - the bundled IANA cache is a one-time snapshot (fetched 2026-09-06), not auto-refreshed.
|
|
34
|
+
- `history.retention_days` and `risk_rules.*` are defined in config but not yet consumed - history and risk flags aren't implemented.
|
|
35
|
+
- `logging.audit_log` is defined but nothing writes to it yet - there's no `free`/`explain` command to audit.
|
|
36
|
+
|
|
37
|
+
## [0.2.0] - 2026-09-06
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- `@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.
|
|
41
|
+
- `@portmind/cli`: `portmind web` command (`--port`, `--no-open`) that starts the dashboard and opens the default browser.
|
|
42
|
+
- Vitest coverage for the web server: page route, API route, and 404 handling.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
- 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.
|
|
46
|
+
|
|
47
|
+
### Known limitations
|
|
48
|
+
- 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`).
|
|
49
|
+
- `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.
|
|
50
|
+
|
|
9
51
|
## [0.1.0] - 2026-09-06
|
|
10
52
|
|
|
11
53
|
### 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,13 @@ 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)
|
|
22
|
+
- AI `explain` (opt-in, off by default) — `portmind explain <port>` and the web dashboard's "Explain with AI" button both make a real call to Anthropic or OpenAI (your choice, via `ai.provider`), sending only the explicit field allowlist (never full env vars or file contents), with cmdline secrets/tokens redacted first and results cached in SQLite so repeat queries don't re-call the API. **Not tested against a live API key** — the request-construction logic (allowlist, sanitization, exact payload shape) is unit-tested with a mocked HTTP layer, but no real call to Anthropic/OpenAI has been made to confirm end-to-end correctness.
|
|
18
23
|
|
|
19
|
-
**Not implemented yet** (see [Next steps](#next-steps)): Docker cross-reference,
|
|
24
|
+
**Not implemented yet** (see [Next steps](#next-steps)): Docker cross-reference, local history/"usual" detection, risk flags, `watch`/`free`/`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
25
|
|
|
21
26
|
See [CHANGELOG.md](./CHANGELOG.md) for a dated record of what shipped when.
|
|
22
27
|
|
|
@@ -55,20 +60,47 @@ Example output:
|
|
|
55
60
|
```
|
|
56
61
|
PORT PROTO PROCESS PID DOCKER USUAL NOTE
|
|
57
62
|
3000 tcp node 41822 - yes /Users/you/Projects/react-dashboard
|
|
58
|
-
5432 tcp postgres 1758 - yes
|
|
63
|
+
5432 tcp postgres 1758 - yes PostgreSQL Database
|
|
59
64
|
```
|
|
60
65
|
|
|
61
66
|
**Exit codes:** `0` success, `1` scan error, `2` config error.
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
### Web dashboard
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
portmind web # starts on http://127.0.0.1:4400 and opens your browser
|
|
72
|
+
portmind web --port 4401 # use a different port for the dashboard itself
|
|
73
|
+
portmind web --no-open # don't open the browser automatically
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Why a plain server + vanilla JS instead of a framework: the dashboard is three routes (the page, `/api/ports`, `/api/explain`), it never leaves your machine except for the explicit opt-in AI call, 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 opens a fixed panel from the right edge of the screen (with a dimmed overlay behind it) showing full detail — no scrolling required regardless of where in the table you clicked. Its "Explain with AI" button only fires `/api/explain` when clicked, never automatically.
|
|
77
|
+
|
|
78
|
+
### AI explain
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
portmind explain 5432 # runs AI deep-search for whatever is currently on port 5432
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Requires `ai.enabled: true` in config and an API key in the matching environment variable (`ANTHROPIC_API_KEY` or `OPENAI_API_KEY` depending on `ai.provider` — never put the key in the config file itself). Disabled by default; both the CLI command and the web button fail with a clear config error (exit code `2` for the CLI) rather than silently doing nothing if it's not configured. Results are cached in SQLite keyed by `(process_name, port)`, so asking about the same shape of thing twice doesn't re-call the API.
|
|
85
|
+
|
|
86
|
+
### Config
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
portmind config show # print the fully resolved config (defaults + user + project merged), as JSON
|
|
90
|
+
portmind config path # print ~/.portmind/config.yaml and ./.portmind.yaml, and whether each exists
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
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.
|
|
94
|
+
|
|
95
|
+
Planned commands not yet implemented: `watch`, `free`, `history`, `ssh <host> list|check`, `tui`.
|
|
64
96
|
|
|
65
97
|
## Storage
|
|
66
98
|
|
|
67
|
-
History will be stored locally in a SQLite database at `~/.portmind/portmind.db` (configurable) once the history feature (Phase 4) lands. Nothing leaves the machine unless you explicitly run `portmind explain <port>` with AI enabled in config, and even then only an explicit allowlist of fields is sent — never full environment variables or file contents.
|
|
99
|
+
History will be stored locally in a SQLite database at `~/.portmind/portmind.db` (configurable) once the history feature (Phase 4) lands - the same db file already holds the `ai_explanations` cache table today. Nothing leaves the machine unless you explicitly run `portmind explain <port>` (or click "Explain with AI") with AI enabled in config, and even then only an explicit allowlist of fields is sent — never full environment variables or file contents, and cmdline values are scanned for secret/token-shaped substrings and redacted before sending.
|
|
68
100
|
|
|
69
101
|
## Configuration
|
|
70
102
|
|
|
71
|
-
|
|
103
|
+
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
104
|
|
|
73
105
|
```yaml
|
|
74
106
|
scan:
|
|
@@ -114,7 +146,7 @@ logging:
|
|
|
114
146
|
audit_log: false
|
|
115
147
|
```
|
|
116
148
|
|
|
117
|
-
The
|
|
149
|
+
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
150
|
|
|
119
151
|
## Architecture
|
|
120
152
|
|
|
@@ -123,7 +155,7 @@ packages/
|
|
|
123
155
|
├── core/ # scanning, enrichment, data model, config — no UI, no AI dependency
|
|
124
156
|
├── cli/ # table/JSON output over @portmind/core (implemented)
|
|
125
157
|
├── tui/ # live terminal dashboard — not started
|
|
126
|
-
├── web/ # local HTML dashboard
|
|
158
|
+
├── web/ # local HTML dashboard (implemented: /api/ports + static page)
|
|
127
159
|
└── ai/ # optional AI deep-search plugin — not started
|
|
128
160
|
```
|
|
129
161
|
|
|
@@ -133,16 +165,17 @@ packages/
|
|
|
133
165
|
|
|
134
166
|
Remaining phases, in build order:
|
|
135
167
|
|
|
136
|
-
1. **
|
|
137
|
-
2. **
|
|
138
|
-
3. **
|
|
139
|
-
4. **
|
|
140
|
-
5. **
|
|
141
|
-
6. **
|
|
142
|
-
7. **
|
|
143
|
-
8. **
|
|
144
|
-
9. **
|
|
145
|
-
|
|
168
|
+
1. **History and "usual" detection** — SQLite `observations`/`port_fingerprints` tables, `history <port>` command
|
|
169
|
+
2. **Docker cross-reference** — match `docker ps` output against scanned ports, populate `PortEntry.docker`
|
|
170
|
+
3. **Risk flags** — `bound_all_interfaces`, `unsigned_binary`, `no_known_service`, each independently configurable
|
|
171
|
+
4. **TUI** (`ink` or `blessed` — undecided) — live table with inline `explain`/`free`
|
|
172
|
+
5. **SSH remote support** — `ssh_hosts` config, same `PortEntry` shape with `host` set to the remote name
|
|
173
|
+
6. **Audit logging** — log every `free`/`explain` action per `logging.audit_log`
|
|
174
|
+
7. **IANA cache auto-refresh** — automate re-fetching the CSV on `known_ports.refresh_days`, rather than the current bundled-once snapshot
|
|
175
|
+
8. **Polish** — full `--help` text, packaging for `npm install -g @portmind/cli`
|
|
176
|
+
9. **AI explain: real end-to-end verification** — the feature is built and unit-tested with a mocked HTTP layer, but has never actually been called against Anthropic or OpenAI with a real API key. Verifying that happens whenever a key becomes available to test with.
|
|
177
|
+
|
|
178
|
+
Web dashboard, IANA enrichment, the config file loader, and AI `explain` (untested end-to-end - see above) are done — see [Status](#status).
|
|
146
179
|
|
|
147
180
|
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
181
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portmind-monorepo",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.1",
|
|
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
|
|
3
|
+
"version": "0.3.1",
|
|
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,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
import {
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import {
|
|
5
|
+
ConfigError,
|
|
6
|
+
explainPort,
|
|
7
|
+
loadConfig,
|
|
8
|
+
resolveConfigPaths,
|
|
9
|
+
scanPorts,
|
|
10
|
+
type PortEntry,
|
|
11
|
+
} from "@portmind/core";
|
|
12
|
+
import { createWebServer } from "@portmind/web";
|
|
4
13
|
import { renderTable } from "./renderTable.js";
|
|
5
14
|
|
|
6
15
|
const program = new Command();
|
|
7
16
|
|
|
8
|
-
program.name("portmind").description("Local-first port scanning, enrichment and history tool").version("0.1
|
|
17
|
+
program.name("portmind").description("Local-first port scanning, enrichment and history tool").version("0.3.1");
|
|
9
18
|
|
|
10
19
|
interface ListOptions {
|
|
11
20
|
range?: string;
|
|
@@ -24,7 +33,8 @@ program
|
|
|
24
33
|
.action(async (options: ListOptions) => {
|
|
25
34
|
try {
|
|
26
35
|
const range = parseRange(options.range);
|
|
27
|
-
|
|
36
|
+
const config = await loadConfig();
|
|
37
|
+
let entries = await scanPorts({ includeUdp: config.scan.includeUdp }, config.knownPorts);
|
|
28
38
|
|
|
29
39
|
if (range) {
|
|
30
40
|
entries = entries.filter((e) => e.port >= range.min && e.port <= range.max);
|
|
@@ -44,8 +54,109 @@ program
|
|
|
44
54
|
console.log(renderTable(entries));
|
|
45
55
|
}
|
|
46
56
|
} catch (error) {
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
if (error instanceof ConfigError) {
|
|
58
|
+
console.error(`portmind list: config error - ${error.message}`);
|
|
59
|
+
process.exitCode = 2;
|
|
60
|
+
} else {
|
|
61
|
+
console.error(`portmind list: scan failed - ${(error as Error).message}`);
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
interface WebOptions {
|
|
68
|
+
port: string;
|
|
69
|
+
open: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
program
|
|
73
|
+
.command("web")
|
|
74
|
+
.description("Start the local web dashboard")
|
|
75
|
+
.option("--port <port>", "port for the dashboard itself", "4400")
|
|
76
|
+
.option("--no-open", "don't open the browser automatically")
|
|
77
|
+
.action((options: WebOptions) => {
|
|
78
|
+
const port = Number.parseInt(options.port, 10);
|
|
79
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
80
|
+
console.error(`portmind web: invalid --port "${options.port}"`);
|
|
81
|
+
process.exitCode = 2;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
createWebServer({ port });
|
|
86
|
+
const url = `http://127.0.0.1:${port}`;
|
|
87
|
+
console.log(`portmind web: dashboard running at ${url}`);
|
|
88
|
+
|
|
89
|
+
if (options.open) {
|
|
90
|
+
const opener = process.platform === "darwin" ? "open" : "xdg-open";
|
|
91
|
+
execFile(opener, [url], (error) => {
|
|
92
|
+
if (error) {
|
|
93
|
+
console.error(`portmind web: could not open browser automatically (${error.message})`);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
program
|
|
100
|
+
.command("explain")
|
|
101
|
+
.description("Run AI deep-search for a specific port (opt-in, requires ai.enabled: true)")
|
|
102
|
+
.argument("<port>", "port number to explain")
|
|
103
|
+
.action(async (portArg: string) => {
|
|
104
|
+
const port = Number.parseInt(portArg, 10);
|
|
105
|
+
if (Number.isNaN(port)) {
|
|
106
|
+
console.error(`portmind explain: invalid port "${portArg}"`);
|
|
107
|
+
process.exitCode = 2;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const resolvedConfig = await loadConfig();
|
|
113
|
+
const entries = await scanPorts({ includeUdp: resolvedConfig.scan.includeUdp }, resolvedConfig.knownPorts);
|
|
114
|
+
const entry = entries.find((e) => e.port === port);
|
|
115
|
+
if (!entry) {
|
|
116
|
+
console.error(`portmind explain: nothing is currently listening on port ${port}`);
|
|
117
|
+
process.exitCode = 1;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const explanation = await explainPort(entry, resolvedConfig);
|
|
122
|
+
console.log(explanation);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error instanceof ConfigError) {
|
|
125
|
+
console.error(`portmind explain: ${error.message}`);
|
|
126
|
+
process.exitCode = 2;
|
|
127
|
+
} else {
|
|
128
|
+
console.error(`portmind explain: failed - ${(error as Error).message}`);
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const config = program.command("config").description("Inspect resolved portmind configuration");
|
|
135
|
+
|
|
136
|
+
config
|
|
137
|
+
.command("show")
|
|
138
|
+
.description("Print the resolved config (defaults + user + project merged)")
|
|
139
|
+
.action(async () => {
|
|
140
|
+
try {
|
|
141
|
+
const resolved = await loadConfig();
|
|
142
|
+
console.log(JSON.stringify(resolved, null, 2));
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error(`portmind config show: ${(error as Error).message}`);
|
|
145
|
+
process.exitCode = 2;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
config
|
|
150
|
+
.command("path")
|
|
151
|
+
.description("Print the path(s) to the active config file(s) and whether they exist")
|
|
152
|
+
.action(async () => {
|
|
153
|
+
try {
|
|
154
|
+
const paths = await resolveConfigPaths();
|
|
155
|
+
console.log(`user: ${paths.user}${paths.userExists ? "" : " (not found)"}`);
|
|
156
|
+
console.log(`project: ${paths.project}${paths.projectExists ? "" : " (not found)"}`);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error(`portmind config path: ${(error as Error).message}`);
|
|
159
|
+
process.exitCode = 2;
|
|
49
160
|
}
|
|
50
161
|
});
|
|
51
162
|
|