Skip to content
n6-studioPublic

About

VS Code / Cursor extension: git, ports and dev servers for every repo and lane in your workspace — discovered, no config file.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Immu

A VS Code / Cursor extension for the folder you actually have open: git state, ports, and product dev servers — the same overview as the Immu macOS widget, scoped to the workspace.

Immu lives in an editor tab. Open it with the Immu icon in the editor title bar (next to split editor), the status-bar item, or Immu: Open. The tab is restored after a window reload.

  • Overview: one card per project, showing branch, changes, ahead/behind, and which dev servers are up. Each card has Pull and Stop buttons, and the header has Pull all, Fetch all and Stop all for the whole workspace.
  • Project page: click a card. The same cards stay in a scrollable sidebar on the left, so you can switch projects in one click. The page shows git (Fetch / Pull / Push and the last git output) and every server, each with Start / Stop, its terminal, its scripts and its ports. Stop all stops every server of that project.
  • Press / to filter projects and Esc to go back to the overview.
  • Each product lists its ports grouped by the command that starts them: pnpm pro dev with Dashboard and Web App, pnpm pro storybook with Storybook. Each group has its own Start / Stop, so you always know what a button runs (see Commands and processes).
  • Start runs that command in an editor terminal. It shows orange while starting and green once ready. Stop sends Ctrl+C, stops the whole process tree the command started, and frees its ports. A command started outside Immu (another terminal, tmux) shows as Up and can be stopped too. Hover a port and click × to kill just the process listening on it.
  • While the Immu tab is visible it refreshes every 30 seconds: files, git, and what is running. Nothing is scanned while it is hidden.
  • There is no config file. Immu finds repos, lanes, dev commands and ports in the files the project already has (see Discovery).

Install

From Open VSX (Cursor, VSCodium, …): search Immu in the Extensions panel, or

cursor --install-extension immu.immu

It updates itself like any marketplace extension.

From a GitHub Release (VS Code, or a specific version):

gh release download -R n6-studio/immu --pattern '*.vsix' --clobber -D /tmp
cursor --install-extension /tmp/immu-*.vsix --force   # or: code --install-extension …

From source: npm install, then npm run package and install the generated .vsix, or press F5 (Run Extension) to debug.

The Immu icon (2×2 grid) appears in the editor title bar. Click it, or the status bar item, to open the Immu tab.

Releasing

npm version patch        # bumps package.json and tags vX.Y.Z
git push --follow-tags

The Release workflow checks the tag matches package.json, runs the tests, builds the .vsix, attaches it to a GitHub Release and publishes it to Open VSX (when the OVSX_PAT secret is set).

Remote SSH

Immu is a workspace extension, so in a Cursor / VS Code Remote-SSH window it runs on the remote machine: git, port checks, and ▶ Dev terminals all happen on the server, next to the code. Opening an endpoint goes through the editor's port forwarding, so localhost:4400 on the server opens in your local browser.

Install it on the remote once per host:

  1. Connect to the host and open your project folder.
  2. In the remote window, install Immu from the Extensions panel (Open VSX), or run one of the install commands above in its integrated terminal.
  3. Open the project (or the parent folder of its lanes). Nothing else to set up.

Linux notes: commands run in $SHELL (falling back to bash / sh). Processes, listening ports and working directories are read straight from /proc, so nothing (lsof, ss, procps) has to be installed; only your own user's processes are visible, as with ss -p.

Discovery

Immu reads what is already on disk. Nothing is written into your repos.

Projects and lanes. An opened folder that is a git repo is one project. A folder that is not a repo shows every git repo directly inside it (_* and dot folders are skipped). Repos that share an origin are lanes of one family: acme-1, acme-3 → lanes 1 and 3, with neighbouring colors and A1 / A3 badges. Opening a single lane still knows its number.

Products and dev commands, first match wins:

  1. Root scripts that only forward to one workspace package — "pro": "vp run --filter 'acme-pro'" (also pnpm --filter, -F, turbo --filter, yarn workspace, npm -w) → product Pro, command pnpm pro dev.
  2. Otherwise one product per workspace package (pnpm-workspace.yaml or package.json workspaces) with a dev script → pnpm --filter <name> dev.
  3. Otherwise the root dev script → one product named after the package.
  4. Otherwise one product per folder directly inside the repo that has its own package.json with a dev script — a front/ next to a Python backend → product Front, command pnpm -C front dev (npm --prefix front run dev, cd front && yarn dev).

The dev script is the first of dev, dev:local, start:dev, develop, serve, start, storybook, so a component library with only a Storybook is a product too. The package manager comes from packageManager, then the lockfile.

A product's other server scripts — any script with dev, develop, start, serve, server, storybook, preview, mock or emulators in its name (docs:dev, dev:mock), but not build:dev or test:start — are offered under ⋯ → Run a script and get their own ports.

Ports and URLs, with the source shown in every tooltip:

  1. PORT_*, *_PORT and PORT from .env < .env.development < .env.local < .env.development.local (later wins, ${VAR} expanded). PORT_PRO_DASHBOARD → Pro › Dashboard, PORT_STORYBOOK_PRO → Pro › Storybook; the rest goes to Platform. The same files inside a product's own package count for that product: PORT=3001 in apps/web/.env.local → Web, STORYBOOK_PORT there → Web › Storybook; $VAR in that package's scripts and configs is read from its own files first.
  2. Ports in the dev script chain: --port 3000, -p $PORT_X, --listen localhost:3000, PORT=8080, STORYBOOK_PORT=6006, serve -l 3000, including nested commands such as convex dev --start 'vite dev --port 3000' or sh -c '…'. Then the ports the product's other server scripts ask for the same way: "dev:mock": "vite --port 3001" → Front › Dev Mock.
  3. http(s):// / ws(s):// *_URL values on localhost that point at a port nothing else declared.
  4. Private-network URLs (*.internal, *.localhost, *.local, *.test, private / Tailscale IPs) only when they replace a localhost value from a lower env file (PUBLIC_PLATFORM_IDENTITY_URL=https://id.ep1.internal in .env.local). Hosted environments such as https://id.dev.acme.dev and backends like VITE_CONVEX_URL stay out. These URLs usually sit behind a local proxy that always accepts connections, so Immu checks them with an HTTP request: a 502/503/504 from the proxy, or no answer at all, counts as down.
  5. Last resort: server.port in vite.config.* / astro.config.* (literal or Number(process.env.X ?? 4771)), or the default port of the CLI the dev script runs, by subcommand: vite 5173, vite preview 4173, next dev 3000, astro 4321, ng serve 4200, gatsby develop 8000, storybook dev 6006, wrangler dev 8787, parcel 1234… (vite build, next build and commands that pass their own --port serve no default). The full table is in src/discovery/frameworks.ts.

Backing services are not dev servers and stay out: DB_PORT, POSTGRES_PORT, REDIS_PORT, SMTP_PORT and the like, postgres:// / redis:// URLs, and a *_PORT whose *_HOST is another machine (SMTP_HOST=smtp.sendgrid.net).

The ready port of a product is its first plain port (Pro Dashboard before Pro Web App, never Storybook, mock ports or another script's ports).

Commands and processes

On every refresh (the button, or every 30s while the tab is visible) Immu reads every TCP listener and the process table (/proc on Linux; lsof and ps on macOS). From each listener it climbs the parent processes up to the command that was typed into a shell or run by a task, stopping at the shell, tmux, the terminal or the editor. vite under sst dev under pnpm pro dev belongs to pnpm pro dev. Runners are shown as you would type them (node …/pnpm.cjs pro dev → pnpm pro dev), and pnpm dev typed inside products/pro counts as the same script as pnpm pro dev at the root.

Each port of a product goes under a command, strongest evidence first:

  1. Running: the command it listens under right now. Ports no file declares (sst on 13557) are added to that product too.
  2. Learned: the command it was last seen under. Immu remembers this in editor storage, per repo and by port name rather than number, so one lane teaches the others. ⋯ → Forget learned commands clears it.
  3. Declared: before anything has run, Immu follows each script through the workspace — pnpm --filter / -F / -r / -C, yarn workspace, npm -w, turbo run, vp run, nx, lerna, run-p / npm-run-all, concurrently "npm:dev:*", cd dir &&, and wrappers such as cross-env, dotenv, npx — and looks at what the scripts it reaches say: the port's env var ($PORT_STORYBOOK_PRO), a literal port (--port 4460, PORT=4460), the same env var in the config of a tool the script runs (sst.config.ts for sst dev, vite.config.ts for vite, .storybook/main.ts for storybook, next / nuxt / astro / webpack / wrangler / compose…), or the default port of the framework it runs. The dev command wins a tie; scripts the dev command runs itself are not separate commands.
  4. Named: a script of the product named like the port (storybook ↔ Storybook, dev:mock ↔ Mock Convex), unless the dev script already runs that script.
  5. Otherwise the dev command. Once the dev command has been seen running without a port, that port moves to Not started by these commands instead.

SST. A script that runs sst dev — directly or through wrappers such as pnpm with-env sst dev, whose arguments pnpm appends to the script — is read through its sst.config.ts and the local modules it imports, without running them. Every new sst.<namespace>.<Type>("Name", { … }) with something to run in dev is listed under the command as sst dev starts: a sst.x.DevCommand with its dev.command / dev.directory / dev.title, a frontend (TanStackStart, Nextjs, Astro, Remix, SvelteKit, Nuxt, SolidStart, React, Analog) with its dev.command (npm run dev unless set) in its path. Each component's scripts are followed like any other, and a component serves the ports its dev.url points at: url: platformDevUrls.hqWebsite → devUrl("PUBLIC_PLATFORM_HQ_WEBSITE_URL", "http://localhost:4200") → that variable's port, or the local value it replaced when .env.local points it at a hosted stage.

Technologies. Each command, SST component and port shows icons (Simple Icons) for what it runs: SST, TanStack Start, Vite / Vite+, Next.js, Storybook, Convex, Bun, Node.js… — from the programs its scripts reach, the SST components it starts and the processes listening under it. Package managers and shell plumbing are not shown; Node.js only when nothing more specific runs. Processes are named by the command their package installs: node …/vite-plus/dist/cli.js dev shows as vp dev.

Aliases. A private URL (https://ep6.internal) is another name for the local port it replaced, so it shows as a chip under that port's row rather than as a server of its own. Hover a port for why it sits under its command (products/pro/sst.config.ts uses PORT_PRO_DASHBOARD, vite (pid 4521) listens under pnpm pro dev), the file it was found in, and which process listens on it — also when that process is not the project's: ControlCenter (pid 512), not started from this project is macOS AirPlay on 5000. Hover a product's name for its dev command, ready port and where it was found.

Lanes sharing ports. Lanes of one repo usually declare the same ports. When lane 1's server holds a port lane 2 declares too, lane 2 does not show as up: the port gets a hollow orange dot and in use by acme-1, and lane 2's Stop leaves it alone.

Stopping. Stop ends the command's whole process tree (SIGTERM, then SIGKILL after 2.5 s), then anything still on its ports. The tree is recorded before Ctrl+C reaches the terminal, so a runner that exits on Ctrl+C and orphans its children (sst dev does) does not take them out of reach, and it is re-read until nothing is left, so children spawned while shutting down go too. A launcher only counts when it runs inside the project, so a daemon such as pm2 or Docker is never taken for the command and never stopped as a whole. Freeing a port never kills infrastructure that holds it for something else — Docker / OrbStack / Podman forwarding a container, an ssh or kubectl tunnel, macOS ControlCenter / rapportd — Immu says what holds it instead. Before stopping a tree Immu checks the pid still runs the same command line, and it never signals the editor or its parents.

A leftover listening on a port (an orphaned vp dev still on :9200) stays under the command that declares the port, marked Left over from an earlier run; Stop ends it and its children. Leftovers are never learned as commands of their own.

Leftovers. Some tools leave processes running after they exit — sst dev's workers and the servers it started, adopted by launchd / init. After a Stop, Immu also stops orphans that run inside the command's directory (by working directory, or a path on their command line such as …/acme/node_modules/.bin/vite) and started during that run. Orphans left by earlier runs — a Ctrl+C in your own terminal — show on the project page as N leftover processes from earlier runs with Clean up; Stop all for a project stops them too. Apps, system daemons, agents (ssh-agent, gpg-agent), multiplexers (tmux, screen), git, the editor and infrastructure are never counted. npm run discover -- --running lists them.

When one command serves several products (a root pnpm dev running pnpm -r dev, turbo dev), each card says Also serves …, and Stop on one product stops only the branches of the tree that serve its ports. Some runners shut every child down once one exits (pnpm -r --parallel does, even with --no-bail); Immu checks afterwards and tells you when the runner took the others down too. Stop all stops such commands entirely.

Exited commands. A command Immu started in a terminal can end while the terminal stays open (Ctrl+C in it, a crash, a script that finishes). Immu watches what runs under the terminal's shell and shows it as Exited (or Failed if it never got ready); the terminal stays open so you can read why.

Changes to .env*, package.json, pnpm-workspace.yaml, the configs that can name a port (vite.config.*, astro.config.*, sst.config.*, .storybook/main.*, wrangler.toml…) or a new .git re-run discovery automatically.

Overrides

  • Immu: Set Dev Command (or ⋯ → Edit dev command… on a server) stores a command and ready port in extension storage, per repo origin, so it applies to every lane. Leave it empty to go back to the discovered one.

  • Git commands default to:

    Action Default
    status git status --porcelain=v2 --branch
    fetch git fetch --prune
    pull git pull --rebase --autostash origin {defaultBranch}
    push git push --set-upstream origin $(git symbolic-ref --short HEAD)

    {defaultBranch} is origin's default branch, read from .git/refs/remotes/origin/HEAD (else whichever of main / master / develop / trunk exists; main if none). If it is missing or stale, git remote set-head origin --auto fixes it.

    Override any of them with the immu.gitCommands setting (user, workspace or folder settings); {defaultBranch} works there too:

{ "immu.gitCommands": { "pull": "git pull --ff-only origin {defaultBranch}" } }

.vscode/immu.json files are no longer read; you can delete them.

Commands

Command What it does
Immu: Open Open (or focus) the Immu tab
Immu: Refresh Re-run discovery and scan for running dev servers
Immu: Pull All Repos Run the pull command in every repo at once, with one summary of what failed
Immu: Fetch All Repos Run the fetch command in every repo at once
Immu: Start / Stop Dev Server Quick-pick a command (startable ones, or running ones to stop)
Immu: Stop Project Servers Quick-pick a project and stop all of its servers
Immu: Set Dev Command Override a product's command / ready port
Immu: Stop All Dev Servers Stop every running command and free its ports, in every project

Settings: immu.gitCommands, immu.portIntervalMs (5000), immu.gitIntervalMs (30000).

Debugging discovery

Run discovery without the editor to see what it finds and why:

npm run discover -- ~/Code/acme              # products, commands, ports, and the file each came from
npm run discover -- --running ~/Code/acme    # plus what listens under each project right now
npm run discover -- --json ~/Code/acme       # the raw resolved projects
acme-1  lane 1  ~/Code/acme-1
  Pro  $ pnpm pro dev  ready on :4400
    found in: package.json script "pro" → acme-pro
    :4400    Dashboard          .env.local PORT_PRO_DASHBOARD
              declared by dev: products/pro/sst.config.ts uses PORT_PRO_DASHBOARD
    :4460    Storybook          .env.local PORT_STORYBOOK_PRO
              declared by storybook: script "storybook" in products/pro/package.json uses PORT_STORYBOOK_PRO

A port that is missing, or sits under the wrong command, usually shows up here with the rule that put it there.

Layout

src/
  extension.ts          activate, commands, file watchers
  model.ts              live git / ports / ▶ Dev state, stop logic, snapshots for the tab
  protocol.ts           messages and snapshots between the extension and the tab
  tech.ts               programs / processes → technologies, for the icons
  discovery/
    index.ts            folder → lanes → products → endpoints (entry point)
    lanes.ts  git.ts    repos, origins, lane numbers, default branch
    packages.ts         package.json, workspaces, package managers, how to run a script
    products.ts         which packages are products, which scripts start servers
    env.ts              layered .env files, port variable names, backing services
    commandline.ts      tokenizing scripts, ports on command lines, unwrapping cross-env / npx
    frameworks.ts       dev-server CLIs: default port per subcommand, config files
    sst.ts              what `sst dev` starts, read from sst.config.ts and its imports
    endpoints.ts        the port discovery steps above, in order
    scriptgraph.ts      which scripts a script reaches across the workspace, declared ports
    runtime.ts  processes.ts  procfs.ts   listeners, process tree, launchers (lsof / ps / /proc)
    overrides.ts        stored dev commands and learned launches (editor storage)
    report.ts  cli.ts   `npm run discover`
  products/             endpoint grouping by product and by command, script candidates
  git/  ports/          status, TCP / HTTP check, kill
  host/                 editor tab, terminals, status bar
  webview/              tab UI (overview, project page), icons.ts (Simple Icons); preview.html renders it with
                        sample data (`?project=/epicparty-06` opens a project page)

About

VS Code / Cursor extension: git, ports and dev servers for every repo and lane in your workspace — discovered, no config file.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages