/ports: every port listening on this machine, which project or container owns it, and whether your phone can reach it, with its Tailscale URL and a QR code to…

<img src=".claude-plugin/icon.png" width="72" align="right" alt="">
/ports answers "what is on 5173 again, and can my phone reach it?"
For every listening port, it shows:
python -m modules), or the containerFor a phone-reachable port you get its Tailscale URL and a QR code to scan. Tailscale Serve mappings are shown on the port they front.
It reads, and never changes:
netstat, tasklist and the process command lines on Windows; lsof and ps elsewherepodman ps and docker ps for published ports, which the OS does not list as the container'stailscale ip -4 and tailscale serve statusThe QR codes come from the vendored qrcode-generator (MIT; see THIRD_PARTY_NOTICES.md).
No settings.
hooks/register.tsx 271 lines1// Port Watch: what is listening here, who owns it, and how to reach it from your phone.
2// - Windows: netstat -ano, tasklist and each listener's command line (one CIM query); macOS and
3// Linux: lsof and ps. podman's (or docker's) published ports are named by their container.
4// - The Tailscale IP turns a port bound to every interface into a URL your phone can open,
5// shown as text and as a QR code to scan. A port bound to 127.0.0.1 says why it cannot.
6// - /ports opens the pane; it scans again every 20 s while it is open.
7import { atom, read, update } from 'claude-code'
8import type { EngineInterface, Register } from 'claude-code'
9
10import type { PortRow } from '../types'
11import { parseLsof, parseNetstat, parsePublished, parseServe, parseTasklist, qrSvg, rows as toRows, type Served } from './ports'
12
13const PANE = 'port-watch'
14const PER_PAGE = 12
15const RESCAN_MS = 20_000
16const TAILSCALE_WINDOWS = 'C:/Program Files/Tailscale/tailscale.exe'
17
18const rows = atom({ plugin: 'port-watch', key: 'rows' } as const, [] as PortRow[])
19const scannedAt = atom({ plugin: 'port-watch', key: 'scannedAt' } as const, 0)
20const scanning = atom({ plugin: 'port-watch', key: 'scanning' } as const, false)
21const filter = atom({ plugin: 'port-watch', key: 'filter' } as const, 'dev' as 'dev' | 'phone' | 'all')
22const query = atom({ plugin: 'port-watch', key: 'query' } as const, '')
23const qr = atom({ plugin: 'port-watch', key: 'qr' } as const, 0)
24const page = atom({ plugin: 'port-watch', key: 'page' } as const, 0)
25const tailnetIp = atom({ plugin: 'port-watch', key: 'tailnetIp' } as const, '')
26const error = atom({ plugin: 'port-watch', key: 'error' } as const, '')
27
28export const register: Register = on => {
29 on('session.start', async ($, e, next) => {
30 const r = await next(e)
31 await $.command.register({ name: 'ports', description: 'What is listening on this machine, who owns it, and a QR code to open it on your phone' }).catch(() => {})
32 $.clock.every(RESCAN_MS, () => {
33 void (async () => {
34 if ((await $.ui.panes()).some(p => p.id === PANE && p.isPlaced)) await scan($)
35 })().catch(() => {})
36 })
37 return r
38 })
39
40 on('command.run', { command: 'ports' }, async ($, e) => {
41 const arg = e.args.trim()
42 if (/^\d+$/.test(arg)) await update($, qr, () => Number(arg))
43 await $.ui.open({ id: PANE, title: 'Ports', focus: true })
44 await scan($)
45 const list = await read($, rows)
46 return { text: `Ports: ${list.filter(r => r.kind === 'dev').length} dev servers listening, ${list.filter(r => r.kind === 'dev' && r.phoneUrl).length} reachable from your phone.` }
47 })
48
49 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => drawPane($, e))
50}
51
52// ── Scanning ──────────────────────────────────────────────────────────────
53
54const slash = (p: string) => p.replace(/\\/g, '/')
55
56async function run($: EngineInterface, argv: string[], timeoutMs = 20_000) {
57 const r = await $.process.run(argv, { timeoutMs }).catch(() => null)
58 return r && r.exitCode === 0 ? r.stdout : ''
59}
60
61// The Tailscale IP and what Tailscale Serve proxies, from whichever tailscale binary answers.
62async function findTailnet($: EngineInterface, isWindows: boolean): Promise<{ ip: string; served: Served[] }> {
63 for (const exe of isWindows ? [TAILSCALE_WINDOWS, 'tailscale'] : ['tailscale']) {
64 if (isWindows && exe === TAILSCALE_WINDOWS && !(await $.fs.exists(exe).catch(() => false))) continue
65 const ip = (await run($, [exe, 'ip', '-4'], 8_000)).trim().split(/\r?\n/)[0] ?? ''
66 if (/^\d+\.\d+\.\d+\.\d+$/.test(ip)) return { ip, served: parseServe(await run($, [exe, 'serve', 'status', '--json'], 8_000)) }
67 }
68 return { ip: '', served: [] }
69}
70
71async function scan($: EngineInterface) {
72 if (await read($, scanning)) return
73 await update($, scanning, () => true)
74 try {
75 const isWindows = /^[A-Za-z]:\//.test(slash($.plugin.root))
76 const { ip, served } = await findTailnet($, isWindows)
77 let published = new Map()
78 for (const runtime of ['podman', 'docker']) {
79 const out = await run($, [runtime, 'ps', '--format', 'json'])
80 if (out.trim()) {
81 published = parsePublished(out)
82 break
83 }
84 }
85 let next: PortRow[]
86 if (isWindows) {
87 const listeners = parseNetstat(await run($, ['netstat', '-ano', '-p', 'TCP'])).concat(parseNetstat(await run($, ['netstat', '-ano', '-p', 'TCPv6'])))
88 const names = parseTasklist(await run($, ['tasklist', '/fo', 'csv', '/nh']))
89 const pids = [...new Set(listeners.map(l => l.pid).filter(p => p > 4))]
90 const commandLines = new Map<number, string>()
91 if (pids.length) {
92 const filterText = pids.map(p => `ProcessId=${p}`).join(' OR ')
93 const json = await run($, ['powershell', '-NoProfile', '-NonInteractive', '-Command', `Get-CimInstance Win32_Process -Filter "${filterText}" | Select-Object ProcessId,CommandLine | ConvertTo-Json -Compress`], 30_000)
94 try {
95 const parsed = JSON.parse(json || '[]')
96 for (const p of (Array.isArray(parsed) ? parsed : [parsed]) as { ProcessId?: number; CommandLine?: string }[]) if (p.ProcessId) commandLines.set(p.ProcessId, p.CommandLine ?? '')
97 } catch {}
98 }
99 next = toRows({ listeners, names, commandLines, published, served, tailnetIp: ip })
100 } else {
101 const listeners = parseLsof(await run($, ['lsof', '-nP', '-iTCP', '-sTCP:LISTEN']))
102 const commandLines = new Map<number, string>()
103 const pids = [...new Set(listeners.map(l => l.pid))]
104 if (pids.length) {
105 for (const line of (await run($, ['ps', '-o', 'pid=,command=', '-p', pids.join(',')])).split(/\r?\n/)) {
106 const m = line.trim().match(/^(\d+)\s+(.*)$/)
107 if (m) commandLines.set(Number(m[1]), m[2]!)
108 }
109 }
110 next = toRows({ listeners, names: new Map(), commandLines, published, served, tailnetIp: ip })
111 }
112 await update($, rows, () => next)
113 await update($, tailnetIp, () => ip)
114 await update($, scannedAt, () => Date.now())
115 await update($, error, () => (next.length ? '' : 'Nothing listening was found (or the scan tools did not run).'))
116 } finally {
117 await update($, scanning, () => false)
118 }
119}
120
121// ── Doing ─────────────────────────────────────────────────────────────────
122
123async function openUrl($: EngineInterface, url: string) {
124 const isWindows = /^[A-Za-z]:\//.test(slash($.plugin.root))
125 const uname = isWindows ? '' : (await run($, ['uname', '-s'], 3_000)).trim()
126 await $.process.run(isWindows ? ['explorer.exe', url] : [uname === 'Darwin' ? 'open' : 'xdg-open', url], { timeoutMs: 10_000 }).catch(() => null)
127}
128
129async function copy($: EngineInterface, text: string) {
130 const viaSurface = await $.ui.copy({ text }).catch(() => ({ isCopied: false }))
131 const isWindows = /^[A-Za-z]:\//.test(slash($.plugin.root))
132 const ok = viaSurface.isCopied || (await $.process.run(isWindows ? ['clip'] : ['pbcopy'], { stdin: text, timeoutMs: 5_000 }).catch(() => null))?.exitCode === 0
133 $.ui.toast(ok ? `Copied ${text}` : `Copy it by hand: ${text}`)
134}
135
136// ── The pane ──────────────────────────────────────────────────────────────
137
138export function shown(list: PortRow[], f: 'dev' | 'phone' | 'all', q: string): PortRow[] {
139 const words = q.toLowerCase().split(/\s+/).filter(Boolean)
140 return list.filter(r => (f === 'all' || r.kind === 'dev') && (f !== 'phone' || r.phoneUrl !== '') && words.every(w => `${r.port} ${r.owner} ${r.process}`.toLowerCase().includes(w)))
141}
142
143async function drawPane($: EngineInterface, e: Parameters<EngineInterface['ui']['resolve']>[0]) {
144 const elements = $.ui.resolve(e)
145 const { Box, Text, Button } = elements
146 const Input = 'Input' in elements ? elements.Input : undefined
147 const Svg = 'Svg' in elements ? elements.Svg : undefined
148 const list = await read($, rows)
149 const f = await read($, filter)
150 const q = await read($, query)
151 const open = await read($, qr)
152 const at = await read($, page)
153 const ip = await read($, tailnetIp)
154 const when = await read($, scannedAt)
155 const isScanning = await read($, scanning)
156 const problem = await read($, error)
157 const visible = shown(list, f, q)
158 const pages = Math.max(1, Math.ceil(visible.length / PER_PAGE))
159 const p = Math.min(at, pages - 1)
160 const dev = list.filter(r => r.kind === 'dev').length
161 const phone = list.filter(r => r.kind === 'dev' && r.phoneUrl).length
162 const pill = (key: string, text: string, bg: string) => (
163 <Text key={key} backgroundColor={bg} color="inverseText" weight="semibold" size="caption">
164 {` ${text} `}
165 </Text>
166 )
167 const chip = (value: 'dev' | 'phone' | 'all', label: string) => (
168 <Button
169 key={`f-${value}`}
170 label={label}
171 variant={f === value ? 'primary' : undefined}
172 plain={f === value ? undefined : true}
173 dimColor={f !== value}
174 onPress={async () => {
175 await update($, filter, () => value)
176 await update($, page, () => 0)
177 }}
178 />
179 )
180
181 return (
182 <Box flexDirection="column" gap={1}>
183 <Box flexDirection="row" justifyContent="space-between" alignItems="center" columnGap={2} flexWrap="wrap">
184 <Box flexDirection="column">
185 <Text size="title" weight="bold">
186 <Text color="claude">{'⇄ '}</Text>
187 {'Ports'}
188 </Text>
189 <Text size="footnote" dimColor>
190 {`${dev} dev servers · ${phone} reachable from your phone${ip ? ` over Tailscale (${ip})` : ' · Tailscale not found'}`}
191 </Text>
192 </Box>
193 <Box flexDirection="row" columnGap={1} alignItems="center">
194 <Button key="scan" label={isScanning ? 'scanning…' : '↻ Scan'} plain={isScanning ? true : undefined} onPress={() => scan($)} />
195 <Button key="close" label="✕" plain dimColor role="dismiss" onPress={() => $.ui.close({ id: PANE })} />
196 </Box>
197 </Box>
198
199 <Box flexDirection="column" borderStyle="round" borderColor="inactive" paddingX={1} gap={1}>
200 {Input && (
201 <Input
202 key="search"
203 placeholder="Search port, project or process…"
204 value={q}
205 submitLabel="search"
206 onInput={async v => {
207 await update($, query, () => v)
208 await update($, page, () => 0)
209 }}
210 onSubmit={v => update($, query, () => v)}
211 />
212 )}
213 <Box flexDirection="row" columnGap={1} alignItems="center" flexWrap="wrap">
214 {chip('dev', `Dev servers ${dev}`)}
215 {chip('phone', `Phone ok ${phone}`)}
216 {chip('all', `All ${list.length}`)}
217 <Text size="caption" dimColor>{when ? `scanned ${Math.round((Date.now() - when) / 1000)} s ago` : ''}</Text>
218 </Box>
219 </Box>
220
221 {problem !== '' && <Text color="warning">{problem}</Text>}
222 {list.length > 0 && visible.length === 0 && <Text dimColor>Nothing matches. Try All, or another word.</Text>}
223
224 {visible.slice(p * PER_PAGE, (p + 1) * PER_PAGE).map(r => {
225 const isOpen = open === r.port
226 return (
227 <Box key={`port-${r.port}`} flexDirection="column" borderStyle="round" borderColor={isOpen ? 'claude' : r.phoneUrl ? 'success' : 'inactive'} borderDimColor={!isOpen && !r.phoneUrl} hover={{ borderColor: 'claude', borderDimColor: false }} paddingX={1}>
228 <Box flexDirection="row" justifyContent="space-between" alignItems="center" columnGap={1} flexWrap="wrap">
229 <Text wrap="truncate-end">
230 <Text size="heading" weight="bold" mono>{`:${r.port}`}</Text>
231 <Text weight="semibold">{` ${r.owner}`}</Text>
232 <Text size="footnote" dimColor>{` ${r.process} · pid ${r.pid}`}</Text>
233 </Text>
234 {r.phoneUrl ? pill(`b-${r.port}`, 'PHONE OK', 'success') : r.bind === 'local' ? pill(`b-${r.port}`, 'THIS PC ONLY', 'inactive') : pill(`b-${r.port}`, r.bind.toUpperCase(), 'inactive')}
235 </Box>
236 <Box flexDirection="row" columnGap={1} alignItems="center" flexWrap="wrap">
237 <Text mono size="footnote" dimColor={!r.phoneUrl}>
238 {r.phoneUrl || r.url}
239 </Text>
240 <Button key={`open-${r.port}`} label="Open ↗" plain onPress={() => openUrl($, r.url)} />
241 <Button key={`copy-${r.port}`} label="⧉ Copy" plain dimColor onPress={() => copy($, r.phoneUrl || r.url)} />
242 {r.phoneUrl !== '' && <Button key={`qr-${r.port}`} label={isOpen ? '▣ Hide QR' : '▣ QR'} variant={isOpen ? 'primary' : undefined} plain={isOpen ? undefined : true} onPress={() => update($, qr, v => (v === r.port ? 0 : r.port))} />}
243 </Box>
244 {isOpen && r.phoneUrl !== '' && (
245 <Box flexDirection="row" columnGap={2} alignItems="center">
246 {Svg ? <Svg key={`qr-svg-${r.port}`} source={qrSvg(r.phoneUrl)} alt={`QR code for ${r.phoneUrl}`} width={220} height={220} /> : <Text dimColor>This surface draws no pictures.</Text>}
247 <Text size="footnote" dimColor wrap="wrap">
248 {`Scan with your phone's camera while it is on Tailscale.\n${r.phoneUrl}`}
249 </Text>
250 </Box>
251 )}
252 {!r.phoneUrl && r.bind === 'local' && r.kind === 'dev' && (
253 <Text size="caption" dimColor>
254 Bound to 127.0.0.1, so only this PC reaches it. Start it on 0.0.0.0, or `tailscale serve` it, to open it from your phone.
255 </Text>
256 )}
257 </Box>
258 )
259 })}
260
261 {pages > 1 && (
262 <Box flexDirection="row" justifyContent="space-between" alignItems="center">
263 <Button key="prev" label="‹ Prev" plain dimColor={p === 0} onPress={() => update($, page, n => Math.max(0, n - 1))} />
264 <Text size="footnote" dimColor>{`${p + 1} of ${pages}`}</Text>
265 <Button key="next" label="Next ›" plain dimColor={p >= pages - 1} onPress={() => update($, page, n => Math.min(pages - 1, n + 1))} />
266 </Box>
267 )}
268 </Box>
269 )
270}
271hooks/ports.ts 191 lines1// What port-watch reads and how it names it. Pure: no $, so the tests call it directly.
2import { qrcode } from './vendor/qrcode.mjs'
3
4export type Listener = { address: string; port: number; pid: number }
5export type Published = { container: string; containerPort: number; hostIp: string }
6export type Served = { port: number; host: string; targetPort: number } // a Tailscale Serve mapping
7export type Bind = 'local' | 'all' | 'tailnet' | 'other'
8export type Kind = 'dev' | 'app' | 'system'
9export type PortRow = {
10 port: number
11 pid: number
12 process: string
13 owner: string // the project, container or package it belongs to, as best as can be told
14 kind: Kind // dev: a runtime, a container or a Serve proxy; app: a desktop program; system: Windows' own
15 bind: Bind
16 address: string
17 url: string // where a browser reaches it from here
18 phoneUrl: string // over Tailscale (its IP, or a Serve URL); '' when the phone cannot reach it
19}
20
21// Windows' own services and anything under 1024: never a dev server.
22const SYSTEM = new Set(['system', 'system idle process', 'svchost.exe', 'lsass.exe', 'wininit.exe', 'services.exe', 'spoolsv.exe', 'smss.exe', 'csrss.exe', 'winlogon.exe', 'searchindexer.exe', 'mdnsresponder.exe', 'jhi_service.exe'])
23// The processes a dev server runs as: language runtimes, web servers, container relays.
24const RUNTIMES = /^(node|nodejs|bun|deno|python\d*(\.\d+)?|pythonw|py|uvicorn|gunicorn|hypercorn|java|javaw|dotnet|ruby|php|php-cgi|go|air|caddy|nginx|httpd|gvproxy|wslrelay|podman|docker-proxy|com\.docker\.backend|vpnkit|esbuild|vite|next-server|cargo|rust|code)(\.exe)?$/i
25
26// `netstat -ano` LISTENING lines, IPv4 and IPv6.
27export function parseNetstat(text: string): Listener[] {
28 const out: Listener[] = []
29 for (const line of text.split(/\r?\n/)) {
30 const m = line.trim().match(/^TCP\s+(\S+):(\d+)\s+\S+\s+LISTENING\s+(\d+)$/i)
31 if (m) out.push({ address: m[1]!.replace(/^\[|\]$/g, ''), port: Number(m[2]), pid: Number(m[3]) })
32 }
33 return out
34}
35
36// `lsof -nP -iTCP -sTCP:LISTEN` (macOS, Linux): COMMAND PID ... NAME (addr:port) (LISTEN).
37export function parseLsof(text: string): (Listener & { process: string })[] {
38 const out: (Listener & { process: string })[] = []
39 for (const line of text.split(/\r?\n/).slice(1)) {
40 const cols = line.trim().split(/\s+/)
41 const name = cols.find(c => /:\d+$/.test(c))
42 if (!name || cols.length < 2) continue
43 const at = name.lastIndexOf(':')
44 out.push({ process: cols[0]!, pid: Number(cols[1]), address: name.slice(0, at).replace(/^\[|\]$/g, '').replace(/^\*$/, '0.0.0.0'), port: Number(name.slice(at + 1)) })
45 }
46 return out
47}
48
49// `tasklist /fo csv /nh`: "name","pid",... as pid → name.
50export function parseTasklist(csv: string): Map<number, string> {
51 const out = new Map<number, string>()
52 for (const line of csv.split(/\r?\n/)) {
53 const m = line.match(/^"([^"]*)","(\d+)"/)
54 if (m) out.set(Number(m[2]), m[1]!)
55 }
56 return out
57}
58
59// `podman ps --format json` (or docker's, one object per line) as host port → its container.
60export function parsePublished(text: string): Map<number, Published> {
61 const out = new Map<number, Published>()
62 let list: unknown[] = []
63 try {
64 const parsed = JSON.parse(text)
65 list = Array.isArray(parsed) ? parsed : [parsed]
66 } catch {
67 list = text.split(/\r?\n/).filter(Boolean).flatMap(l => {
68 try {
69 return [JSON.parse(l)]
70 } catch {
71 return []
72 }
73 })
74 }
75 for (const c of list as { Names?: string[] | string; Ports?: unknown }[]) {
76 const name = Array.isArray(c.Names) ? (c.Names[0] ?? '') : String(c.Names ?? '')
77 for (const p of (Array.isArray(c.Ports) ? c.Ports : []) as { host_port?: number; container_port?: number; host_ip?: string; range?: number }[]) {
78 if (!p.host_port) continue
79 for (let i = 0; i < Math.max(1, p.range ?? 1); i++) out.set(p.host_port + i, { container: name, containerPort: (p.container_port ?? 0) + i, hostIp: p.host_ip ?? '' })
80 }
81 }
82 return out
83}
84
85// `tailscale serve status --json`: each tailnet port and the local port it proxies to.
86export function parseServe(text: string): Served[] {
87 let s: { Web?: Record<string, { Handlers?: Record<string, { Proxy?: string }> }> }
88 try {
89 s = JSON.parse(text)
90 } catch {
91 return []
92 }
93 const out: Served[] = []
94 for (const [hostPort, web] of Object.entries(s?.Web ?? {})) {
95 const at = hostPort.lastIndexOf(':')
96 const proxy = Object.values(web?.Handlers ?? {}).find(h => typeof h?.Proxy === 'string')?.Proxy ?? ''
97 const target = Number(proxy.match(/:(\d+)(?:\/|$)/)?.[1] ?? 0)
98 if (at > 0 && target) out.push({ host: hostPort.slice(0, at), port: Number(hostPort.slice(at + 1)), targetPort: target })
99 }
100 return out
101}
102
103// A process's project from its command line: a projects/<name> folder, an npx package, the app a
104// node_modules tool runs for, a Python module, else its script's folder.
105export function ownerOf(commandLine: string, process: string): string {
106 const cl = commandLine.replace(/\\\\/g, '\\')
107 const project = cl.match(/[\\/]projects[\\/]([^\\/"'\s]+)/i)?.[1]
108 if (project) return project
109 const npx = cl.match(/npx-cli\.js"?\s+(?:-y\s+)?(@?[\w.-]+(?:\/[\w.-]+)?)/i)?.[1]
110 if (npx) return npx.replace(/@[\d.]+$/, '')
111 const app = cl.match(/([\w.-]+)[\\/]node_modules[\\/]/i)?.[1] // the app whose dependency runs it (vite, next…)
112 if (app) return app
113 const module = cl.match(/\s-m\s+([\w]+)/)?.[1]
114 if (module) return module
115 const script = cl.match(/([\w.-]+)[\\/][^\\/"'\s]+\.(?:m?js|ts|py)\b/i)?.[1]
116 if (script && !/^(bin|dist|src|scripts|node_modules)$/i.test(script)) return script
117 return process.replace(/\.exe$/i, '')
118}
119
120export function bindOf(address: string, tailnetIp: string): Bind {
121 if (address === '127.0.0.1' || address === '::1' || address === 'localhost') return 'local'
122 if (address === '0.0.0.0' || address === '::' || address === '*') return 'all'
123 if (tailnetIp && address === tailnetIp) return 'tailnet'
124 return 'other'
125}
126
127// One row per port (IPv4 and IPv6 listeners merged, the widest bind kept), sorted by port:
128// container ports netstat cannot see are added, and Tailscale Serve ports named by what they serve.
129export function rows(args: {
130 listeners: (Listener & { process?: string })[]
131 names: Map<number, string>
132 commandLines: Map<number, string>
133 published: Map<number, Published>
134 served?: Served[]
135 tailnetIp: string
136}): PortRow[] {
137 const rank: Record<Bind, number> = { all: 3, tailnet: 2, other: 1, local: 0 }
138 const served = args.served ?? []
139 const byPort = new Map<number, PortRow>()
140 const put = (row: PortRow) => {
141 const had = byPort.get(row.port)
142 if (!had || rank[row.bind] > rank[had.bind]) byPort.set(row.port, had ? { ...row, owner: had.owner.startsWith('pid ') ? row.owner : had.owner } : row)
143 }
144 for (const l of args.listeners) {
145 const process = l.process ?? args.names.get(l.pid) ?? `pid ${l.pid}`
146 const pub = args.published.get(l.port)
147 const bind = pub && pub.hostIp && pub.hostIp !== '0.0.0.0' ? bindOf(pub.hostIp, args.tailnetIp) : bindOf(l.address, args.tailnetIp)
148 const owner = pub ? `${pub.container} (container :${pub.containerPort})` : ownerOf(args.commandLines.get(l.pid) ?? '', process)
149 const isSystem = !pub && (l.pid <= 4 || SYSTEM.has(process.toLowerCase()) || l.port < 1024)
150 const kind: Kind = isSystem ? 'system' : pub || RUNTIMES.test(process) || /[\\/]projects[\\/]/i.test(args.commandLines.get(l.pid) ?? '') ? 'dev' : 'app'
151 put({
152 port: l.port,
153 pid: l.pid,
154 process,
155 owner,
156 kind,
157 bind,
158 address: l.address,
159 url: `http://127.0.0.1:${l.port}/`,
160 phoneUrl: args.tailnetIp && (bind === 'all' || bind === 'tailnet') ? `http://${args.tailnetIp}:${l.port}/` : '',
161 })
162 }
163 // Published container ports the host's netstat does not list (podman relays them from its VM).
164 for (const [port, pub] of args.published) {
165 if (byPort.has(port)) continue
166 const bind = bindOf(pub.hostIp || '0.0.0.0', args.tailnetIp)
167 put({ port, pid: 0, process: 'container', owner: `${pub.container} (container :${pub.containerPort})`, kind: 'dev', bind, address: pub.hostIp || '0.0.0.0', url: `http://127.0.0.1:${port}/`, phoneUrl: args.tailnetIp && bind !== 'local' ? `http://${args.tailnetIp}:${port}/` : '' })
168 }
169 // Tailscale Serve: the tailnet port says what it serves; the local server gets the Serve URL.
170 for (const s of served) {
171 const url = `https://${s.host}${s.port === 443 ? '' : `:${s.port}`}/`
172 const target = byPort.get(s.targetPort)
173 const proxy = byPort.get(s.port)
174 if (proxy) byPort.set(s.port, { ...proxy, kind: 'dev', owner: `serve → :${s.targetPort}${target ? ` ${target.owner}` : ''}`, phoneUrl: url })
175 if (target && !target.phoneUrl) byPort.set(s.targetPort, { ...target, kind: target.kind === 'system' ? 'system' : 'dev', phoneUrl: url })
176 }
177 return [...byPort.values()].sort((a, b) => a.port - b.port)
178}
179
180// A QR code for `text` as an SVG document: one path of dark modules, a quiet zone of 4.
181export function qrSvg(text: string, size = 220): string {
182 const qr = qrcode(0, 'M')
183 qr.addData(text)
184 qr.make()
185 const n = qr.getModuleCount()
186 const cells = n + 8
187 let d = ''
188 for (let r = 0; r < n; r++) for (let c = 0; c < n; c++) if (qr.isDark(r, c)) d += `M${c + 4} ${r + 4}h1v1h-1z`
189 return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${cells} ${cells}" shape-rendering="crispEdges"><rect width="${cells}" height="${cells}" fill="#ffffff"/><path d="${d}" fill="#111111"/></svg>`
190}
191hooks/vendor/qrcode.mjs 2238 lines1//---------------------------------------------------------------------
2//
3// QR Code Generator for JavaScript
4//
5// Copyright (c) 2009 Kazuhiko Arase
6//
7// URL: http://www.d-project.com/
8//
9// Licensed under the MIT license:
10// http://www.opensource.org/licenses/mit-license.php
11//
12// The word 'QR Code' is registered trademark of
13// DENSO WAVE INCORPORATED
14// http://www.denso-wave.com/qrcode/faqpatent-e.html
15//
16//---------------------------------------------------------------------
17
18//---------------------------------------------------------------------
19// qrcode
20//---------------------------------------------------------------------
21
22/**
23 * qrcode
24 * @param typeNumber 1 to 40
25 * @param errorCorrectionLevel 'L','M','Q','H'
26 */
27export const qrcode = function(typeNumber, errorCorrectionLevel) {
28
29 const PAD0 = 0xEC;
30 const PAD1 = 0x11;
31
32 let _typeNumber = typeNumber;
33 const _errorCorrectionLevel = QRErrorCorrectionLevel[errorCorrectionLevel];
34 let _modules = null;
35 let _moduleCount = 0;
36 let _dataCache = null;
37 const _dataList = [];
38
39 const _this = {};
40
41 const makeImpl = function(test, maskPattern) {
42
43 _moduleCount = _typeNumber * 4 + 17;
44 _modules = function(moduleCount) {
45 const modules = new Array(moduleCount);
46 for (let row = 0; row < moduleCount; row += 1) {
47 modules[row] = new Array(moduleCount);
48 for (let col = 0; col < moduleCount; col += 1) {
49 modules[row][col] = null;
50 }
51 }
52 return modules;
53 }(_moduleCount);
54
55 setupPositionProbePattern(0, 0);
56 setupPositionProbePattern(_moduleCount - 7, 0);
57 setupPositionProbePattern(0, _moduleCount - 7);
58 setupPositionAdjustPattern();
59 setupTimingPattern();
60 setupTypeInfo(test, maskPattern);
61
62 if (_typeNumber >= 7) {
63 setupTypeNumber(test);
64 }
65
66 if (_dataCache == null) {
67 _dataCache = createData(_typeNumber, _errorCorrectionLevel, _dataList);
68 }
69
70 mapData(_dataCache, maskPattern);
71 };
72
73 const setupPositionProbePattern = function(row, col) {
74
75 for (let r = -1; r <= 7; r += 1) {
76
77 if (row + r <= -1 || _moduleCount <= row + r) continue;
78
79 for (let c = -1; c <= 7; c += 1) {
80
81 if (col + c <= -1 || _moduleCount <= col + c) continue;
82
83 if ( (0 <= r && r <= 6 && (c == 0 || c == 6) )
84 || (0 <= c && c <= 6 && (r == 0 || r == 6) )
85 || (2 <= r && r <= 4 && 2 <= c && c <= 4) ) {
86 _modules[row + r][col + c] = true;
87 } else {
88 _modules[row + r][col + c] = false;
89 }
90 }
91 }
92 };
93
94 const getBestMaskPattern = function() {
95
96 let minLostPoint = 0;
97 let pattern = 0;
98
99 for (let i = 0; i < 8; i += 1) {
100
101 makeImpl(true, i);
102
103 const lostPoint = QRUtil.getLostPoint(_this);
104
105 if (i == 0 || minLostPoint > lostPoint) {
106 minLostPoint = lostPoint;
107 pattern = i;
108 }
109 }
110
111 return pattern;
112 };
113
114 const setupTimingPattern = function() {
115
116 for (let r = 8; r < _moduleCount - 8; r += 1) {
117 if (_modules[r][6] != null) {
118 continue;
119 }
120 _modules[r][6] = (r % 2 == 0);
121 }
122
123 for (let c = 8; c < _moduleCount - 8; c += 1) {
124 if (_modules[6][c] != null) {
125 continue;
126 }
127 _modules[6][c] = (c % 2 == 0);
128 }
129 };
130
131 const setupPositionAdjustPattern = function() {
132
133 const pos = QRUtil.getPatternPosition(_typeNumber);
134
135 for (let i = 0; i < pos.length; i += 1) {
136
137 for (let j = 0; j < pos.length; j += 1) {
138
139 const row = pos[i];
140 const col = pos[j];
141
142 if (_modules[row][col] != null) {
143 continue;
144 }
145
146 for (let r = -2; r <= 2; r += 1) {
147
148 for (let c = -2; c <= 2; c += 1) {
149
150 if (r == -2 || r == 2 || c == -2 || c == 2
151 || (r == 0 && c == 0) ) {
152 _modules[row + r][col + c] = true;
153 } else {
154 _modules[row + r][col + c] = false;
155 }
156 }
157 }
158 }
159 }
160 };
161
162 const setupTypeNumber = function(test) {
163
164 const bits = QRUtil.getBCHTypeNumber(_typeNumber);
165
166 for (let i = 0; i < 18; i += 1) {
167 const mod = (!test && ( (bits >> i) & 1) == 1);
168 _modules[Math.floor(i / 3)][i % 3 + _moduleCount - 8 - 3] = mod;
169 }
170
171 for (let i = 0; i < 18; i += 1) {
172 const mod = (!test && ( (bits >> i) & 1) == 1);
173 _modules[i % 3 + _moduleCount - 8 - 3][Math.floor(i / 3)] = mod;
174 }
175 };
176
177 const setupTypeInfo = function(test, maskPattern) {
178
179 const data = (_errorCorrectionLevel << 3) | maskPattern;
180 const bits = QRUtil.getBCHTypeInfo(data);
181
182 // vertical
183 for (let i = 0; i < 15; i += 1) {
184
185 const mod = (!test && ( (bits >> i) & 1) == 1);
186
187 if (i < 6) {
188 _modules[i][8] = mod;
189 } else if (i < 8) {
190 _modules[i + 1][8] = mod;
191 } else {
192 _modules[_moduleCount - 15 + i][8] = mod;
193 }
194 }
195
196 // horizontal
197 for (let i = 0; i < 15; i += 1) {
198
199 const mod = (!test && ( (bits >> i) & 1) == 1);
200
201 if (i < 8) {
202 _modules[8][_moduleCount - i - 1] = mod;
203 } else if (i < 9) {
204 _modules[8][15 - i - 1 + 1] = mod;
205 } else {
206 _modules[8][15 - i - 1] = mod;
207 }
208 }
209
210 // fixed module
211 _modules[_moduleCount - 8][8] = (!test);
212 };
213
214 const mapData = function(data, maskPattern) {
215
216 let inc = -1;
217 let row = _moduleCount - 1;
218 let bitIndex = 7;
219 let byteIndex = 0;
220 const maskFunc = QRUtil.getMaskFunction(maskPattern);
221
222 for (let col = _moduleCount - 1; col > 0; col -= 2) {
223
224 if (col == 6) col -= 1;
225
226 while (true) {
227
228 for (let c = 0; c < 2; c += 1) {
229
230 if (_modules[row][col - c] == null) {
231
232 let dark = false;
233
234 if (byteIndex < data.length) {
235 dark = ( ( (data[byteIndex] >>> bitIndex) & 1) == 1);
236 }
237
238 const mask = maskFunc(row, col - c);
239
240 if (mask) {
241 dark = !dark;
242 }
243
244 _modules[row][col - c] = dark;
245 bitIndex -= 1;
246
247 if (bitIndex == -1) {
248 byteIndex += 1;
249 bitIndex = 7;
250 }
251 }
252 }
253
254 row += inc;
255
256 if (row < 0 || _moduleCount <= row) {
257 row -= inc;
258 inc = -inc;
259 break;
260 }
261 }
262 }
263 };
264
265 const createBytes = function(buffer, rsBlocks) {
266
267 let offset = 0;
268
269 let maxDcCount = 0;
270 let maxEcCount = 0;
271
272 const dcdata = new Array(rsBlocks.length);
273 const ecdata = new Array(rsBlocks.length);
274
275 for (let r = 0; r < rsBlocks.length; r += 1) {
276
277 const dcCount = rsBlocks[r].dataCount;
278 const ecCount = rsBlocks[r].totalCount - dcCount;
279
280 maxDcCount = Math.max(maxDcCount, dcCount);
281 maxEcCount = Math.max(maxEcCount, ecCount);
282
283 dcdata[r] = new Array(dcCount);
284
285 for (let i = 0; i < dcdata[r].length; i += 1) {
286 dcdata[r][i] = 0xff & buffer.getBuffer()[i + offset];
287 }
288 offset += dcCount;
289
290 const rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);
291 const rawPoly = qrPolynomial(dcdata[r], rsPoly.getLength() - 1);
292
293 const modPoly = rawPoly.mod(rsPoly);
294 ecdata[r] = new Array(rsPoly.getLength() - 1);
295 for (let i = 0; i < ecdata[r].length; i += 1) {
296 const modIndex = i + modPoly.getLength() - ecdata[r].length;
297 ecdata[r][i] = (modIndex >= 0)? modPoly.getAt(modIndex) : 0;
298 }
299 }
300
301 let totalCodeCount = 0;
302 for (let i = 0; i < rsBlocks.length; i += 1) {
303 totalCodeCount += rsBlocks[i].totalCount;
304 }
305
306 const data = new Array(totalCodeCount);
307 let index = 0;
308
309 for (let i = 0; i < maxDcCount; i += 1) {
310 for (let r = 0; r < rsBlocks.length; r += 1) {
311 if (i < dcdata[r].length) {
312 data[index] = dcdata[r][i];
313 index += 1;
314 }
315 }
316 }
317
318 for (let i = 0; i < maxEcCount; i += 1) {
319 for (let r = 0; r < rsBlocks.length; r += 1) {
320 if (i < ecdata[r].length) {
321 data[index] = ecdata[r][i];
322 index += 1;
323 }
324 }
325 }
326
327 return data;
328 };
329
330 const createData = function(typeNumber, errorCorrectionLevel, dataList) {
331
332 const rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectionLevel);
333
334 const buffer = qrBitBuffer();
335
336 for (let i = 0; i < dataList.length; i += 1) {
337 const data = dataList[i];
338 buffer.put(data.getMode(), 4);
339 buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber) );
340 data.write(buffer);
341 }
342
343 // calc num max data.
344 let totalDataCount = 0;
345 for (let i = 0; i < rsBlocks.length; i += 1) {
346 totalDataCount += rsBlocks[i].dataCount;
347 }
348
349 if (buffer.getLengthInBits() > totalDataCount * 8) {
350 throw 'code length overflow. ('
351 + buffer.getLengthInBits()
352 + '>'
353 + totalDataCount * 8
354 + ')';
355 }
356
357 // end code
358 if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
359 buffer.put(0, 4);
360 }
361
362 // padding
363 while (buffer.getLengthInBits() % 8 != 0) {
364 buffer.putBit(false);
365 }
366
367 // padding
368 while (true) {
369
370 if (buffer.getLengthInBits() >= totalDataCount * 8) {
371 break;
372 }
373 buffer.put(PAD0, 8);
374
375 if (buffer.getLengthInBits() >= totalDataCount * 8) {
376 break;
377 }
378 buffer.put(PAD1, 8);
379 }
380
381 return createBytes(buffer, rsBlocks);
382 };
383
384 _this.addData = function(data, mode) {
385
386 mode = mode || 'Byte';
387
388 let newData = null;
389
390 switch(mode) {
391 case 'Numeric' :
392 newData = qrNumber(data);
393 break;
394 case 'Alphanumeric' :
395 newData = qrAlphaNum(data);
396 break;
397 case 'Byte' :
398 newData = qr8BitByte(data);
399 break;
400 case 'Kanji' :
401 newData = qrKanji(data);
402 break;
403 default :
404 throw 'mode:' + mode;
405 }
406
407 _dataList.push(newData);
408 _dataCache = null;
409 };
410
411 _this.isDark = function(row, col) {
412 if (row < 0 || _moduleCount <= row || col < 0 || _moduleCount <= col) {
413 throw row + ',' + col;
414 }
415 return _modules[row][col];
416 };
417
418 _this.getModuleCount = function() {
419 return _moduleCount;
420 };
421
422 _this.make = function() {
423 if (_typeNumber < 1) {
424 let typeNumber = 1;
425
426 for (; typeNumber < 40; typeNumber++) {
427 const rsBlocks = QRRSBlock.getRSBlocks(typeNumber, _errorCorrectionLevel);
428 const buffer = qrBitBuffer();
429
430 for (let i = 0; i < _dataList.length; i++) {
431 const data = _dataList[i];
432 buffer.put(data.getMode(), 4);
433 buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber) );
434 data.write(buffer);
435 }
436
437 let totalDataCount = 0;
438 for (let i = 0; i < rsBlocks.length; i++) {
439 totalDataCount += rsBlocks[i].dataCount;
440 }
441
442 if (buffer.getLengthInBits() <= totalDataCount * 8) {
443 break;
444 }
445 }
446
447 _typeNumber = typeNumber;
448 }
449
450 makeImpl(false, getBestMaskPattern() );
451 };
452
453 _this.createTableTag = function(cellSize, margin) {
454
455 cellSize = cellSize || 2;
456 margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
457
458 let qrHtml = '';
459
460 qrHtml += '<table style="';
461 qrHtml += ' border-width: 0px; border-style: none;';
462 qrHtml += ' border-collapse: collapse;';
463 qrHtml += ' padding: 0px; margin: ' + margin + 'px;';
464 qrHtml += '">';
465 qrHtml += '<tbody>';
466
467 for (let r = 0; r < _this.getModuleCount(); r += 1) {
468
469 qrHtml += '<tr>';
470
471 for (let c = 0; c < _this.getModuleCount(); c += 1) {
472 qrHtml += '<td style="';
473 qrHtml += ' border-width: 0px; border-style: none;';
474 qrHtml += ' border-collapse: collapse;';
475 qrHtml += ' padding: 0px; margin: 0px;';
476 qrHtml += ' width: ' + cellSize + 'px;';
477 qrHtml += ' height: ' + cellSize + 'px;';
478 qrHtml += ' background-color: ';
479 qrHtml += _this.isDark(r, c)? '#000000' : '#ffffff';
480 qrHtml += ';';
481 qrHtml += '"/>';
482 }
483
484 qrHtml += '</tr>';
485 }
486
487 qrHtml += '</tbody>';
488 qrHtml += '</table>';
489
490 return qrHtml;
491 };
492
493 _this.createSvgTag = function(cellSize, margin, alt, title) {
494
495 let opts = {};
496 if (typeof arguments[0] == 'object') {
497 // Called by options.
498 opts = arguments[0];
499 // overwrite cellSize and margin.
500 cellSize = opts.cellSize;
501 margin = opts.margin;
502 alt = opts.alt;
503 title = opts.title;
504 }
505
506 cellSize = cellSize || 2;
507 margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
508
509 // Compose alt property surrogate
510 alt = (typeof alt === 'string') ? {text: alt} : alt || {};
511 alt.text = alt.text || null;
512 alt.id = (alt.text) ? alt.id || 'qrcode-description' : null;
513
514 // Compose title property surrogate
515 title = (typeof title === 'string') ? {text: title} : title || {};
516 title.text = title.text || null;
517 title.id = (title.text) ? title.id || 'qrcode-title' : null;
518
519 const size = _this.getModuleCount() * cellSize + margin * 2;
520 let c, mc, r, mr, qrSvg='', rect;
521
522 rect = 'l' + cellSize + ',0 0,' + cellSize +
523 ' -' + cellSize + ',0 0,-' + cellSize + 'z ';
524
525 qrSvg += '<svg version="1.1" xmlns="http://www.w3.org/2000/svg"';
526 qrSvg += !opts.scalable ? ' width="' + size + 'px" height="' + size + 'px"' : '';
527 qrSvg += ' viewBox="0 0 ' + size + ' ' + size + '" ';
528 qrSvg += ' preserveAspectRatio="xMinYMin meet"';
529 qrSvg += (title.text || alt.text) ? ' role="img" aria-labelledby="' +
530 escapeXml([title.id, alt.id].join(' ').trim() ) + '"' : '';
531 qrSvg += '>';
532 qrSvg += (title.text) ? '<title id="' + escapeXml(title.id) + '">' +
533 escapeXml(title.text) + '</title>' : '';
534 qrSvg += (alt.text) ? '<description id="' + escapeXml(alt.id) + '">' +
535 escapeXml(alt.text) + '</description>' : '';
536 qrSvg += '<rect width="100%" height="100%" fill="white" cx="0" cy="0"/>';
537 qrSvg += '<path d="';
538
539 for (r = 0; r < _this.getModuleCount(); r += 1) {
540 mr = r * cellSize + margin;
541 for (c = 0; c < _this.getModuleCount(); c += 1) {
542 if (_this.isDark(r, c) ) {
543 mc = c*cellSize+margin;
544 qrSvg += 'M' + mc + ',' + mr + rect;
545 }
546 }
547 }
548
549 qrSvg += '" stroke="transparent" fill="black"/>';
550 qrSvg += '</svg>';
551
552 return qrSvg;
553 };
554
555 _this.createDataURL = function(cellSize, margin) {
556
557 cellSize = cellSize || 2;
558 margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
559
560 const size = _this.getModuleCount() * cellSize + margin * 2;
561 const min = margin;
562 const max = size - margin;
563
564 return createDataURL(size, size, function(x, y) {
565 if (min <= x && x < max && min <= y && y < max) {
566 const c = Math.floor( (x - min) / cellSize);
567 const r = Math.floor( (y - min) / cellSize);
568 return _this.isDark(r, c)? 0 : 1;
569 } else {
570 return 1;
571 }
572 } );
573 };
574
575 _this.createImgTag = function(cellSize, margin, alt) {
576
577 cellSize = cellSize || 2;
578 margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
579
580 const size = _this.getModuleCount() * cellSize + margin * 2;
581
582 let img = '';
583 img += '<img';
584 img += '\u0020src="';
585 img += _this.createDataURL(cellSize, margin);
586 img += '"';
587 img += '\u0020width="';
588 img += size;
589 img += '"';
590 img += '\u0020height="';
591 img += size;
592 img += '"';
593 if (alt) {
594 img += '\u0020alt="';
595 img += escapeXml(alt);
596 img += '"';
597 }
598 img += '/>';
599
600 return img;
601 };
602
603 const escapeXml = function(s) {
604 let escaped = '';
605 for (let i = 0; i < s.length; i += 1) {
606 const c = s.charAt(i);
607 switch(c) {
608 case '<': escaped += '<'; break;
609 case '>': escaped += '>'; break;
610 case '&': escaped += '&'; break;
611 case '"': escaped += '"'; break;
612 default : escaped += c; break;
613 }
614 }
615 return escaped;
616 };
617
618 const _createHalfASCII = function(margin) {
619 const cellSize = 1;
620 margin = (typeof margin == 'undefined')? cellSize * 2 : margin;
621
622 const size = _this.getModuleCount() * cellSize + margin * 2;
623 const min = margin;
624 const max = size - margin;
625
626 let y, x, r1, r2, p;
627
628 const blocks = {
629 '██': '█',
630 '█ ': '▀',
631 ' █': '▄',
632 ' ': ' '
633 };
634
635 const blocksLastLineNoMargin = {
636 '██': '▀',
637 '█ ': '▀',
638 ' █': ' ',
639 ' ': ' '
640 };
641
642 let ascii = '';
643 for (y = 0; y < size; y += 2) {
644 r1 = Math.floor((y - min) / cellSize);
645 r2 = Math.floor((y + 1 - min) / cellSize);
646 for (x = 0; x < size; x += 1) {
647 p = '█';
648
649 if (min <= x && x < max && min <= y && y < max && _this.isDark(r1, Math.floor((x - min) / cellSize))) {
650 p = ' ';
651 }
652
653 if (min <= x && x < max && min <= y+1 && y+1 < max && _this.isDark(r2, Math.floor((x - min) / cellSize))) {
654 p += ' ';
655 }
656 else {
657 p += '█';
658 }
659
660 // Output 2 characters per pixel, to create full square. 1 character per pixels gives only half width of square.
661 ascii += (margin < 1 && y+1 >= max) ? blocksLastLineNoMargin[p] : blocks[p];
662 }
663
664 ascii += '\n';
665 }
666
667 if (size % 2 && margin > 0) {
668 return ascii.substring(0, ascii.length - size - 1) + Array(size+1).join('▀');
669 }
670
671 return ascii.substring(0, ascii.length-1);
672 };
673
674 _this.createASCII = function(cellSize, margin) {
675 cellSize = cellSize || 1;
676
677 if (cellSize < 2) {
678 return _createHalfASCII(margin);
679 }
680
681 cellSize -= 1;
682 margin = (typeof margin == 'undefined')? cellSize * 2 : margin;
683
684 const size = _this.getModuleCount() * cellSize + margin * 2;
685 const min = margin;
686 const max = size - margin;
687
688 let y, x, r, p;
689
690 const white = Array(cellSize+1).join('██');
691 const black = Array(cellSize+1).join(' ');
692
693 let ascii = '';
694 let line = '';
695 for (y = 0; y < size; y += 1) {
696 r = Math.floor( (y - min) / cellSize);
697 line = '';
698 for (x = 0; x < size; x += 1) {
699 p = 1;
700
701 if (min <= x && x < max && min <= y && y < max && _this.isDark(r, Math.floor((x - min) / cellSize))) {
702 p = 0;
703 }
704
705 // Output 2 characters per pixel, to create full square. 1 character per pixels gives only half width of square.
706 line += p ? white : black;
707 }
708
709 for (r = 0; r < cellSize; r += 1) {
710 ascii += line + '\n';
711 }
712 }
713
714 return ascii.substring(0, ascii.length-1);
715 };
716
717 _this.renderTo2dContext = function(context, cellSize) {
718 cellSize = cellSize || 2;
719 const length = _this.getModuleCount();
720 for (let row = 0; row < length; row++) {
721 for (let col = 0; col < length; col++) {
722 context.fillStyle = _this.isDark(row, col) ? 'black' : 'white';
723 context.fillRect(col * cellSize, row * cellSize, cellSize, cellSize);
724 }
725 }
726 }
727
728 return _this;
729};
730
731//---------------------------------------------------------------------
732// qrcode.stringToBytes
733//---------------------------------------------------------------------
734
735qrcode.stringToBytes = function(s) {
736 const bytes = [];
737 for (let i = 0; i < s.length; i += 1) {
738 const c = s.charCodeAt(i);
739 bytes.push(c & 0xff);
740 }
741 return bytes;
742};
743
744//---------------------------------------------------------------------
745// qrcode.createStringToBytes
746//---------------------------------------------------------------------
747
748/**
749 * @param unicodeData base64 string of byte array.
750 * [16bit Unicode],[16bit Bytes], ...
751 * @param numChars
752 */
753qrcode.createStringToBytes = function(unicodeData, numChars) {
754
755 // create conversion map.
756
757 const unicodeMap = function() {
758
759 const bin = base64DecodeInputStream(unicodeData);
760 const read = function() {
761 const b = bin.read();
762 if (b == -1) throw 'eof';
763 return b;
764 };
765
766 let count = 0;
767 const unicodeMap = {};
768 while (true) {
769 const b0 = bin.read();
770 if (b0 == -1) break;
771 const b1 = read();
772 const b2 = read();
773 const b3 = read();
774 const k = String.fromCharCode( (b0 << 8) | b1);
775 const v = (b2 << 8) | b3;
776 unicodeMap[k] = v;
777 count += 1;
778 }
779 if (count != numChars) {
780 throw count + ' != ' + numChars;
781 }
782
783 return unicodeMap;
784 }();
785
786 const unknownChar = '?'.charCodeAt(0);
787
788 return function(s) {
789 const bytes = [];
790 for (let i = 0; i < s.length; i += 1) {
791 const c = s.charCodeAt(i);
792 if (c < 128) {
793 bytes.push(c);
794 } else {
795 const b = unicodeMap[s.charAt(i)];
796 if (typeof b == 'number') {
797 if ( (b & 0xff) == b) {
798 // 1byte
799 bytes.push(b);
800 } else {
801 // 2bytes
802 bytes.push(b >>> 8);
803 bytes.push(b & 0xff);
804 }
805 } else {
806 bytes.push(unknownChar);
807 }
808 }
809 }
810 return bytes;
811 };
812};
813
814//---------------------------------------------------------------------
815// QRMode
816//---------------------------------------------------------------------
817
818const QRMode = {
819 MODE_NUMBER : 1 << 0,
820 MODE_ALPHA_NUM : 1 << 1,
821 MODE_8BIT_BYTE : 1 << 2,
822 MODE_KANJI : 1 << 3
823};
824
825//---------------------------------------------------------------------
826// QRErrorCorrectionLevel
827//---------------------------------------------------------------------
828
829const QRErrorCorrectionLevel = {
830 L : 1,
831 M : 0,
832 Q : 3,
833 H : 2
834};
835
836//---------------------------------------------------------------------
837// QRMaskPattern
838//---------------------------------------------------------------------
839
840const QRMaskPattern = {
841 PATTERN000 : 0,
842 PATTERN001 : 1,
843 PATTERN010 : 2,
844 PATTERN011 : 3,
845 PATTERN100 : 4,
846 PATTERN101 : 5,
847 PATTERN110 : 6,
848 PATTERN111 : 7
849};
850
851//---------------------------------------------------------------------
852// QRUtil
853//---------------------------------------------------------------------
854
855const QRUtil = function() {
856
857 const PATTERN_POSITION_TABLE = [
858 [],
859 [6, 18],
860 [6, 22],
861 [6, 26],
862 [6, 30],
863 [6, 34],
864 [6, 22, 38],
865 [6, 24, 42],
866 [6, 26, 46],
867 [6, 28, 50],
868 [6, 30, 54],
869 [6, 32, 58],
870 [6, 34, 62],
871 [6, 26, 46, 66],
872 [6, 26, 48, 70],
873 [6, 26, 50, 74],
874 [6, 30, 54, 78],
875 [6, 30, 56, 82],
876 [6, 30, 58, 86],
877 [6, 34, 62, 90],
878 [6, 28, 50, 72, 94],
879 [6, 26, 50, 74, 98],
880 [6, 30, 54, 78, 102],
881 [6, 28, 54, 80, 106],
882 [6, 32, 58, 84, 110],
883 [6, 30, 58, 86, 114],
884 [6, 34, 62, 90, 118],
885 [6, 26, 50, 74, 98, 122],
886 [6, 30, 54, 78, 102, 126],
887 [6, 26, 52, 78, 104, 130],
888 [6, 30, 56, 82, 108, 134],
889 [6, 34, 60, 86, 112, 138],
890 [6, 30, 58, 86, 114, 142],
891 [6, 34, 62, 90, 118, 146],
892 [6, 30, 54, 78, 102, 126, 150],
893 [6, 24, 50, 76, 102, 128, 154],
894 [6, 28, 54, 80, 106, 132, 158],
895 [6, 32, 58, 84, 110, 136, 162],
896 [6, 26, 54, 82, 110, 138, 166],
897 [6, 30, 58, 86, 114, 142, 170]
898 ];
899 const G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0);
900 const G18 = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0);
901 const G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1);
902
903 const _this = {};
904
905 const getBCHDigit = function(data) {
906 let digit = 0;
907 while (data != 0) {
908 digit += 1;
909 data >>>= 1;
910 }
911 return digit;
912 };
913
914 _this.getBCHTypeInfo = function(data) {
915 let d = data << 10;
916 while (getBCHDigit(d) - getBCHDigit(G15) >= 0) {
917 d ^= (G15 << (getBCHDigit(d) - getBCHDigit(G15) ) );
918 }
919 return ( (data << 10) | d) ^ G15_MASK;
920 };
921
922 _this.getBCHTypeNumber = function(data) {
923 let d = data << 12;
924 while (getBCHDigit(d) - getBCHDigit(G18) >= 0) {
925 d ^= (G18 << (getBCHDigit(d) - getBCHDigit(G18) ) );
926 }
927 return (data << 12) | d;
928 };
929
930 _this.getPatternPosition = function(typeNumber) {
931 return PATTERN_POSITION_TABLE[typeNumber - 1];
932 };
933
934 _this.getMaskFunction = function(maskPattern) {
935
936 switch (maskPattern) {
937
938 case QRMaskPattern.PATTERN000 :
939 return function(i, j) { return (i + j) % 2 == 0; };
940 case QRMaskPattern.PATTERN001 :
941 return function(i, j) { return i % 2 == 0; };
942 case QRMaskPattern.PATTERN010 :
943 return function(i, j) { return j % 3 == 0; };
944 case QRMaskPattern.PATTERN011 :
945 return function(i, j) { return (i + j) % 3 == 0; };
946 case QRMaskPattern.PATTERN100 :
947 return function(i, j) { return (Math.floor(i / 2) + Math.floor(j / 3) ) % 2 == 0; };
948 case QRMaskPattern.PATTERN101 :
949 return function(i, j) { return (i * j) % 2 + (i * j) % 3 == 0; };
950 case QRMaskPattern.PATTERN110 :
951 return function(i, j) { return ( (i * j) % 2 + (i * j) % 3) % 2 == 0; };
952 case QRMaskPattern.PATTERN111 :
953 return function(i, j) { return ( (i * j) % 3 + (i + j) % 2) % 2 == 0; };
954
955 default :
956 throw 'bad maskPattern:' + maskPattern;
957 }
958 };
959
960 _this.getErrorCorrectPolynomial = function(errorCorrectLength) {
961 let a = qrPolynomial([1], 0);
962 for (let i = 0; i < errorCorrectLength; i += 1) {
963 a = a.multiply(qrPolynomial([1, QRMath.gexp(i)], 0) );
964 }
965 return a;
966 };
967
968 _this.getLengthInBits = function(mode, type) {
969
970 if (1 <= type && type < 10) {
971
972 // 1 - 9
973
974 switch(mode) {
975 case QRMode.MODE_NUMBER : return 10;
976 case QRMode.MODE_ALPHA_NUM : return 9;
977 case QRMode.MODE_8BIT_BYTE : return 8;
978 case QRMode.MODE_KANJI : return 8;
979 default :
980 throw 'mode:' + mode;
981 }
982
983 } else if (type < 27) {
984
985 // 10 - 26
986
987 switch(mode) {
988 case QRMode.MODE_NUMBER : return 12;
989 case QRMode.MODE_ALPHA_NUM : return 11;
990 case QRMode.MODE_8BIT_BYTE : return 16;
991 case QRMode.MODE_KANJI : return 10;
992 default :
993 throw 'mode:' + mode;
994 }
995
996 } else if (type < 41) {
997
998 // 27 - 40
999
1000 switch(mode) {
1001 case QRMode.MODE_NUMBER : return 14;
1002 case QRMode.MODE_ALPHA_NUM : return 13;
1003 case QRMode.MODE_8BIT_BYTE : return 16;
1004 case QRMode.MODE_KANJI : return 12;
1005 default :
1006 throw 'mode:' + mode;
1007 }
1008
1009 } else {
1010 throw 'type:' + type;
1011 }
1012 };
1013
1014 _this.getLostPoint = function(qrcode) {
1015
1016 const moduleCount = qrcode.getModuleCount();
1017
1018 let lostPoint = 0;
1019
1020 // LEVEL1
1021
1022 for (let row = 0; row < moduleCount; row += 1) {
1023 for (let col = 0; col < moduleCount; col += 1) {
1024
1025 let sameCount = 0;
1026 const dark = qrcode.isDark(row, col);
1027
1028 for (let r = -1; r <= 1; r += 1) {
1029
1030 if (row + r < 0 || moduleCount <= row + r) {
1031 continue;
1032 }
1033
1034 for (let c = -1; c <= 1; c += 1) {
1035
1036 if (col + c < 0 || moduleCount <= col + c) {
1037 continue;
1038 }
1039
1040 if (r == 0 && c == 0) {
1041 continue;
1042 }
1043
1044 if (dark == qrcode.isDark(row + r, col + c) ) {
1045 sameCount += 1;
1046 }
1047 }
1048 }
1049
1050 if (sameCount > 5) {
1051 lostPoint += (3 + sameCount - 5);
1052 }
1053 }
1054 };
1055
1056 // LEVEL2
1057
1058 for (let row = 0; row < moduleCount - 1; row += 1) {
1059 for (let col = 0; col < moduleCount - 1; col += 1) {
1060 let count = 0;
1061 if (qrcode.isDark(row, col) ) count += 1;
1062 if (qrcode.isDark(row + 1, col) ) count += 1;
1063 if (qrcode.isDark(row, col + 1) ) count += 1;
1064 if (qrcode.isDark(row + 1, col + 1) ) count += 1;
1065 if (count == 0 || count == 4) {
1066 lostPoint += 3;
1067 }
1068 }
1069 }
1070
1071 // LEVEL3
1072
1073 for (let row = 0; row < moduleCount; row += 1) {
1074 for (let col = 0; col < moduleCount - 6; col += 1) {
1075 if (qrcode.isDark(row, col)
1076 && !qrcode.isDark(row, col + 1)
1077 && qrcode.isDark(row, col + 2)
1078 && qrcode.isDark(row, col + 3)
1079 && qrcode.isDark(row, col + 4)
1080 && !qrcode.isDark(row, col + 5)
1081 && qrcode.isDark(row, col + 6) ) {
1082 lostPoint += 40;
1083 }
1084 }
1085 }
1086
1087 for (let col = 0; col < moduleCount; col += 1) {
1088 for (let row = 0; row < moduleCount - 6; row += 1) {
1089 if (qrcode.isDark(row, col)
1090 && !qrcode.isDark(row + 1, col)
1091 && qrcode.isDark(row + 2, col)
1092 && qrcode.isDark(row + 3, col)
1093 && qrcode.isDark(row + 4, col)
1094 && !qrcode.isDark(row + 5, col)
1095 && qrcode.isDark(row + 6, col) ) {
1096 lostPoint += 40;
1097 }
1098 }
1099 }
1100
1101 // LEVEL4
1102
1103 let darkCount = 0;
1104
1105 for (let col = 0; col < moduleCount; col += 1) {
1106 for (let row = 0; row < moduleCount; row += 1) {
1107 if (qrcode.isDark(row, col) ) {
1108 darkCount += 1;
1109 }
1110 }
1111 }
1112
1113 const ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5;
1114 lostPoint += ratio * 10;
1115
1116 return lostPoint;
1117 };
1118
1119 return _this;
1120}();
1121
1122//---------------------------------------------------------------------
1123// QRMath
1124//---------------------------------------------------------------------
1125
1126const QRMath = function() {
1127
1128 const EXP_TABLE = new Array(256);
1129 const LOG_TABLE = new Array(256);
1130
1131 // initialize tables
1132 for (let i = 0; i < 8; i += 1) {
1133 EXP_TABLE[i] = 1 << i;
1134 }
1135 for (let i = 8; i < 256; i += 1) {
1136 EXP_TABLE[i] = EXP_TABLE[i - 4]
1137 ^ EXP_TABLE[i - 5]
1138 ^ EXP_TABLE[i - 6]
1139 ^ EXP_TABLE[i - 8];
1140 }
1141 for (let i = 0; i < 255; i += 1) {
1142 LOG_TABLE[EXP_TABLE[i] ] = i;
1143 }
1144
1145 const _this = {};
1146
1147 _this.glog = function(n) {
1148
1149 if (n < 1) {
1150 throw 'glog(' + n + ')';
1151 }
1152
1153 return LOG_TABLE[n];
1154 };
1155
1156 _this.gexp = function(n) {
1157
1158 while (n < 0) {
1159 n += 255;
1160 }
1161
1162 while (n >= 256) {
1163 n -= 255;
1164 }
1165
1166 return EXP_TABLE[n];
1167 };
1168
1169 return _this;
1170}();
1171
1172//---------------------------------------------------------------------
1173// qrPolynomial
1174//---------------------------------------------------------------------
1175
1176const qrPolynomial = function(num, shift) {
1177
1178 if (typeof num.length == 'undefined') {
1179 throw num.length + '/' + shift;
1180 }
1181
1182 const _num = function() {
1183 let offset = 0;
1184 while (offset < num.length && num[offset] == 0) {
1185 offset += 1;
1186 }
1187 const _num = new Array(num.length - offset + shift);
1188 for (let i = 0; i < num.length - offset; i += 1) {
1189 _num[i] = num[i + offset];
1190 }
1191 return _num;
1192 }();
1193
1194 const _this = {};
1195
1196 _this.getAt = function(index) {
1197 return _num[index];
1198 };
1199
1200 _this.getLength = function() {types/index.d.ts 29 lines1// One listening port; hooks/ports.ts holds the same shape.
2export type PortRow = {
3 port: number
4 pid: number
5 process: string
6 owner: string
7 kind: 'dev' | 'app' | 'system'
8 bind: 'local' | 'all' | 'tailnet' | 'other'
9 address: string
10 url: string
11 phoneUrl: string
12}
13
14declare module 'claude-code' {
15 interface PluginState {
16 'port-watch': {
17 rows: PortRow[]
18 scannedAt: number
19 scanning: boolean
20 filter: 'dev' | 'phone' | 'all'
21 query: string
22 qr: number // the port whose QR code is open, 0 for none
23 page: number
24 tailnetIp: string
25 error: string
26 }
27 }
28}
29