@jobshimo/browser-link 0.39.0 → 0.41.0

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.
Files changed (76) hide show
  1. package/README.md +23 -8
  2. package/dist/bridge/dispatch.js +17 -7
  3. package/dist/bridge/dispatch.js.map +1 -1
  4. package/dist/bridge/ws-bridge.d.ts +28 -3
  5. package/dist/bridge/ws-bridge.js +51 -7
  6. package/dist/bridge/ws-bridge.js.map +1 -1
  7. package/dist/cli.js +6 -2
  8. package/dist/cli.js.map +1 -1
  9. package/dist/commands/about.js +10 -0
  10. package/dist/commands/about.js.map +1 -1
  11. package/dist/commands/doctor.d.ts +24 -0
  12. package/dist/commands/doctor.js +62 -1
  13. package/dist/commands/doctor.js.map +1 -1
  14. package/dist/commands/free-port.d.ts +40 -1
  15. package/dist/commands/free-port.js +72 -7
  16. package/dist/commands/free-port.js.map +1 -1
  17. package/dist/config.d.ts +7 -0
  18. package/dist/config.js +9 -0
  19. package/dist/config.js.map +1 -1
  20. package/dist/extension/background.js +100 -8
  21. package/dist/extension/background.js.map +1 -1
  22. package/dist/extension/manifest.json +1 -1
  23. package/dist/extension/notice-policy.d.ts +48 -0
  24. package/dist/extension/notice-policy.js +83 -0
  25. package/dist/extension/notice-policy.js.map +1 -0
  26. package/dist/extension/popup.html +99 -0
  27. package/dist/extension/popup.js +139 -1
  28. package/dist/extension/popup.js.map +1 -1
  29. package/dist/extension/repair-policy.d.ts +60 -0
  30. package/dist/extension/repair-policy.js +79 -0
  31. package/dist/extension/repair-policy.js.map +1 -0
  32. package/dist/extension/version-status.d.ts +44 -0
  33. package/dist/extension/version-status.js +60 -0
  34. package/dist/extension/version-status.js.map +1 -0
  35. package/dist/lifecycle/election.d.ts +98 -0
  36. package/dist/lifecycle/election.js +187 -0
  37. package/dist/lifecycle/election.js.map +1 -0
  38. package/dist/lifecycle/processes.d.ts +67 -0
  39. package/dist/lifecycle/processes.js +239 -0
  40. package/dist/lifecycle/processes.js.map +1 -0
  41. package/dist/lifecycle/repair.d.ts +48 -0
  42. package/dist/lifecycle/repair.js +67 -0
  43. package/dist/lifecycle/repair.js.map +1 -0
  44. package/dist/lifecycle/version-endpoint.d.ts +16 -0
  45. package/dist/lifecycle/version-endpoint.js +50 -0
  46. package/dist/lifecycle/version-endpoint.js.map +1 -0
  47. package/dist/manual/topics.js +1 -1
  48. package/dist/manual/topics.js.map +1 -1
  49. package/dist/messages.d.ts +31 -2
  50. package/dist/notice.d.ts +14 -0
  51. package/dist/notice.js +25 -0
  52. package/dist/notice.js.map +1 -0
  53. package/dist/server.js +90 -1
  54. package/dist/server.js.map +1 -1
  55. package/dist/tools/browser-dispatch.d.ts +8 -0
  56. package/dist/tools/browser-dispatch.js +15 -1
  57. package/dist/tools/browser-dispatch.js.map +1 -1
  58. package/dist/ui/app.d.ts +4 -1
  59. package/dist/ui/app.js +12 -3
  60. package/dist/ui/app.js.map +1 -1
  61. package/dist/ui/screens/about.js +3 -1
  62. package/dist/ui/screens/about.js.map +1 -1
  63. package/dist/ui/screens/free-port.js +4 -2
  64. package/dist/ui/screens/free-port.js.map +1 -1
  65. package/dist/ui/screens/index.d.ts +1 -0
  66. package/dist/ui/screens/index.js +1 -0
  67. package/dist/ui/screens/index.js.map +1 -1
  68. package/dist/ui/screens/notice.d.ts +6 -0
  69. package/dist/ui/screens/notice.js +17 -0
  70. package/dist/ui/screens/notice.js.map +1 -0
  71. package/dist/ui/start.js +2 -1
  72. package/dist/ui/start.js.map +1 -1
  73. package/dist/version.d.ts +8 -0
  74. package/dist/version.js +16 -0
  75. package/dist/version.js.map +1 -1
  76. package/package.json +1 -1
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Version facts the popup shows, computed from what the server reported in
3
+ * `tab.registered`. Pure on purpose: background.ts gathers the inputs, this
4
+ * module decides, and popup.ts only renders.
5
+ *
6
+ * Three versions are in play:
7
+ * - `extension`: this extension's manifest version.
8
+ * - `server`: the version the running server booted with (`serverVersion`).
9
+ * - `installed`: the package version on disk right now (`installedVersion`,
10
+ * servers from 0.41.0). When it is newer than `server`, the package was
11
+ * updated while an old process kept serving: the server is outdated.
12
+ */
13
+ /**
14
+ * Compare two `major.minor.patch` versions numerically. A leading `v` and any
15
+ * prerelease or build suffix are ignored; missing or non-numeric parts count
16
+ * as 0. Returns a negative number, 0 or a positive number like a sort
17
+ * comparator.
18
+ */
19
+ export declare function compareSemver(a: string, b: string): number;
20
+ export interface VersionCheckInput {
21
+ extension: string;
22
+ server: string | null;
23
+ installed: string | null;
24
+ }
25
+ /** What background.ts answers to the popup's `versionCheck`. `extension`,
26
+ * `server` and `aligned` keep their pre-0.41.0 meaning; `installed` and
27
+ * `serverOutdated` are additive. */
28
+ export interface VersionCheck {
29
+ extension: string;
30
+ server: string | null;
31
+ installed: string | null;
32
+ /** Extension and server run the same version; null while no server has
33
+ * reported one. */
34
+ aligned: boolean | null;
35
+ /** The installed package is newer than the code the server is running. */
36
+ serverOutdated: boolean;
37
+ }
38
+ export declare function computeVersionCheck(input: VersionCheckInput): VersionCheck;
39
+ /** The existing "Version mismatch" banner: shown only when a server version
40
+ * is known and differs from the extension's. Unchanged by 0.41.0. */
41
+ export declare function shouldShowVersionBanner(check: VersionCheck): boolean;
42
+ /** "Server running X · installed Y" when the server runs a different version
43
+ * than the one installed on disk; null when they match or either is unknown. */
44
+ export declare function runningInstalledLine(check: VersionCheck): string | null;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Version facts the popup shows, computed from what the server reported in
3
+ * `tab.registered`. Pure on purpose: background.ts gathers the inputs, this
4
+ * module decides, and popup.ts only renders.
5
+ *
6
+ * Three versions are in play:
7
+ * - `extension`: this extension's manifest version.
8
+ * - `server`: the version the running server booted with (`serverVersion`).
9
+ * - `installed`: the package version on disk right now (`installedVersion`,
10
+ * servers from 0.41.0). When it is newer than `server`, the package was
11
+ * updated while an old process kept serving: the server is outdated.
12
+ */
13
+ /**
14
+ * Compare two `major.minor.patch` versions numerically. A leading `v` and any
15
+ * prerelease or build suffix are ignored; missing or non-numeric parts count
16
+ * as 0. Returns a negative number, 0 or a positive number like a sort
17
+ * comparator.
18
+ */
19
+ export function compareSemver(a, b) {
20
+ const pa = semverParts(a);
21
+ const pb = semverParts(b);
22
+ for (let i = 0; i < 3; i += 1) {
23
+ const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
24
+ if (diff !== 0)
25
+ return diff;
26
+ }
27
+ return 0;
28
+ }
29
+ function semverParts(version) {
30
+ const core = version.trim().replace(/^v/i, '').split(/[-+]/, 1)[0] ?? '';
31
+ return core.split('.').map((part) => {
32
+ const n = Number.parseInt(part, 10);
33
+ return Number.isFinite(n) && n >= 0 ? n : 0;
34
+ });
35
+ }
36
+ export function computeVersionCheck(input) {
37
+ const { extension, server, installed } = input;
38
+ return {
39
+ extension,
40
+ server,
41
+ installed,
42
+ aligned: server === null ? null : server === extension,
43
+ serverOutdated: server !== null && installed !== null && compareSemver(installed, server) > 0,
44
+ };
45
+ }
46
+ /** The existing "Version mismatch" banner: shown only when a server version
47
+ * is known and differs from the extension's. Unchanged by 0.41.0. */
48
+ export function shouldShowVersionBanner(check) {
49
+ return check.server !== null && check.aligned === false;
50
+ }
51
+ /** "Server running X · installed Y" when the server runs a different version
52
+ * than the one installed on disk; null when they match or either is unknown. */
53
+ export function runningInstalledLine(check) {
54
+ if (check.server === null || check.installed === null)
55
+ return null;
56
+ if (check.installed === check.server)
57
+ return null;
58
+ return `Server running ${check.server} · installed ${check.installed}`;
59
+ }
60
+ //# sourceMappingURL=version-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-status.js","sourceRoot":"","sources":["../src/version-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,CAAS,EAAE,CAAS;IAChD,MAAM,EAAE,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;IAC1B,MAAM,EAAE,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;IAC1B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACzC,IAAI,IAAI,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;IAC9B,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAS,WAAW,CAAC,OAAe;IAClC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACzE,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QAClC,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACpC,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;AACL,CAAC;AAsBD,MAAM,UAAU,mBAAmB,CAAC,KAAwB;IAC1D,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IAC/C,OAAO;QACL,SAAS;QACT,MAAM;QACN,SAAS;QACT,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS;QACtD,cAAc,EAAE,MAAM,KAAK,IAAI,IAAI,SAAS,KAAK,IAAI,IAAI,aAAa,CAAC,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC;KAC9F,CAAC;AACJ,CAAC;AAED;qEACqE;AACrE,MAAM,UAAU,uBAAuB,CAAC,KAAmB;IACzD,OAAO,KAAK,CAAC,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,KAAK,KAAK,CAAC;AAC1D,CAAC;AAED;gFACgF;AAChF,MAAM,UAAU,oBAAoB,CAAC,KAAmB;IACtD,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,SAAS,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACnE,IAAI,KAAK,CAAC,SAAS,KAAK,KAAK,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAClD,OAAO,kBAAkB,KAAK,CAAC,MAAM,gBAAgB,KAAK,CAAC,SAAS,EAAE,CAAC;AACzE,CAAC"}
@@ -0,0 +1,98 @@
1
+ import { type ExecFileLike } from './processes.js';
2
+ /**
3
+ * Version-aware primary election.
4
+ *
5
+ * Without this, the primary is simply whoever bound 127.0.0.1:17529 first,
6
+ * so an old server kept serving after an update until somebody killed it.
7
+ * When a starting process finds the port taken it probes the running
8
+ * primary's `GET /version` (see `version-endpoint.ts`) and decides:
9
+ *
10
+ * (a) remote version known and older than ours → take over;
11
+ * (b) remote answers HTTP but has no `/version` (a primary older than
12
+ * 0.41.0) and its process started before the installed code was last
13
+ * written: it is necessarily running stale code → take over;
14
+ * (c) anything else (same or newer version, unknown start
15
+ * time, nothing answering HTTP) → defer.
16
+ *
17
+ * Deferring is the existing behaviour: proxy mode with multi-agent on, the
18
+ * existing error otherwise. Taking over = step the owner down (a kill: old
19
+ * primaries understand no step-down frame), then bind with retries. The hard
20
+ * kill closes the extension sockets uncleanly, which is exactly what the
21
+ * extension's auto-reconnect waits for.
22
+ *
23
+ * Every effect is injected so the decision is testable without killing or
24
+ * binding anything real; `server.ts` only wires the real implementations.
25
+ */
26
+ export interface VersionInfo {
27
+ name: string;
28
+ version: string;
29
+ pid: number;
30
+ startedAt: string;
31
+ installedVersion: string | null;
32
+ }
33
+ export type VersionProbe =
34
+ /** The primary answered `/version`. */
35
+ {
36
+ kind: 'version';
37
+ info: VersionInfo;
38
+ }
39
+ /** Something answered HTTP on the port, but not a valid `/version`. */
40
+ | {
41
+ kind: 'no-endpoint';
42
+ }
43
+ /** Nothing answered HTTP (refused, timeout, not an HTTP server). */
44
+ | {
45
+ kind: 'unreachable';
46
+ };
47
+ export type ElectionDecision = 'take-over' | 'defer';
48
+ export interface ElectionOutcome {
49
+ decision: ElectionDecision;
50
+ /** The process stepped down (take-over) or that would have been. */
51
+ pid: number | null;
52
+ reason: string;
53
+ }
54
+ export interface DecideInput {
55
+ local: string;
56
+ probe: VersionProbe;
57
+ ownerPid: number | null;
58
+ ownerStartedAt: number | null;
59
+ installedCodeMtime: number | null;
60
+ }
61
+ /** The pure decision table (a)/(b)/(c) above. */
62
+ export declare function decideElection(input: DecideInput): ElectionOutcome;
63
+ export interface ElectionDeps {
64
+ localVersion: string;
65
+ probe: () => Promise<VersionProbe>;
66
+ /** Pid listening on the bridge port; used only when `/version` is absent. */
67
+ findOwnerPid: () => number | null;
68
+ processStartTime: (pid: number) => number | null;
69
+ installedCodeMtime: () => number | null;
70
+ stepDown: (pid: number) => Promise<void>;
71
+ }
72
+ /** Probe, decide and, on take-over, step the owner down. The caller binds
73
+ * afterwards (see `retryBind`). A step-down that fails turns into `defer`. */
74
+ export declare function runElection(deps: ElectionDeps): Promise<ElectionOutcome>;
75
+ /** `GET http://<host>:<port>/version`. Never rejects. */
76
+ export declare function probeVersion(port: number, timeoutMs?: number, host?: string): Promise<VersionProbe>;
77
+ /** Run `bind` until it succeeds, retrying only errors `isRetryable` accepts
78
+ * (a busy port), at most `attempts` times with `delayMs` between tries. */
79
+ export declare function retryBind<T>(bind: () => Promise<T>, opts: {
80
+ attempts: number;
81
+ delayMs: number;
82
+ isRetryable: (err: unknown) => boolean;
83
+ sleep?: (ms: number) => Promise<void>;
84
+ }): Promise<T>;
85
+ /** Unix grace period between SIGTERM and SIGKILL. */
86
+ export declare const STEP_DOWN_GRACE_MS = 3000;
87
+ /**
88
+ * Stop a process: Windows `taskkill /PID n /F`; Unix `SIGTERM`, wait up to
89
+ * 3 s for it to exit, then `SIGKILL`. Throws when the kill itself fails.
90
+ */
91
+ export declare function stepDownProcess(pid: number, deps?: {
92
+ os?: NodeJS.Platform;
93
+ execFile?: ExecFileLike;
94
+ kill?: (pid: number, signal: NodeJS.Signals) => void;
95
+ isAlive?: (pid: number) => boolean;
96
+ sleep?: (ms: number) => Promise<void>;
97
+ now?: () => number;
98
+ }): Promise<void>;
@@ -0,0 +1,187 @@
1
+ import { request } from 'node:http';
2
+ import { platform } from 'node:os';
3
+ import { compareSemver } from '../utils/semver.js';
4
+ import { defaultExecFile } from './processes.js';
5
+ /** The pure decision table (a)/(b)/(c) above. */
6
+ export function decideElection(input) {
7
+ const { probe } = input;
8
+ if (probe.kind === 'version') {
9
+ const remote = probe.info.version;
10
+ if (compareSemver(remote, input.local) < 0) {
11
+ return {
12
+ decision: 'take-over',
13
+ pid: probe.info.pid,
14
+ reason: `running primary ${remote} is older than ${input.local}`,
15
+ };
16
+ }
17
+ return {
18
+ decision: 'defer',
19
+ pid: probe.info.pid,
20
+ reason: `running primary ${remote} is not older than ${input.local}`,
21
+ };
22
+ }
23
+ if (probe.kind === 'unreachable') {
24
+ return { decision: 'defer', pid: input.ownerPid, reason: 'port holder does not answer HTTP' };
25
+ }
26
+ const { ownerPid, ownerStartedAt, installedCodeMtime } = input;
27
+ if (ownerPid === null || ownerStartedAt === null || installedCodeMtime === null) {
28
+ return {
29
+ decision: 'defer',
30
+ pid: ownerPid,
31
+ reason: 'primary has no /version and its start time cannot be compared',
32
+ };
33
+ }
34
+ if (ownerStartedAt < installedCodeMtime) {
35
+ return {
36
+ decision: 'take-over',
37
+ pid: ownerPid,
38
+ reason: 'primary has no /version and started before the installed code was written',
39
+ };
40
+ }
41
+ return {
42
+ decision: 'defer',
43
+ pid: ownerPid,
44
+ reason: 'primary has no /version but started after the installed code was written',
45
+ };
46
+ }
47
+ /** Probe, decide and, on take-over, step the owner down. The caller binds
48
+ * afterwards (see `retryBind`). A step-down that fails turns into `defer`. */
49
+ export async function runElection(deps) {
50
+ const probe = await deps.probe();
51
+ let ownerPid = null;
52
+ let ownerStartedAt = null;
53
+ let installedCodeMtime = null;
54
+ if (probe.kind === 'no-endpoint') {
55
+ ownerPid = deps.findOwnerPid();
56
+ ownerStartedAt = ownerPid === null ? null : deps.processStartTime(ownerPid);
57
+ installedCodeMtime = deps.installedCodeMtime();
58
+ }
59
+ const outcome = decideElection({
60
+ local: deps.localVersion,
61
+ probe,
62
+ ownerPid,
63
+ ownerStartedAt,
64
+ installedCodeMtime,
65
+ });
66
+ if (outcome.decision !== 'take-over' || outcome.pid === null)
67
+ return outcome;
68
+ try {
69
+ await deps.stepDown(outcome.pid);
70
+ return outcome;
71
+ }
72
+ catch (err) {
73
+ return {
74
+ decision: 'defer',
75
+ pid: outcome.pid,
76
+ reason: `step-down of PID ${outcome.pid} failed: ${err instanceof Error ? err.message : String(err)}`,
77
+ };
78
+ }
79
+ }
80
+ function isVersionInfo(v) {
81
+ if (typeof v !== 'object' || v === null)
82
+ return false;
83
+ const o = v;
84
+ return (typeof o.name === 'string' &&
85
+ typeof o.version === 'string' &&
86
+ typeof o.pid === 'number' &&
87
+ Number.isInteger(o.pid) &&
88
+ o.pid > 0 &&
89
+ typeof o.startedAt === 'string' &&
90
+ (o.installedVersion === null || typeof o.installedVersion === 'string'));
91
+ }
92
+ /** `GET http://<host>:<port>/version`. Never rejects. */
93
+ export function probeVersion(port, timeoutMs = 1000, host = '127.0.0.1') {
94
+ return new Promise((resolve) => {
95
+ let settled = false;
96
+ const done = (probe) => {
97
+ if (settled)
98
+ return;
99
+ settled = true;
100
+ resolve(probe);
101
+ };
102
+ const req = request({ host, port, path: '/version', method: 'GET', timeout: timeoutMs }, (res) => {
103
+ const chunks = [];
104
+ res.on('data', (c) => chunks.push(c));
105
+ res.on('end', () => {
106
+ if (res.statusCode !== 200) {
107
+ done({ kind: 'no-endpoint' });
108
+ return;
109
+ }
110
+ try {
111
+ const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
112
+ done(isVersionInfo(body) ? { kind: 'version', info: body } : { kind: 'no-endpoint' });
113
+ }
114
+ catch {
115
+ done({ kind: 'no-endpoint' });
116
+ }
117
+ });
118
+ res.on('error', () => {
119
+ done({ kind: 'no-endpoint' });
120
+ });
121
+ });
122
+ req.on('timeout', () => {
123
+ req.destroy();
124
+ done({ kind: 'unreachable' });
125
+ });
126
+ req.on('error', () => {
127
+ done({ kind: 'unreachable' });
128
+ });
129
+ req.end();
130
+ });
131
+ }
132
+ const sleepReal = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
133
+ /** Run `bind` until it succeeds, retrying only errors `isRetryable` accepts
134
+ * (a busy port), at most `attempts` times with `delayMs` between tries. */
135
+ export async function retryBind(bind, opts) {
136
+ const sleep = opts.sleep ?? sleepReal;
137
+ for (let attempt = 1;; attempt++) {
138
+ try {
139
+ return await bind();
140
+ }
141
+ catch (err) {
142
+ if (!opts.isRetryable(err) || attempt >= opts.attempts)
143
+ throw err;
144
+ await sleep(opts.delayMs);
145
+ }
146
+ }
147
+ }
148
+ /** Unix grace period between SIGTERM and SIGKILL. */
149
+ export const STEP_DOWN_GRACE_MS = 3000;
150
+ const STEP_DOWN_POLL_MS = 100;
151
+ function isAliveReal(pid) {
152
+ try {
153
+ process.kill(pid, 0);
154
+ return true;
155
+ }
156
+ catch (err) {
157
+ // EPERM: it exists but belongs to someone else.
158
+ return err.code === 'EPERM';
159
+ }
160
+ }
161
+ /**
162
+ * Stop a process: Windows `taskkill /PID n /F`; Unix `SIGTERM`, wait up to
163
+ * 3 s for it to exit, then `SIGKILL`. Throws when the kill itself fails.
164
+ */
165
+ export async function stepDownProcess(pid, deps = {}) {
166
+ const os = deps.os ?? platform();
167
+ if (os === 'win32') {
168
+ (deps.execFile ?? defaultExecFile)('taskkill', ['/PID', String(pid), '/F'], {
169
+ timeout: 5000,
170
+ });
171
+ return;
172
+ }
173
+ const kill = deps.kill ?? ((p, s) => void process.kill(p, s));
174
+ const isAlive = deps.isAlive ?? isAliveReal;
175
+ const sleep = deps.sleep ?? sleepReal;
176
+ const now = deps.now ?? Date.now;
177
+ kill(pid, 'SIGTERM');
178
+ const deadline = now() + STEP_DOWN_GRACE_MS;
179
+ while (isAlive(pid)) {
180
+ if (now() >= deadline) {
181
+ kill(pid, 'SIGKILL');
182
+ return;
183
+ }
184
+ await sleep(STEP_DOWN_POLL_MS);
185
+ }
186
+ }
187
+ //# sourceMappingURL=election.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"election.js","sourceRoot":"","sources":["../../src/lifecycle/election.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Process enumeration for the lifecycle features: `browser-link stop`
3
+ * (kill every live bridge, not only the port owner), `doctor` (list them)
4
+ * and the primary election (when did the port owner start?).
5
+ *
6
+ * Every OS call goes through an injectable `ExecFileLike`, the same
7
+ * dependency-injection pattern `auth/silent-debugger.ts` uses, but
8
+ * synchronous: `runFreePort` is synchronous and consumed as such by the CLI,
9
+ * the TUI and `self-update`. Nothing in this module throws: a failed or
10
+ * unparseable listing degrades to "no processes" / `null`.
11
+ *
12
+ * Windows uses `Get-CimInstance Win32_Process` (never `wmic`: deprecated and
13
+ * absent on newer Windows 11 installs). Unix uses BSD-style `ps -axo`,
14
+ * accepted by both macOS and procps.
15
+ */
16
+ /** `execFileSync`-shaped primitive: argv array, no shell, stdout as text. */
17
+ export type ExecFileLike = (file: string, args: readonly string[], options: {
18
+ timeout: number;
19
+ }) => string;
20
+ /** One row of the OS process table. `startedAt` is epoch milliseconds, or
21
+ * `null` when the OS did not report a usable start time. */
22
+ export interface ProcessEntry {
23
+ pid: number;
24
+ ppid: number;
25
+ startedAt: number | null;
26
+ command: string;
27
+ }
28
+ export declare const defaultExecFile: ExecFileLike;
29
+ /**
30
+ * Parse a process start time in any of the forms the OS tools print:
31
+ * PowerShell 5.1's JSON `/Date(ms)/`, ISO 8601 (PowerShell 7 JSON,
32
+ * `ToString('o')`), a WMI datetime `yyyyMMddHHmmss.ffffff±UUU` (offset in
33
+ * minutes) and `ps`'s `lstart` (`Mon Nov 9 02:17:44 2026`, local time).
34
+ */
35
+ export declare function parseProcessTime(raw: string): number | null;
36
+ /** Parse `Get-CimInstance … | ConvertTo-Json -Compress`. One process comes
37
+ * out as a bare object, several as an array; both are accepted. */
38
+ export declare function parseWindowsProcessList(stdout: string): ProcessEntry[];
39
+ /** Parse `ps -axo pid=,ppid=,lstart=,command=` (C locale). */
40
+ export declare function parseUnixProcessList(stdout: string): ProcessEntry[];
41
+ /** The whole process table, or `[]` when it cannot be read. */
42
+ export declare function listProcesses(os?: NodeJS.Platform, exec?: ExecFileLike): ProcessEntry[];
43
+ /** Start time of one process (epoch ms), or `null` when unknown. */
44
+ export declare function readProcessStartTime(pid: number, opts?: {
45
+ os?: NodeJS.Platform;
46
+ exec?: ExecFileLike;
47
+ }): number | null;
48
+ /**
49
+ * Whether a command line runs this package, and with which subcommand
50
+ * (`null` = none, i.e. the MCP server or the TUI). Only the script argument
51
+ * counts (the first non-flag argument after the runtime, or the package
52
+ * spec after npm's `npx-cli.js`), so a process that merely mentions a
53
+ * browser-link path as an option value is never matched.
54
+ */
55
+ export declare function matchBrowserLinkCommand(command: string): {
56
+ subcommand: string | null;
57
+ } | null;
58
+ /** Live browser-link bridges in `list`, excluding `selfPid`, its ancestor
59
+ * chain (the shell or client that launched it) and non-bridge commands. */
60
+ export declare function findBridgeProcesses(list: readonly ProcessEntry[], selfPid: number): ProcessEntry[];
61
+ export declare function listBridgeProcesses(opts?: {
62
+ os?: NodeJS.Platform;
63
+ exec?: ExecFileLike;
64
+ selfPid?: number;
65
+ }): ProcessEntry[];
66
+ /** Short, display-only form: runtime name, `browser-link`, then arguments. */
67
+ export declare function shortCommand(command: string, max?: number): string;