@seanmars/tospec 0.14.1 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Codex skill duration metrics — a second, less precise source for the same
3
+ * report `skill-metrics.ts` builds from Claude Code transcripts.
4
+ *
5
+ * Codex keeps one session tree shared by every project
6
+ * (`<CODEX_HOME or ~/.codex>/sessions/<yyyy>/<mm>/<dd>/rollout-*.jsonl`),
7
+ * partitioned by date rather than by project, with the working directory
8
+ * recorded once per session (`type: "session_meta"`, `payload.cwd`) rather than
9
+ * encoded into a directory name. Project scoping therefore reads that field
10
+ * instead of resolving a path (see codex-metrics-source spec, "Codex sessions
11
+ * are scoped to a project by their recorded working directory").
12
+ *
13
+ * Codex transcripts carry nothing like Claude Code's `attributionSkill`. Every
14
+ * session's system prompt lists all installed skills identically whether or
15
+ * not any of them were used, so that listing carries no signal. The one event
16
+ * confirmed (against this project's own real `~/.codex` history) to correlate
17
+ * with a skill actually being used is a tool-call command whose text reads a
18
+ * path ending in `<skill-name>/SKILL.md` — the moment the agent pulls in that
19
+ * skill's instructions. Skill attribution here is therefore a heuristic, not a
20
+ * recorded fact, and undercounts by construction: a skill run that never
21
+ * re-reads its own instructions file is invisible to it.
22
+ */
23
+ import { promises as fs } from 'node:fs';
24
+ import * as os from 'node:os';
25
+ import * as path from 'node:path';
26
+ import fg from 'fast-glob';
27
+ import { aggregateRuns, buildRuns, normalizeSkillName, DEFAULT_IDLE_GAP_MS, DEFAULT_SPLIT_GAP_MS, } from './skill-metrics.js';
28
+ const CODEX_HOME_DIR_NAME = '.codex';
29
+ /**
30
+ * Locates Codex's session tree. `CODEX_HOME` is honoured the same way
31
+ * `CLAUDE_CONFIG_DIR` is for Claude Code; without it the default is
32
+ * `~/.codex`. This is the directory to search, not a per-project one — Codex
33
+ * has no equivalent of Claude Code's per-project transcript directory.
34
+ */
35
+ export function resolveCodexSessionsDir(options = {}) {
36
+ const env = options.env ?? process.env;
37
+ const home = env.CODEX_HOME
38
+ ? path.resolve(env.CODEX_HOME)
39
+ : path.join(options.homedir ?? os.homedir(), CODEX_HOME_DIR_NAME);
40
+ return path.join(home, 'sessions');
41
+ }
42
+ /**
43
+ * Whether a session's recorded working directory names the given project.
44
+ * `null` (no `session_meta` seen, or no `cwd` field) never matches — a session
45
+ * with nothing recorded cannot be attributed to any project.
46
+ */
47
+ export function sessionBelongsToProject(cwd, rootPath) {
48
+ if (cwd === null)
49
+ return false;
50
+ const a = path.resolve(cwd);
51
+ const b = path.resolve(rootPath);
52
+ return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
53
+ }
54
+ /**
55
+ * The working directory a record records, or null when it is not the kind of
56
+ * record that carries one. Shared by the full parse and the prefix probe so a
57
+ * single statement defines a session's cwd — the first `session_meta` whose
58
+ * `payload.cwd` is a string — and the probe can never disagree with the parse
59
+ * it exists to skip.
60
+ */
61
+ function recordedCwd(record) {
62
+ if (record.type !== 'session_meta')
63
+ return null;
64
+ const cwd = record.payload?.cwd;
65
+ return typeof cwd === 'string' ? cwd : null;
66
+ }
67
+ /**
68
+ * Matches a tool-call command's text against a path ending in
69
+ * `<skill-name>/SKILL.md`, on either separator. This is the one signal
70
+ * confirmed to correlate with a skill actually being read, as opposed to
71
+ * merely appearing in the session's fixed skill-listing text (see the module
72
+ * docstring).
73
+ *
74
+ * `function_call` arguments are JSON-encoded twice — once as the tool-call
75
+ * argument string itself, once more for the outer transcript line — so a
76
+ * Windows path's single backslash separator survives as a doubled `\\` in the
77
+ * text this matches against. The separator class therefore matches one or
78
+ * more repetitions rather than exactly one.
79
+ */
80
+ const SKILL_MD_READ_PATTERN = /([^\\/"]+)[\\/]+SKILL\.md/i;
81
+ function extractSkillFromCommandText(text) {
82
+ const match = SKILL_MD_READ_PATTERN.exec(text);
83
+ return match ? match[1] : null;
84
+ }
85
+ /**
86
+ * Parses one Codex rollout `.jsonl` file into the recorded project directory
87
+ * plus a sorted, timestamped, skill-labelled entry list — the same shape
88
+ * `parseTranscript` produces for Claude Code, so `buildRuns`/`aggregateRuns`
89
+ * apply unchanged (design.md D3). Malformed lines are skipped rather than
90
+ * fatal, for the same reason Claude Code transcripts are: an append-only log
91
+ * that can be torn at the tail while a session is live.
92
+ *
93
+ * Only `function_call`/`custom_tool_call` entries are ever inspected for a
94
+ * `SKILL.md` read. Every other entry — including the one carrying the fixed
95
+ * skill-listing text every session opens with — is folded in unattributed
96
+ * (`skill: null`), exactly as an unattributed Claude Code entry is: it cannot
97
+ * end a run, but it is not evidence a skill became active either.
98
+ */
99
+ export function parseCodexSession(text) {
100
+ let cwd = null;
101
+ const entries = [];
102
+ for (const line of text.split(/\r?\n/)) {
103
+ if (!line.trim())
104
+ continue;
105
+ let record;
106
+ try {
107
+ record = JSON.parse(line);
108
+ }
109
+ catch {
110
+ continue;
111
+ }
112
+ if (typeof record.timestamp !== 'string')
113
+ continue;
114
+ const timeMs = Date.parse(record.timestamp);
115
+ if (Number.isNaN(timeMs))
116
+ continue;
117
+ const payload = record.payload;
118
+ if (cwd === null) {
119
+ const found = recordedCwd(record);
120
+ if (found !== null) {
121
+ cwd = found;
122
+ continue;
123
+ }
124
+ }
125
+ let skill = null;
126
+ if (record.type === 'response_item' &&
127
+ payload &&
128
+ (payload.type === 'function_call' || payload.type === 'custom_tool_call')) {
129
+ const commandText = (typeof payload.arguments === 'string' ? payload.arguments : '') +
130
+ (typeof payload.input === 'string' ? payload.input : '');
131
+ const found = extractSkillFromCommandText(commandText);
132
+ if (found)
133
+ skill = normalizeSkillName(found);
134
+ }
135
+ entries.push({ timeMs, skill });
136
+ }
137
+ entries.sort((a, b) => a.timeMs - b.timeMs);
138
+ return { cwd, entries };
139
+ }
140
+ // -----------------------------------------------------------------------------
141
+ // IO: project scoping without reading whole sessions
142
+ // -----------------------------------------------------------------------------
143
+ /**
144
+ * Bytes read from a session when only its `cwd` is wanted.
145
+ *
146
+ * Codex writes `session_meta` as the very first line, but that line embeds the
147
+ * session's whole base-instructions text and so runs to ~20 KB. The probe has
148
+ * to be large enough to hold a *complete* line: a line cut by the read boundary
149
+ * fails `JSON.parse` and is indistinguishable from an absent one. 64 KiB clears
150
+ * the observed maximum several times over, and anything the probe cannot settle
151
+ * falls back to a full read — so an unusually long first line costs time, never
152
+ * correctness.
153
+ */
154
+ const CWD_PROBE_BYTES = 64 * 1024;
155
+ /** First recorded cwd in already-read text, stopping at the first one found. */
156
+ function findRecordedCwd(text) {
157
+ for (const line of text.split(/\r?\n/)) {
158
+ if (!line.trim())
159
+ continue;
160
+ let record;
161
+ try {
162
+ record = JSON.parse(line);
163
+ }
164
+ catch {
165
+ continue;
166
+ }
167
+ const cwd = recordedCwd(record);
168
+ if (cwd !== null)
169
+ return cwd;
170
+ }
171
+ return null;
172
+ }
173
+ /**
174
+ * Reads just enough of a session to decide which project it belongs to.
175
+ *
176
+ * Every session in the shared Codex tree has to be classified, but only the
177
+ * matching ones are worth parsing, and on a real machine the tree is dominated
178
+ * by other projects: the history that motivated this held 716 MB of sessions of
179
+ * which one project owned 0.9 MB, and parsing all of it cost ~9 s of the
180
+ * command's startup. Because the deciding field sits on line 0, reading a prefix
181
+ * instead of the whole file makes the cost proportional to this project's share
182
+ * of Codex history rather than to all of it.
183
+ */
184
+ async function probeSessionCwd(filePath) {
185
+ const handle = await fs.open(filePath, 'r');
186
+ try {
187
+ const buffer = Buffer.alloc(CWD_PROBE_BYTES);
188
+ const { bytesRead } = await handle.read(buffer, 0, CWD_PROBE_BYTES, 0);
189
+ const cwd = findRecordedCwd(buffer.subarray(0, bytesRead).toString('utf8'));
190
+ // A short read means the prefix was the entire file, so "no session_meta
191
+ // here" is the final answer rather than a reason to read it again.
192
+ return { cwd, conclusive: cwd !== null || bytesRead < CWD_PROBE_BYTES };
193
+ }
194
+ finally {
195
+ await handle.close();
196
+ }
197
+ }
198
+ /**
199
+ * Finds and parses every Codex session belonging to one project.
200
+ *
201
+ * Shared by `collectCodexSkillMetrics` and the metrics server, which need
202
+ * exactly this and previously each carried their own copy of the walk — so the
203
+ * probe optimisation could otherwise have been applied to one and not the other.
204
+ */
205
+ export async function scanCodexSessions(rootPath, options = {}) {
206
+ const sessionsDir = resolveCodexSessionsDir(options);
207
+ // fast-glob returns an empty list for a missing `cwd` rather than throwing,
208
+ // so existence has to be checked directly — otherwise "Codex never used on
209
+ // this machine" (available: false) would be indistinguishable from "used,
210
+ // but nothing here matches this project" (available: true, sessions: 0).
211
+ const stat = await fs.stat(sessionsDir).catch(() => null);
212
+ if (!stat)
213
+ return { sessionsDir, available: false, sessions: [] };
214
+ const filePaths = await fg('**/*.jsonl', { cwd: sessionsDir, absolute: true });
215
+ const sessions = [];
216
+ for (const filePath of filePaths) {
217
+ try {
218
+ const probe = await probeSessionCwd(filePath);
219
+ if (probe.conclusive && !sessionBelongsToProject(probe.cwd, rootPath))
220
+ continue;
221
+ // The probe only ever skips work; the full parse stays authoritative for
222
+ // every session that survives it.
223
+ const { cwd, entries } = parseCodexSession(await fs.readFile(filePath, 'utf8'));
224
+ if (!sessionBelongsToProject(cwd, rootPath))
225
+ continue;
226
+ sessions.push({ session: path.basename(filePath, '.jsonl'), entries });
227
+ }
228
+ catch {
229
+ // A session file can vanish between listing and read; skip it.
230
+ continue;
231
+ }
232
+ }
233
+ return { sessionsDir, available: true, sessions };
234
+ }
235
+ /**
236
+ * Derives skill timings from the `SKILL.md`-read heuristic over this project's
237
+ * Codex sessions — the Codex-flavoured equivalent of `collectSkillMetrics`.
238
+ * `scanCodexSessions` does the finding and scoping.
239
+ *
240
+ * A missing sessions directory is a reportable state (`available: false`), not
241
+ * an error, for the same reason it is for Claude Code: the project may simply
242
+ * never have been opened in Codex.
243
+ */
244
+ export async function collectCodexSkillMetrics(rootPath, options = {}) {
245
+ const thresholds = {
246
+ idleGapMs: options.idleGapMs ?? DEFAULT_IDLE_GAP_MS,
247
+ splitGapMs: options.splitGapMs ?? DEFAULT_SPLIT_GAP_MS,
248
+ };
249
+ const scan = await scanCodexSessions(rootPath, options);
250
+ if (!scan.available) {
251
+ return {
252
+ source: {
253
+ transcriptDir: scan.sessionsDir,
254
+ available: false,
255
+ sessions: 0,
256
+ attributedSessions: 0,
257
+ firstSeen: null,
258
+ lastSeen: null,
259
+ },
260
+ thresholds,
261
+ skills: [],
262
+ runs: [],
263
+ };
264
+ }
265
+ let attributedSessions = 0;
266
+ const allRuns = [];
267
+ for (const { session, entries } of scan.sessions) {
268
+ const runs = buildRuns(entries, session, { ...thresholds, source: 'codex' });
269
+ if (runs.length > 0)
270
+ attributedSessions++;
271
+ for (const run of runs) {
272
+ if (options.sinceMs !== undefined && run.endMs < options.sinceMs)
273
+ continue;
274
+ allRuns.push(run);
275
+ }
276
+ }
277
+ allRuns.sort((a, b) => a.startMs - b.startMs);
278
+ const source = {
279
+ transcriptDir: scan.sessionsDir,
280
+ available: true,
281
+ sessions: scan.sessions.length,
282
+ attributedSessions,
283
+ firstSeen: allRuns.length > 0 ? new Date(allRuns[0].startMs).toISOString() : null,
284
+ lastSeen: allRuns.length > 0 ? new Date(Math.max(...allRuns.map((r) => r.endMs))).toISOString() : null,
285
+ };
286
+ return {
287
+ source,
288
+ thresholds,
289
+ skills: aggregateRuns(allRuns),
290
+ runs: allRuns,
291
+ };
292
+ }
293
+ //# sourceMappingURL=codex-metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-metrics.js","sourceRoot":"","sources":["../../src/core/codex-metrics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Plumbing shared by every local web command (`dashboard`, `metrics`).
3
+ *
4
+ * Two of these are trust boundaries, and that is the reason this module exists
5
+ * rather than each command carrying its own copy: `isAllowedHostHeader` is a
6
+ * DNS-rebinding guard and `serveAsset` is a path-escape guard. A second
7
+ * implementation of either is a hole that survives fixing the first one.
8
+ *
9
+ * `serveAsset` takes its root directory as an argument — the commands serve
10
+ * different asset trees, and parameterising the directory is what lets one
11
+ * implementation serve both.
12
+ */
13
+ import * as http from 'node:http';
14
+ /** Hosts considered loopback for binding and Host-header checks. */
15
+ export declare const LOOPBACK_HOSTS: Set<string>;
16
+ export declare function sendJson(res: http.ServerResponse, status: number, payload: unknown): void;
17
+ /**
18
+ * Absolute path to a shipped asset tree (`assets/<name>`), resolved relative to
19
+ * this module's own URL.
20
+ *
21
+ * The depth is load-bearing: compiled to `dist/core/`, two levels up is the
22
+ * package root, which is also what `dist/commands/` yields. Assets are shipped
23
+ * as package `files` and never compiled, so moving this module to a different
24
+ * depth would silently resolve outside the package. Every caller goes through
25
+ * here so that constraint has exactly one home.
26
+ */
27
+ export declare function packageAssetsDir(name: string): string;
28
+ /**
29
+ * Serves one file from `dir`. The requested name is confined to that directory:
30
+ * URL parsing already normalises `..`, but a resolved path outside `dir` is
31
+ * refused regardless, since this is the only thing standing between a crafted
32
+ * request and the rest of the filesystem.
33
+ */
34
+ export declare function serveAsset(res: http.ServerResponse, dir: string, rawName: string): Promise<void>;
35
+ /**
36
+ * DNS-rebinding guard: only requests whose Host header names loopback (or the
37
+ * explicitly bound non-loopback address) are served. A malicious page can make
38
+ * a victim's browser resolve an attacker domain to 127.0.0.1 and read the
39
+ * server's endpoints; the attacker domain in the Host header is what gives that
40
+ * away.
41
+ */
42
+ export declare function isAllowedHostHeader(hostHeader: string | undefined, boundHost: string): boolean;
43
+ /** Resolves once the server is listening, or rejects with the listen error. */
44
+ export declare function listenOnce(server: http.Server, port: number, host: string): Promise<void>;
45
+ /**
46
+ * Binds `basePort`, advancing to the next port on `EADDRINUSE` until one is
47
+ * free, and returns the port actually bound. Every local command wants the same
48
+ * rule — a predictable port you can keep in your head, that still yields rather
49
+ * than failing when something already holds it.
50
+ *
51
+ * `port: 0` is passed straight through: the OS picks a free port itself, so
52
+ * there is nothing to retry and incrementing from whatever it chose would be
53
+ * meaningless.
54
+ *
55
+ * The attempt bound is a backstop, not a policy: without it a machine that
56
+ * refuses every bind for some other reason would spin through 65k ports.
57
+ */
58
+ export declare function listenFrom(server: http.Server, basePort: number, host: string, maxAttempts?: number): Promise<number>;
59
+ /**
60
+ * Builds the platform command that opens `url` in the default browser.
61
+ * Pure (no side effects) so the platform branching is testable without
62
+ * launching anything.
63
+ */
64
+ export declare function browserOpenCommand(url: string, platform?: NodeJS.Platform): string;
65
+ /** Opens `url` in a browser; a failure only warns (the URL is already printed). */
66
+ export declare function openBrowser(url: string): void;
67
+ //# sourceMappingURL=local-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-server.d.ts","sourceRoot":"","sources":["../../src/core/local-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAYlC,oEAAoE;AACpE,eAAO,MAAM,cAAc,aAA6C,CAAC;AAMzE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAGzF;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGrD;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAkBf;AAMD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAQ9F;AAMD,+EAA+E;AAC/E,wBAAgB,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAczF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,IAAI,CAAC,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,WAAW,SAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CAiBjB;AAMD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,MAAM,CAIR;AAED,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAQ7C"}
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Plumbing shared by every local web command (`dashboard`, `metrics`).
3
+ *
4
+ * Two of these are trust boundaries, and that is the reason this module exists
5
+ * rather than each command carrying its own copy: `isAllowedHostHeader` is a
6
+ * DNS-rebinding guard and `serveAsset` is a path-escape guard. A second
7
+ * implementation of either is a hole that survives fixing the first one.
8
+ *
9
+ * `serveAsset` takes its root directory as an argument — the commands serve
10
+ * different asset trees, and parameterising the directory is what lets one
11
+ * implementation serve both.
12
+ */
13
+ import { promises as fs } from 'node:fs';
14
+ import * as path from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { exec } from 'node:child_process';
17
+ const ASSET_CONTENT_TYPES = {
18
+ '.html': 'text/html',
19
+ '.js': 'text/javascript',
20
+ '.css': 'text/css',
21
+ };
22
+ /** Hosts considered loopback for binding and Host-header checks. */
23
+ export const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
24
+ // -----------------------------------------------------------------------------
25
+ // Responses
26
+ // -----------------------------------------------------------------------------
27
+ export function sendJson(res, status, payload) {
28
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
29
+ res.end(JSON.stringify(payload));
30
+ }
31
+ /**
32
+ * Absolute path to a shipped asset tree (`assets/<name>`), resolved relative to
33
+ * this module's own URL.
34
+ *
35
+ * The depth is load-bearing: compiled to `dist/core/`, two levels up is the
36
+ * package root, which is also what `dist/commands/` yields. Assets are shipped
37
+ * as package `files` and never compiled, so moving this module to a different
38
+ * depth would silently resolve outside the package. Every caller goes through
39
+ * here so that constraint has exactly one home.
40
+ */
41
+ export function packageAssetsDir(name) {
42
+ const here = path.dirname(fileURLToPath(import.meta.url));
43
+ return path.join(here, '..', '..', 'assets', name);
44
+ }
45
+ /**
46
+ * Serves one file from `dir`. The requested name is confined to that directory:
47
+ * URL parsing already normalises `..`, but a resolved path outside `dir` is
48
+ * refused regardless, since this is the only thing standing between a crafted
49
+ * request and the rest of the filesystem.
50
+ */
51
+ export async function serveAsset(res, dir, rawName) {
52
+ const name = decodeURIComponent(rawName);
53
+ const resolved = path.resolve(dir, name);
54
+ if (resolved !== dir && !resolved.startsWith(dir + path.sep)) {
55
+ return sendJson(res, 404, { error: 'not found' });
56
+ }
57
+ const type = ASSET_CONTENT_TYPES[path.extname(resolved)];
58
+ if (!type) {
59
+ return sendJson(res, 404, { error: 'not found' });
60
+ }
61
+ let content;
62
+ try {
63
+ content = await fs.readFile(resolved);
64
+ }
65
+ catch {
66
+ return sendJson(res, 404, { error: 'not found' });
67
+ }
68
+ res.writeHead(200, { 'content-type': `${type}; charset=utf-8` });
69
+ res.end(content);
70
+ }
71
+ // -----------------------------------------------------------------------------
72
+ // Trust boundary
73
+ // -----------------------------------------------------------------------------
74
+ /**
75
+ * DNS-rebinding guard: only requests whose Host header names loopback (or the
76
+ * explicitly bound non-loopback address) are served. A malicious page can make
77
+ * a victim's browser resolve an attacker domain to 127.0.0.1 and read the
78
+ * server's endpoints; the attacker domain in the Host header is what gives that
79
+ * away.
80
+ */
81
+ export function isAllowedHostHeader(hostHeader, boundHost) {
82
+ if (!hostHeader)
83
+ return false;
84
+ // Strip the port; IPv6 hosts arrive bracketed ([::1]:5620).
85
+ const match = hostHeader.match(/^(\[[^\]]+\]|[^:]+)(:\d+)?$/);
86
+ if (!match)
87
+ return false;
88
+ const name = match[1].toLowerCase().replace(/^\[|\]$/g, '');
89
+ if (LOOPBACK_HOSTS.has(name))
90
+ return true;
91
+ return name === boundHost.toLowerCase();
92
+ }
93
+ // -----------------------------------------------------------------------------
94
+ // Lifecycle
95
+ // -----------------------------------------------------------------------------
96
+ /** Resolves once the server is listening, or rejects with the listen error. */
97
+ export function listenOnce(server, port, host) {
98
+ return new Promise((resolve, reject) => {
99
+ const onError = (err) => {
100
+ server.removeListener('listening', onListening);
101
+ reject(err);
102
+ };
103
+ const onListening = () => {
104
+ server.removeListener('error', onError);
105
+ resolve();
106
+ };
107
+ server.once('error', onError);
108
+ server.once('listening', onListening);
109
+ server.listen(port, host);
110
+ });
111
+ }
112
+ /**
113
+ * Binds `basePort`, advancing to the next port on `EADDRINUSE` until one is
114
+ * free, and returns the port actually bound. Every local command wants the same
115
+ * rule — a predictable port you can keep in your head, that still yields rather
116
+ * than failing when something already holds it.
117
+ *
118
+ * `port: 0` is passed straight through: the OS picks a free port itself, so
119
+ * there is nothing to retry and incrementing from whatever it chose would be
120
+ * meaningless.
121
+ *
122
+ * The attempt bound is a backstop, not a policy: without it a machine that
123
+ * refuses every bind for some other reason would spin through 65k ports.
124
+ */
125
+ export async function listenFrom(server, basePort, host, maxAttempts = 200) {
126
+ let port = basePort;
127
+ for (let attempt = 0;; attempt++) {
128
+ try {
129
+ await listenOnce(server, port, host);
130
+ break;
131
+ }
132
+ catch (err) {
133
+ if (err.code === 'EADDRINUSE' && port !== 0 && attempt < maxAttempts) {
134
+ port++;
135
+ continue;
136
+ }
137
+ throw err;
138
+ }
139
+ }
140
+ // With `port: 0` the bound port is only knowable from the socket.
141
+ const address = server.address();
142
+ return typeof address === 'object' && address ? address.port : port;
143
+ }
144
+ // -----------------------------------------------------------------------------
145
+ // Browser
146
+ // -----------------------------------------------------------------------------
147
+ /**
148
+ * Builds the platform command that opens `url` in the default browser.
149
+ * Pure (no side effects) so the platform branching is testable without
150
+ * launching anything.
151
+ */
152
+ export function browserOpenCommand(url, platform = process.platform) {
153
+ if (platform === 'win32')
154
+ return `start "" "${url}"`;
155
+ if (platform === 'darwin')
156
+ return `open "${url}"`;
157
+ return `xdg-open "${url}"`;
158
+ }
159
+ /** Opens `url` in a browser; a failure only warns (the URL is already printed). */
160
+ export function openBrowser(url) {
161
+ // `start` is a cmd builtin, so this must run through the shell (exec does).
162
+ // windowsHide keeps the transient cmd.exe from flashing a console window.
163
+ exec(browserOpenCommand(url), { windowsHide: true }, (err) => {
164
+ if (err) {
165
+ console.warn(`Could not open a browser automatically: ${err.message}`);
166
+ }
167
+ });
168
+ }
169
+ //# sourceMappingURL=local-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-server.js","sourceRoot":"","sources":["../../src/core/local-server.ts"],"names":[],"mappings":"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"}