SLOPSHOPPER

portly-ports

Shows Portly's running ports, most recently started first, in the status line

newcommandstatusnetworktimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · portly-ports
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /portly-ports ⎿ portly-ports: Portly ports in the status line: off. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

Claude Configuration

This repository contains Claude Code configuration files and automatically syncs with the upstream repository.

Automated Sync

This repository automatically syncs from Melvynx/aiblueprint-cli-premium once per day at midnight UTC.

How It Works

Manual Sync

To trigger a sync manually:

  1. Go to the Actions tab in this repository
  2. Select "Sync from Upstream Repository" workflow
  3. Click "Run workflow" button
  4. Select the main branch and click "Run workflow"

Monitoring

  • Check the Actions tab to view sync history and status
  • Each sync run shows which commits were synchronized
  • You'll receive notifications for failed sync attempts (configure in Settings → Notifications)

Notes

  • This repository should be treated as read-only to avoid merge conflicts
  • The upstream repository always wins in case of conflicts
  • Sync logs are available in the Actions tab for 90 days
Source 1 files
hooks/register.ts 108 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3// Portly's local API (macOS app first, Linux daemon second). Querying it
4// directly never launches Portly.app the way the `portly` CLI does.
5const API_PORTS = [7738, 7737]
6const POLL_MS = 4000
7const MAX_SHOWN = 6
8
9type PortlyServer = {
10  name: string
11  projectName: string
12  port?: number
13  state: string
14  healthy?: boolean
15  startedAt?: string | null
16}
17
18type PortlyStatus = {
19  data?: {
20    projects?: { servers?: PortlyServer[] }[]
21    temporaryServers?: PortlyServer[]
22  }
23}
24
25let apiPort = API_PORTS[0] as number
26let isEnabled = true
27let shown: string | undefined
28
29const isActive = (server: PortlyServer) =>
30  server.port !== undefined && (server.state === 'running' || server.state === 'starting')
31
32const slug = (text: string) =>
33  text.trim().toLowerCase().replace(/[^a-z0-9.]+/g, '-').replace(/^-|-$/g, '')
34
35// `app:port` for each running server, most recently started first; `…` while
36// not yet healthy. A project with several running servers names each one.
37function format(status: PortlyStatus) {
38  const active = [
39    ...(status.data?.projects ?? []).flatMap(project => project.servers ?? []),
40    ...(status.data?.temporaryServers ?? []),
41  ]
42    .filter(isActive)
43    .sort((a, b) => Date.parse(b.startedAt ?? '') - Date.parse(a.startedAt ?? '') || 0)
44
45  if (active.length === 0) return undefined
46
47  const perProject = new Map<string, number>()
48  for (const one of active) {
49    perProject.set(one.projectName, (perProject.get(one.projectName) ?? 0) + 1)
50  }
51  const label = (one: PortlyServer) =>
52    (perProject.get(one.projectName) ?? 0) > 1
53      ? `${slug(one.projectName)}/${slug(one.name)}`
54      : slug(one.projectName)
55
56  const ports = active
57    .slice(0, MAX_SHOWN)
58    .map(one => `${label(one)}:${one.port}${one.healthy ? '' : '…'}`)
59  const more = active.length > MAX_SHOWN ? ` +${active.length - MAX_SHOWN}` : ''
60
61  return `${ports.join(' · ')}${more}`
62}
63
64async function fetchStatus($: EngineInterface) {
65  for (const candidate of [apiPort, ...API_PORTS.filter(one => one !== apiPort)]) {
66    try {
67      const response = await $.http.fetch(`http://127.0.0.1:${candidate}/status`)
68      if (!response.ok) continue
69      apiPort = candidate
70      return JSON.parse(response.text) as PortlyStatus
71    } catch {
72      // Portly not listening on this port.
73    }
74  }
75  return undefined
76}
77
78async function refresh($: EngineInterface) {
79  const status = isEnabled ? await fetchStatus($) : undefined
80  const text = status ? format(status) : undefined
81  if (text === shown) return
82  shown = text
83  $.ui.status(text)
84}
85
86export const register: Register = on => {
87  on('session.start', async ($, e, next) => {
88    isEnabled = (await $.store.get('isEnabled')) !== false
89    shown = undefined
90    await $.command.register({
91      name: 'portly-ports',
92      description: 'Toggle Portly ports in the status line',
93    })
94    void refresh($)
95    $.clock.every(POLL_MS, () => void refresh($))
96
97    return next(e)
98  })
99
100  on('command.run', { command: 'portly-ports' }, async $ => {
101    isEnabled = !isEnabled
102    await $.store.set('isEnabled', isEnabled)
103    await refresh($)
104
105    return { text: `Portly ports in the status line: ${isEnabled ? 'on' : 'off'}.` }
106  })
107}
108