npm package: @forgefield/portlens
Description: Scan local projects for configured service ports.
Latest version: 0.1.2

# PortLens

![PortLens - Logo](https://ik.imagekit.io/angshumanroy/@forgefield-portlens/portlens-logo_-rB-4tNpk.png)

`@forgefield/portlens` is a CLI tool for scanning local project directories, discovering configured service ports from env files, and detecting port collisions across services.

Repository: `https://github.com/forgefield/portlens`

## Why PortLens

- You have multiple microservices under one root folder (monorepo/workspace), each with its own `.env` file, and need a quick way to see all configured ports in one place.
- Your team keeps hitting "address already in use" errors; PortLens surfaces duplicate port assignments early so you can fix collisions before running services.
- You are onboarding to an unfamiliar codebase and need to understand which services are expected to run, what ports they use, and whether they are currently running locally.
- You want to organize services by top-level repository folder and inspect or update configuration in a local UI instead of manually checking dozens of files.

## Requirements

- Node.js `>=18` (from package engines)
- macOS/Linux/Windows supported
- For port status checks:
  - macOS/Linux uses `lsof`
  - Windows uses `netstat` + `findstr`

## Installation

### One-off (no install)

```bash
npx @forgefield/portlens
```

### Global install

```bash
npm install -g @forgefield/portlens
portlens
```

### Local project install

```bash
npm install --save-dev @forgefield/portlens
npx portlens
```

## Quick Start

Scan the current directory:

```bash
portlens
```

Scan a specific root:

```bash
portlens /path/to/workspace
```

Show only collisions:

```bash
portlens --collisions
```

Machine-readable JSON:

```bash
portlens --json
```

Start UI mode:

```bash
portlens ui
```

## CLI Reference

Usage:

```bash
portlens [rootPath] [--config <path>] [--json] [--collisions]
portlens ui [rootPath] [--config <path>]
```

### Positional arguments

- `rootPath` (optional): root directory to scan. Defaults to `process.cwd()`.

### Commands

- `ui`: starts the local PortLens UI server and opens your default browser.

### Flags

- `--json`: print JSON instead of console tables.
- `--collisions`: limit output to collision results.
- `--config <path>`: use a custom config file path (resolved relative to current shell directory).

### Behavior notes

- `--json --collisions` outputs only `collisionSummary`.
- Without `--json`, `--collisions` prints only the collision text report.
- Unknown flags are ignored by the current parser (no hard failure).

## How Scanning Works

1. PortLens recursively walks the target `rootPath`.
2. It skips ignored directories (`node_modules`, `.git`, `dist`, `build` by default).
3. It looks for configured env filenames (default list shown below).
4. It extracts the first matching configured port variable from each matching env file.
5. It groups findings by top-level directory under `rootPath`.
6. It marks collisions when the same port appears in multiple services.
7. It checks runtime status of each port (`running` or `stopped`) using OS-specific commands.

## Configuration

PortLens supports JSON config with these keys:

- `envFilePatterns`: env file names/patterns to scan.
- `portVariablePatterns`: env variable names/patterns to treat as ports.
- `ignoredDirectories`: directory names to skip.

### Default config lookup (CLI)

If you do not pass `--config`, the CLI uses:

```text
<rootPath>/ports.config.json
```

Where `<rootPath>` is:

- the positional `rootPath` argument, or
- the current directory if `rootPath` is omitted.

### Custom config path

```bash
portlens --config ./config/ports.config.json
portlens /path/to/root --config ../shared/ports.config.json
```

### Example config

```json
{
  "envFilePatterns": [
    ".env.local",
    ".env.development",
    ".env.production",
    ".env"
  ],
  "portVariablePatterns": [
    "PORT",
    "VITE_PORT",
    "NEXT_PUBLIC_PORT",
    "REACT_APP_PORT"
  ],
  "ignoredDirectories": ["node_modules", ".git", "dist", "build"]
}
```

Schema file:

- `config/ports.config.schema.json`

Example file:

- `config/ports.config.example.json`

## Output Contract

### Standard mode (`portlens`)

- Prints a collisions section first.
- Prints per-repo service tables with columns:
  - `Service`
  - `Type`
  - `Port`
  - `Status`
  - `Collision`
  - `Location`
  - `From`

### JSON mode (`portlens --json`)

Returns an object with:

- `rootPath`
- `servicesByRepo`
- `allServices`
- `portMap`
- `collisions`
- `collisionSummary`

Representative shape:

```json
{
  "rootPath": "/workspace",
  "servicesByRepo": {
    "repo-a": [
      {
        "Repo": "repo-a",
        "Service": "frontend",
        "Type": "frontend",
        "Port": "3000",
        "From": ".env (PORT)",
        "Status": "running",
        "Location": "apps/frontend",
        "Collision": true,
        "collision": true
      }
    ]
  },
  "allServices": [
    {
      "Repo": "repo-a",
      "Service": "frontend",
      "Type": "frontend",
      "Port": "3000",
      "From": ".env (PORT)",
      "Status": "running",
      "Location": "apps/frontend",
      "Collision": true,
      "collision": true
    },
    {
      "Repo": "repo-b",
      "Service": "backend",
      "Type": "backend",
      "Port": "3000",
      "From": ".env (PORT)",
      "Status": "stopped",
      "Location": "services/backend",
      "Collision": true,
      "collision": true
    }
  ],
  "portMap": {
    "3000": [
      {
        "Repo": "repo-a",
        "Service": "frontend"
      },
      {
        "Repo": "repo-b",
        "Service": "backend"
      }
    ]
  },
  "collisions": [
    {
      "port": "3000",
      "services": [
        {
          "Repo": "repo-a",
          "Service": "frontend"
        },
        {
          "Repo": "repo-b",
          "Service": "backend"
        }
      ]
    }
  ],
  "collisionSummary": {
    "count": 1,
    "byPort": {
      "3000": [
        {
          "Repo": "repo-a",
          "Service": "frontend"
        },
        {
          "Repo": "repo-b",
          "Service": "backend"
        }
      ]
    }
  }
}
```

### Collisions JSON only (`portlens --json --collisions`)

Returns only:

```json
{
  "count": 1,
  "byPort": {
    "3000": [
      {
        "Repo": "repo-a",
        "Service": "frontend"
      }
    ]
  }
}
```

## UI Mode

Run:

```bash
portlens ui
```

What it does:

- Starts a local HTTP server on an ephemeral port.
- Opens your browser automatically:
  - `open` on macOS
  - `start` on Windows
  - `xdg-open` on Linux
- Serves the built UI from `ui/dist`.

UI/API endpoints currently served by the local server:

- `GET /data` and `GET /api/scan`: scan results + merged config
- `GET /config`: current merged config
- `POST /config`: persist config changes

If UI assets are missing, you will see:

```text
UI build not found. Run npm run build:ui.
```

## Common Workflows

Scan current project:

```bash
portlens
```

Scan monorepo root:

```bash
portlens ~/code/my-monorepo
```

Export full JSON for tooling:

```bash
portlens ~/code/my-monorepo --json > portlens-report.json
```

CI-friendly collision check artifact:

```bash
portlens ~/code/my-monorepo --json --collisions > collisions.json
```

## Screenshots

### CLI screenshots

![CLI - Default Scan Output](https://ik.imagekit.io/angshumanroy/@forgefield-portlens/cli-portlens_zc0A9d-lK.png)

![CLI - Collisions Output](https://ik.imagekit.io/angshumanroy/@forgefield-portlens/cli-portlens-with-collisions_OsqScWY6x.png)

![CLI - JSON Output](https://ik.imagekit.io/angshumanroy/@forgefield-portlens/cli-portlens-json_loL1NRzDI.png)

![CLI - JSON Output Collisions](https://ik.imagekit.io/angshumanroy/@forgefield-portlens/cli-portlens-json-collisions_EC_9gV5ff.png)

### UI screenshots

![UI - Dashboard Overview](https://ik.imagekit.io/angshumanroy/@forgefield-portlens/portlens-ui_1uBiOnetv.png)

![UI - Config Editor](https://ik.imagekit.io/angshumanroy/@forgefield-portlens/portlens-ui-settings_yaHkWNe3EU.png)

![UI - Filters](https://ik.imagekit.io/angshumanroy/@forgefield-portlens/portlens-ui-filters_Qt867Zag5L.png)

![UI - Filters Applied](https://ik.imagekit.io/angshumanroy/@forgefield-portlens/portlens-ui-filters-applied_R_qTm16HL.png)

![UI - Collision View](https://ik.imagekit.io/angshumanroy/@forgefield-portlens/portlens-ui-collisions_wpfSakutI.png)

![UI - Collision View - Collision Only Filter Applied](https://ik.imagekit.io/angshumanroy/@forgefield-portlens/portlens-ui-collisions-only-filter_Hw9GIWMGk-.png)

## Troubleshooting

### No services found

- Confirm env files exist and match `envFilePatterns`.
- Confirm port variable names match `portVariablePatterns`.
- Confirm folders are not excluded by `ignoredDirectories`.

### Port status looks incorrect

- Ensure `lsof` (macOS/Linux) or `netstat`/`findstr` (Windows) is available.
- Status is determined at scan time; rerun if services started/stopped recently.

### UI command fails to render app

- Build UI assets first:

```bash
npm run build:ui
```

### Invalid config payload in UI

- `POST /config` expects non-empty string arrays for:
  - `envFilePatterns`
  - `portVariablePatterns`

## Development And Release

This repository currently uses:

- Bun for dependency installation
- npm for build and packaging scripts

Install dependencies:

```bash
bun install
```

Build UI assets:

```bash
npm run build:ui
```

Verify package contents before publish:

```bash
npm pack --dry-run
```

## Contributing Workflow

For external contributors, please follow a fork-based workflow.

1. Fork the repository to your GitHub account.
2. Clone your fork locally.
3. Create a feature branch for your change.
4. Make and test your changes locally.
5. Push your branch to your fork.
6. Open a Pull Request from your fork branch to the main repository.

This keeps the upstream repository clean and makes reviews easier to manage.

## Security And Privacy

- PortLens scans local files under the path you provide.
- It inspects env file contents for configured port variables.
- It performs local OS port checks.
- It does not require outbound network access for scanning itself.

## License

MIT. See `LICENSE`.