Skip to content

feat(cli): add --json output to list, get, doctor, and service status - #6

Merged
rqbazan merged 2 commits into
mainfrom
feat/list-json
Sep 15, 2026
Merged

rqbazan merged 2 commits into
mainfrom
feat/list-json

Conversation

@rqbazan

@rqbazan rqbazan commented Sep 15, 2026

Copy link
Copy Markdown
Member

Summary

Scripts and agents that need portless state either parse the colored text of portless list or read routes.json themselves and re-implement which routes are live: aliases have pid: 0, stale PIDs must be skipped, PORTLESS_STATE_DIR and the sudo home must be resolved. They drift from portless as soon as any of that changes.

This adds one --json convention to the CLI so they can ask portless instead.

Change

--json is a global flag with a single contract:

  • stdout carries only the JSON document, without colors. Warnings and errors go to stderr, and a command that fails exits non-zero.
  • Keys are camelCase, like routes.json and portless.json. Optional fields are omitted when unset.
  • Only list, get, doctor, and service status accept it. Any other command, app runs included, exits 1 with Error: --json is only supported by list, get, doctor, service status. A --json after the child command still reaches the child.
Command Output
portless list --json Array of the routes the proxy serves (live processes and aliases): hostname, pathPrefix, port, pid (0 for an alias), alias, url, and tailscaleUrl or ngrokUrl when shared.
portless get <name> --json name, hostname, pathPrefix, url, proxyPort, tls. Like get, it does not check whether the service is running.
portless doctor --json version, node, platform, arch, stateDir, proxyPort, tls, tlds, lanMode, findings (status, message, hint), failures, warnings. Exits 1 when a check fails, like doctor.
portless service status --json installed, managerState, proxyPort, proxyRunning, tls, tlds, lanMode, lanIp, wildcard, stateDir, serviceEntry.

list --json resolves state like list and returns the same routes the proxy serves (RouteStore.loadRoutes()), without persisting the stale-route cleanup. Unlike list, a routes.json that cannot be read or parsed is an error (exit 1, message on stderr), so a caller can tell "no routes" apart from "cannot tell".

To detect an unreadable file, RouteStore.loadRoutes() now reports read failures (for example EACCES) through onWarning, the way loadRoutesRaw() already did. Commands that already print the corrupted-file warning now print this one too, on stderr. Before, they treated an unreadable file as empty without saying so.

Human-readable output is otherwise unchanged.

Docs updated: README (Commands and a new JSON output section), skills/portless/SKILL.md, the apps/docs commands page, and --help for the CLI, get, doctor, and service.

Upstream, vercel-labs/portless#257 proposes --json for list and get only, parsed per command, with snake_case keys (target_port, kind) and no path prefixes. This PR makes the flag general and keeps key names aligned with portless's own files.

Verification

  • New tests, failing before the change:
    • cli.test.ts: list --json (live routes, aliases, path prefixes, tunnel URLs, no stale-route write-back, empty list, corrupted file, unreadable file), --json before the command, get --json with and without --path, doctor --json, rejection by alias.
    • cli-utils.test.ts: which commands accept --json.
    • service.test.ts: service status --json.
    • routes.test.ts: loadRoutes warns when the file cannot be read.
  • Guards that pass before and after: human-readable list output, and --json after the child command reaching the child.
  • pnpm format:check && pnpm lint && pnpm type-check && pnpm build && pnpm test && pnpm test:e2e: all green (999 unit tests passed and 1 skipped, 15 docs tests, 16 e2e tests passed and 2 skipped).
  • Timing on macOS with Node 24, 20 runs each, a temporary state dir, and the proxy port closed: node -e "" alone takes 130 ms, portless list 162 ms, portless list --json 161 ms, and portless get x --no-worktree --json 158 ms. The JSON path does no more work than list.

No version bump here. The release follows in its own PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BSDFAb4giiESfUnAHgHG3S

rqbazan and others added 2 commits September 15, 2026 11:58
loadRoutes() treated a routes.json it could not read (for example EACCES
or EISDIR) as empty without calling onWarning, while loadRoutesRaw()
already reported it. Report it the same way, so callers can tell an
unreadable registry apart from an empty one.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01BSDFAb4giiESfUnAHgHG3S
--json is a global flag with one contract: stdout carries only the JSON
document, warnings and errors go to stderr, a command that fails exits
non-zero, and keys are camelCase like routes.json and portless.json.
Commands without JSON output reject the flag, and a --json after the
child command still reaches the child.

list --json returns the routes the proxy serves (live processes and
aliases) with their public URLs, without persisting the stale-route
cleanup. It exits 1 when routes.json cannot be read or parsed, instead
of printing an empty list.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01BSDFAb4giiESfUnAHgHG3S
@rqbazan
rqbazan merged commit 967f097 into main Sep 15, 2026
2 checks passed
@rqbazan
rqbazan deleted the feat/list-json branch September 15, 2026 17:10
@rqbazan rqbazan mentioned this pull request Sep 15, 2026
rqbazan added a commit that referenced this pull request Sep 15, 2026
Bump to 0.0.4 and add the changelog entry. The release ships --json
output from #6 for list, get, doctor, and service status, and the new
warning when routes.json cannot be read.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01BSDFAb4giiESfUnAHgHG3S
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant