homebridge-plugin-utils 1.35.0 → 2.1.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 (204) hide show
  1. package/README.md +130 -2
  2. package/build/eslint-plugin/README.md +164 -0
  3. package/build/eslint-plugin/config.mjs +308 -0
  4. package/build/eslint-plugin/index.mjs +7 -0
  5. package/build/eslint-plugin/plugin.mjs +46 -0
  6. package/build/eslint-plugin/rules/blank-line-after-open-brace.mjs +137 -0
  7. package/build/eslint-plugin/rules/blank-line-after-open-brace.test.mjs +112 -0
  8. package/build/eslint-plugin/rules/comment-style.mjs +190 -0
  9. package/build/eslint-plugin/rules/comment-style.test.mjs +190 -0
  10. package/build/eslint-plugin/rules/enforce-node-protocol.mjs +114 -0
  11. package/build/eslint-plugin/rules/enforce-node-protocol.test.mjs +116 -0
  12. package/build/eslint-plugin/rules/paren-comparisons-in-logical.mjs +94 -0
  13. package/build/eslint-plugin/rules/paren-comparisons-in-logical.test.mjs +107 -0
  14. package/build/eslint-plugin/rules/split-type-imports.mjs +354 -0
  15. package/build/eslint-plugin/rules/split-type-imports.test.mjs +268 -0
  16. package/build/eslint-plugin/test-setup.mjs +21 -0
  17. package/build/fs-ops.mjs +184 -0
  18. package/build/tsconfig.json +18 -3
  19. package/dist/backpressure.d.ts +94 -45
  20. package/dist/backpressure.js +229 -93
  21. package/dist/backpressure.js.map +1 -1
  22. package/dist/cli/index.d.ts +166 -0
  23. package/dist/cli/index.js +551 -0
  24. package/dist/cli/index.js.map +1 -0
  25. package/dist/clock-double.d.ts +93 -0
  26. package/dist/clock-double.js +141 -0
  27. package/dist/clock-double.js.map +1 -0
  28. package/dist/clock.d.ts +39 -0
  29. package/dist/clock.js +34 -0
  30. package/dist/clock.js.map +1 -0
  31. package/dist/disposable-stack.d.ts +59 -0
  32. package/dist/disposable-stack.js +155 -0
  33. package/dist/disposable-stack.js.map +1 -0
  34. package/dist/docChrome.d.ts +260 -0
  35. package/dist/docChrome.js +361 -0
  36. package/dist/docChrome.js.map +1 -0
  37. package/dist/eslint-plugin/config.d.mts +193 -0
  38. package/dist/eslint-plugin/index.d.mts +2 -0
  39. package/dist/eslint-plugin/plugin.d.mts +87 -0
  40. package/dist/eslint-plugin/rules/blank-line-after-open-brace.d.mts +19 -0
  41. package/dist/eslint-plugin/rules/comment-style.d.mts +15 -0
  42. package/dist/eslint-plugin/rules/enforce-node-protocol.d.mts +18 -0
  43. package/dist/eslint-plugin/rules/paren-comparisons-in-logical.d.mts +15 -0
  44. package/dist/eslint-plugin/rules/split-type-imports.d.mts +16 -0
  45. package/dist/featureOptions-docs.d.ts +96 -0
  46. package/dist/featureOptions-docs.js +193 -0
  47. package/dist/featureOptions-docs.js.map +1 -0
  48. package/dist/featureOptions.d.ts +674 -0
  49. package/dist/featureOptions.js +870 -0
  50. package/dist/featureOptions.js.map +1 -0
  51. package/dist/ffmpeg/codecs.d.ts +256 -72
  52. package/dist/ffmpeg/codecs.js +477 -262
  53. package/dist/ffmpeg/codecs.js.map +1 -1
  54. package/dist/ffmpeg/dgram-util.d.ts +46 -0
  55. package/dist/ffmpeg/dgram-util.js +38 -0
  56. package/dist/ffmpeg/dgram-util.js.map +1 -0
  57. package/dist/ffmpeg/exec.d.ts +83 -64
  58. package/dist/ffmpeg/exec.js +77 -86
  59. package/dist/ffmpeg/exec.js.map +1 -1
  60. package/dist/ffmpeg/fmp4.d.ts +49 -2
  61. package/dist/ffmpeg/fmp4.js +47 -11
  62. package/dist/ffmpeg/fmp4.js.map +1 -1
  63. package/dist/ffmpeg/hap-enums.d.ts +214 -0
  64. package/dist/ffmpeg/hap-enums.js +92 -0
  65. package/dist/ffmpeg/hap-enums.js.map +1 -0
  66. package/dist/ffmpeg/index.d.ts +16 -9
  67. package/dist/ffmpeg/index.js +6 -0
  68. package/dist/ffmpeg/index.js.map +1 -1
  69. package/dist/ffmpeg/mp4-assembler.d.ts +161 -0
  70. package/dist/ffmpeg/mp4-assembler.js +424 -0
  71. package/dist/ffmpeg/mp4-assembler.js.map +1 -0
  72. package/dist/ffmpeg/mp4-parser.d.ts +94 -0
  73. package/dist/ffmpeg/mp4-parser.js +130 -0
  74. package/dist/ffmpeg/mp4-parser.js.map +1 -0
  75. package/dist/ffmpeg/options.d.ts +62 -149
  76. package/dist/ffmpeg/options.js +608 -499
  77. package/dist/ffmpeg/options.js.map +1 -1
  78. package/dist/ffmpeg/process.d.ts +142 -96
  79. package/dist/ffmpeg/process.js +406 -278
  80. package/dist/ffmpeg/process.js.map +1 -1
  81. package/dist/ffmpeg/record.d.ts +325 -186
  82. package/dist/ffmpeg/record.js +418 -565
  83. package/dist/ffmpeg/record.js.map +1 -1
  84. package/dist/ffmpeg/recording-process-double.d.ts +157 -0
  85. package/dist/ffmpeg/recording-process-double.js +190 -0
  86. package/dist/ffmpeg/recording-process-double.js.map +1 -0
  87. package/dist/ffmpeg/rtp-parser.d.ts +70 -0
  88. package/dist/ffmpeg/rtp-parser.js +77 -0
  89. package/dist/ffmpeg/rtp-parser.js.map +1 -0
  90. package/dist/ffmpeg/rtp.d.ts +198 -141
  91. package/dist/ffmpeg/rtp.js +474 -251
  92. package/dist/ffmpeg/rtp.js.map +1 -1
  93. package/dist/ffmpeg/settings.d.ts +5 -2
  94. package/dist/ffmpeg/settings.js +20 -5
  95. package/dist/ffmpeg/settings.js.map +1 -1
  96. package/dist/ffmpeg/stream.d.ts +57 -107
  97. package/dist/ffmpeg/stream.js +121 -150
  98. package/dist/ffmpeg/stream.js.map +1 -1
  99. package/dist/formatters.d.ts +106 -0
  100. package/dist/formatters.js +174 -0
  101. package/dist/formatters.js.map +1 -0
  102. package/dist/homebridge-enums.d.ts +30 -0
  103. package/dist/homebridge-enums.js +17 -0
  104. package/dist/homebridge-enums.js.map +1 -0
  105. package/dist/index.d.ts +13 -6
  106. package/dist/index.js +8 -2
  107. package/dist/index.js.map +1 -1
  108. package/dist/logclient/auth.d.ts +114 -0
  109. package/dist/logclient/auth.js +199 -0
  110. package/dist/logclient/auth.js.map +1 -0
  111. package/dist/logclient/cli-run.d.ts +76 -0
  112. package/dist/logclient/cli-run.js +639 -0
  113. package/dist/logclient/cli-run.js.map +1 -0
  114. package/dist/logclient/cli.d.ts +3 -0
  115. package/dist/logclient/cli.js +97 -0
  116. package/dist/logclient/cli.js.map +1 -0
  117. package/dist/logclient/client.d.ts +145 -0
  118. package/dist/logclient/client.js +600 -0
  119. package/dist/logclient/client.js.map +1 -0
  120. package/dist/logclient/config.d.ts +173 -0
  121. package/dist/logclient/config.js +199 -0
  122. package/dist/logclient/config.js.map +1 -0
  123. package/dist/logclient/endpoints.d.ts +54 -0
  124. package/dist/logclient/endpoints.js +73 -0
  125. package/dist/logclient/endpoints.js.map +1 -0
  126. package/dist/logclient/filter.d.ts +45 -0
  127. package/dist/logclient/filter.js +51 -0
  128. package/dist/logclient/filter.js.map +1 -0
  129. package/dist/logclient/frame.d.ts +93 -0
  130. package/dist/logclient/frame.js +203 -0
  131. package/dist/logclient/frame.js.map +1 -0
  132. package/dist/logclient/index.d.ts +31 -0
  133. package/dist/logclient/index.js +12 -0
  134. package/dist/logclient/index.js.map +1 -0
  135. package/dist/logclient/parser.d.ts +211 -0
  136. package/dist/logclient/parser.js +393 -0
  137. package/dist/logclient/parser.js.map +1 -0
  138. package/dist/logclient/rest.d.ts +41 -0
  139. package/dist/logclient/rest.js +111 -0
  140. package/dist/logclient/rest.js.map +1 -0
  141. package/dist/logclient/settings.d.ts +15 -0
  142. package/dist/logclient/settings.js +64 -0
  143. package/dist/logclient/settings.js.map +1 -0
  144. package/dist/logclient/socket-double.d.ts +201 -0
  145. package/dist/logclient/socket-double.js +384 -0
  146. package/dist/logclient/socket-double.js.map +1 -0
  147. package/dist/logclient/socket.d.ts +257 -0
  148. package/dist/logclient/socket.js +620 -0
  149. package/dist/logclient/socket.js.map +1 -0
  150. package/dist/logclient/stitch.d.ts +83 -0
  151. package/dist/logclient/stitch.js +146 -0
  152. package/dist/logclient/stitch.js.map +1 -0
  153. package/dist/logclient/time-expression.d.ts +42 -0
  154. package/dist/logclient/time-expression.js +181 -0
  155. package/dist/logclient/time-expression.js.map +1 -0
  156. package/dist/logclient/time-window.d.ts +38 -0
  157. package/dist/logclient/time-window.js +53 -0
  158. package/dist/logclient/time-window.js.map +1 -0
  159. package/dist/logclient/types.d.ts +107 -0
  160. package/dist/logclient/types.js +6 -0
  161. package/dist/logclient/types.js.map +1 -0
  162. package/dist/mqttClient.d.ts +287 -0
  163. package/dist/mqttClient.js +433 -0
  164. package/dist/mqttClient.js.map +1 -0
  165. package/dist/service.d.ts +64 -15
  166. package/dist/service.js +93 -66
  167. package/dist/service.js.map +1 -1
  168. package/dist/ui/featureOptions.js +870 -0
  169. package/dist/ui/featureOptions.js.map +1 -0
  170. package/dist/ui/formatters.js +174 -0
  171. package/dist/ui/formatters.js.map +1 -0
  172. package/dist/ui/pluginConfigSession.mjs +141 -0
  173. package/dist/ui/webUi-featureOptions/categoryState.mjs +135 -0
  174. package/dist/ui/webUi-featureOptions/effects/keyboard.mjs +61 -0
  175. package/dist/ui/webUi-featureOptions/effects/persist.mjs +226 -0
  176. package/dist/ui/webUi-featureOptions/effects/theme.mjs +398 -0
  177. package/dist/ui/webUi-featureOptions/effects/tokens.mjs +152 -0
  178. package/dist/ui/webUi-featureOptions/rendering.mjs +431 -0
  179. package/dist/ui/webUi-featureOptions/selectors.mjs +360 -0
  180. package/dist/ui/webUi-featureOptions/state.mjs +319 -0
  181. package/dist/ui/webUi-featureOptions/store.mjs +181 -0
  182. package/dist/ui/webUi-featureOptions/utils.mjs +213 -0
  183. package/dist/ui/webUi-featureOptions/views/connectionError.mjs +152 -0
  184. package/dist/ui/webUi-featureOptions/views/deviceInfo.mjs +80 -0
  185. package/dist/ui/webUi-featureOptions/views/header.mjs +77 -0
  186. package/dist/ui/webUi-featureOptions/views/nav.mjs +341 -0
  187. package/dist/ui/webUi-featureOptions/views/options.mjs +521 -0
  188. package/dist/ui/webUi-featureOptions/views/search.mjs +395 -0
  189. package/dist/ui/webUi-featureOptions.mjs +702 -0
  190. package/dist/ui/webUi.mjs +225 -88
  191. package/dist/util.d.ts +679 -45
  192. package/dist/util.js +830 -77
  193. package/dist/util.js.map +1 -1
  194. package/package.json +33 -15
  195. package/build/eslint-rules.mjs +0 -511
  196. package/dist/featureoptions.d.ts +0 -264
  197. package/dist/featureoptions.js +0 -480
  198. package/dist/featureoptions.js.map +0 -1
  199. package/dist/mqttclient.d.ts +0 -178
  200. package/dist/mqttclient.js +0 -310
  201. package/dist/mqttclient.js.map +0 -1
  202. package/dist/ui/featureoptions.js +0 -480
  203. package/dist/ui/featureoptions.js.map +0 -1
  204. package/dist/ui/webUi-featureoptions.mjs +0 -3765
@@ -0,0 +1,393 @@
1
+ /* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * logclient/parser.ts: Pure incremental line splitter and per-line parser for Homebridge UI log text.
4
+ */
5
+ // The ANSI escape-sequence regex. Matches CSI sequences (ESC `[` ... final byte) - which covers the SGR color sequences the log stream uses - so they can be stripped
6
+ // when building the human-readable `message`. Compiled once at module scope since it runs on every parsed line on the parse hot path. Two character classes cover the
7
+ // parameter bytes and then the intermediate bytes; the trailing class is the final byte that terminates the sequence.
8
+ // eslint-disable-next-line no-control-regex
9
+ const ANSI_PATTERN = /\[[0-9;?]*[ -/]*[@-~]/g;
10
+ // The SGR (Select Graphic Rendition) foreground color codes homebridge-config-ui-x uses to convey severity, mapped to the corresponding {@link LogLevel}. The server
11
+ // colors the MESSAGE portion of a line - not the whole line - by wrapping it in a single SGR sequence: 31 (red) error, 33 (yellow) warn, 90 (bright black) debug, 32
12
+ // (green) success. An info line carries no message color, so the SGR code 39 is intentionally absent from this map (`readLevel` yields null for it); `parseLogLine` then
13
+ // promotes a colored line with no severity color to "info", reserving null for a color-stripped line. The timestamp (37/white) and plugin (36/cyan) colors are
14
+ // deliberately absent too - they prefix the message and must never be mistaken for severity. Frozen so the shared module-scope table cannot be mutated by a consumer.
15
+ const SGR_LEVEL = Object.freeze({
16
+ "31": "error",
17
+ "32": "success",
18
+ "33": "warn",
19
+ "90": "debug"
20
+ });
21
+ // Match every SGR sequence in a string globally, capturing each one's numeric parameter. The severity color is read by scanning these from the point AFTER the
22
+ // timestamp/plugin brackets (see {@link readLevel}), because the server colors the message body, not the line prefix; the timestamp's and plugin's own colors sit before
23
+ // that point and are excluded by where the scan starts. The capture group is the parameter digits between `ESC[` and the terminating `m`.
24
+ // eslint-disable-next-line no-control-regex
25
+ const SGR_PATTERN = /\[([0-9;]*)m/g;
26
+ // Match the conventional `[timestamp] [Plugin Name] ` prefix at the start of an ANSI-stripped line, capturing the timestamp and plugin text. Each bracketed field is
27
+ // guaranteed present when the match succeeds: the timestamp is rendered white and the plugin name is rendered cyan; the remainder after the closing plugin bracket and
28
+ // its trailing space is the message body. Both captures use a negated character class (`[^\]]*`) that cannot cross a `]`, so a message containing brackets is not
29
+ // absorbed into the prefix.
30
+ const BRACKET_PREFIX_PATTERN = /^\[([^\]]*)\] \[([^\]]*)\] /;
31
+ // Recognize the Homebridge en-US default timestamp in both renderings the server emits: the 12-hour `M/D/YYYY, h:mm:ss AM` and the 24-hour `M/D/YYYY, HH:mm:ss` (meridiem
32
+ // absent). One module-scope regex serves both - the meridiem group is optional - mirroring the other compiled-once patterns above. The capture groups are, in order:
33
+ // month, day, four-digit year, hour, minute, second, and the optional AM/PM token.
34
+ const LOG_TIMESTAMP_PATTERN = /^(\d{1,2})\/(\d{1,2})\/(\d{4}),\s+(\d{1,2}):(\d{2}):(\d{2})(?:\s*([AaPp][Mm]))?$/;
35
+ // Capture the contents of the FIRST bracket at the very start of an ANSI-stripped line - used by {@link isLogLineStart} to test whether a line opens with a timestamp.
36
+ // Only the first bracket is captured (the negated class cannot cross a `]`), because a genuine log entry begins with `[timestamp]` whether or not a `[plugin]` bracket
37
+ // follows; the timestamp's parseability, not the presence of a second bracket, is what marks an entry start.
38
+ const LOG_LINE_START_PATTERN = /^\[([^\]]*)\]/;
39
+ // Strip every ANSI escape sequence from a string, leaving only the printable text. Used to derive the human-readable `message` from a raw line; the raw line itself
40
+ // retains the escapes so a `--raw` consumer can reproduce the server's coloring.
41
+ function stripAnsi(text) {
42
+ return text.replace(ANSI_PATTERN, "");
43
+ }
44
+ /**
45
+ * Cursor-based incremental line splitter for the log text stream.
46
+ *
47
+ * Feed each text chunk through {@link LogLineSplitter.consume} and iterate the raw lines it yields. The splitter carries the bytes of an incomplete trailing line across
48
+ * calls, so a line split across two chunks is reassembled transparently, and it recognizes all four newline conventions the PTY-driven stream mixes (`\r\n`, `\n\r`,
49
+ * `\r`, `\n`) as single line breaks. Crucially, a chunk that ends with a lone `\r` or `\n` holds that terminator in the carry rather than yielding immediately: the next
50
+ * chunk may begin with the matching second half of a `\n\r`/`\r\n` pair, and emitting eagerly would split one line break into two and inject a phantom blank line.
51
+ *
52
+ * The scan is a single integer cursor over the carried buffer; the buffer is rebuilt once per `consume` (the residual tail becomes the next carry), not once per line,
53
+ * so per-line work is a single `slice` of the line's content with no quadratic buffer churn. Because the splitter holds at most one partial line plus the current chunk,
54
+ * its memory is bounded by chunk size regardless of total stream length.
55
+ *
56
+ * The class is intentionally signal-free and event-free. Resource lifecycle and async consumption belong to the composing transport, exactly as `Mp4BoxParser` leaves
57
+ * those concerns to `Mp4SegmentAssembler`.
58
+ *
59
+ * @example
60
+ *
61
+ * ```ts
62
+ * const splitter = new LogLineSplitter();
63
+ *
64
+ * for(const line of splitter.consume(chunk)) {
65
+ *
66
+ * handle(parseLogLine(line));
67
+ * }
68
+ * ```
69
+ *
70
+ * @category Log Client
71
+ */
72
+ export class LogLineSplitter {
73
+ // The residual carry: the bytes of an incomplete trailing line (and any held lone terminator) from the previous call, prepended to the next chunk. Empty when the
74
+ // previous chunk ended exactly on a line break.
75
+ #carry = "";
76
+ /**
77
+ * Feed the splitter a text chunk and yield every complete raw line now available.
78
+ *
79
+ * Each yielded line is the content between line breaks with the terminator removed and ANSI escapes preserved. A line that is still incomplete (no terminator yet, or
80
+ * only a held lone terminator that may pair with the next chunk) is carried internally and surfaces on a later call once its break is unambiguous.
81
+ *
82
+ * @param chunk - A contiguous slice of log text from the transport.
83
+ *
84
+ * @returns An iterable of every complete raw line contained in (or completed by) this chunk, in stream order.
85
+ */
86
+ *consume(chunk) {
87
+ // Prepend the carry only when there is one, keeping the common "chunk completes its own lines and nothing is pending" path free of an empty-string concatenation.
88
+ const buffer = this.#carry.length > 0 ? this.#carry + chunk : chunk;
89
+ // `start` marks the beginning of the current line; `cursor` scans forward looking for terminators. We slice each line's content from `[start, breakStart)` and never
90
+ // rebuild `buffer` mid-loop - the residual tail is computed once at the end.
91
+ let start = 0;
92
+ let cursor = 0;
93
+ while (cursor < buffer.length) {
94
+ const character = buffer.charAt(cursor);
95
+ // Only `\r` and `\n` can start a line break; skip everything else with a single cursor advance.
96
+ if ((character !== "\r") && (character !== "\n")) {
97
+ cursor++;
98
+ continue;
99
+ }
100
+ // We found a terminator character. Determine whether it pairs with the following character to form a two-character break (`\r\n` or `\n\r`). The pair forms only
101
+ // when the partner is the OTHER terminator character - `\r\r` and `\n\n` are two separate breaks (two lines), not one.
102
+ const partner = character === "\r" ? "\n" : "\r";
103
+ const hasNext = (cursor + 1) < buffer.length;
104
+ // A lone terminator at the very end of the buffer is ambiguous: the next chunk might supply the partner that completes a two-character break. Hold the line (and
105
+ // this terminator) in the carry rather than yielding, so a `\n\r`/`\r\n` pair split across chunks is never seen as two breaks. We stop scanning here; the residual
106
+ // computed below preserves `[start, end)` including the held terminator.
107
+ if (!hasNext) {
108
+ break;
109
+ }
110
+ // The break is unambiguous now that we can see the next character. Yield the line content up to the terminator, then advance past either one or two characters
111
+ // depending on whether the partner immediately follows.
112
+ yield buffer.slice(start, cursor);
113
+ const isPair = buffer.charAt(cursor + 1) === partner;
114
+ cursor += isPair ? 2 : 1;
115
+ start = cursor;
116
+ }
117
+ // The residual is everything from the last unfinished line's start to the end of the buffer, including any held lone terminator. It becomes the carry for the next
118
+ // call. When `start` reached the buffer end exactly (the chunk ended on a complete break), the carry is empty.
119
+ this.#carry = buffer.slice(start);
120
+ }
121
+ /**
122
+ * Flush any buffered final line that never received a terminator.
123
+ *
124
+ * A log stream may end without a trailing newline, leaving the last line held in the carry. Call this once after the source has ended to surface that final line. It
125
+ * yields at most one line and clears the carry; calling it again yields nothing. A held lone terminator with no content (a trailing bare `\r`/`\n`) flushes as an
126
+ * empty final line, matching the convention that a terminator delimits a line that exists.
127
+ *
128
+ * @returns An iterable yielding the final unterminated line, or nothing if the carry is empty.
129
+ */
130
+ *flush() {
131
+ if (this.#carry.length === 0) {
132
+ return;
133
+ }
134
+ // Strip a single trailing lone terminator that was being held for possible pairing; at flush time no partner will arrive, so it delimits the end of the final line.
135
+ const trailing = this.#carry.replace(/[\r\n]$/, "");
136
+ this.#carry = "";
137
+ yield trailing;
138
+ }
139
+ }
140
+ /**
141
+ * Parse a single raw log line into a {@link LogRecord}.
142
+ *
143
+ * The `[timestamp] [Plugin Name] ` prefix is extracted from the ANSI-stripped text, and the remainder becomes the human-readable `message`. The severity level is read
144
+ * from the SGR color sequence that the server wraps around the MESSAGE - which means it appears AFTER the timestamp/plugin brackets, not at the start of the line: the
145
+ * timestamp (white) and plugin (cyan) carry their own colors, so reading the line's first color would always misclassify them as severity. We therefore scan for the
146
+ * severity color starting just past the plugin bracket. The original line is preserved verbatim in `raw`. A colored line that carries no severity color resolves to
147
+ * `level: "info"` (Homebridge's info convention), while a fully color-stripped line resolves to `level: null` because severity is genuinely unknown; a line with no
148
+ * bracketed prefix resolves to `timestamp: null` / `plugin: null`, a `message` equal to the full stripped line, and a level read from any severity color present
149
+ * anywhere in the (prefix-less) line.
150
+ *
151
+ * @param raw - A single raw log line (escapes intact, terminator already removed by {@link LogLineSplitter}).
152
+ *
153
+ * @returns The parsed {@link LogRecord}.
154
+ *
155
+ * @category Log Client
156
+ */
157
+ export function parseLogLine(raw) {
158
+ // Strip ANSI to get the printable text, then peel the conventional `[timestamp] [Plugin Name] ` prefix. When the prefix is absent the whole stripped line is the
159
+ // message and both bracketed fields are null.
160
+ const stripped = stripAnsi(raw);
161
+ const match = BRACKET_PREFIX_PATTERN.exec(stripped);
162
+ if (match === null) {
163
+ // No bracketed prefix: this is a status or continuation line, not a formatted Homebridge log line, so the "uncolored = info" convention does not apply. The level is
164
+ // whatever severity color the line carries (read from the whole line, since there is no prefix to skip past), or null.
165
+ return { level: readLevel(raw, 0), message: stripped, plugin: null, raw, timestamp: null };
166
+ }
167
+ // A formatted log line. The severity SGR colors the message, so it sits after the timestamp/plugin brackets; we anchor the scan just past the plugin bracket's closing
168
+ // `]` in the raw line so the timestamp's (white) and plugin's (cyan) own colors are not read as severity. When the message has no severity color, the level depends
169
+ // on whether the line is colored at ALL: a colored line with an uncolored message is Homebridge's info convention ("info"), whereas a line with no ANSI whatsoever is
170
+ // color-stripped, leaving severity genuinely unknown (null). We read "is the line colored" for free from the strip already performed - stripping shortened the text if
171
+ // and only if the line carried ANSI - rather than scanning a second time.
172
+ const colored = stripped.length !== raw.length;
173
+ const level = readLevel(raw, messageColorStart(raw)) ?? (colored ? "info" : null);
174
+ // `match[1]` is the timestamp text and `match[2]` is the plugin text; each bracketed field is guaranteed present when the match succeeds. The message is the
175
+ // stripped text after the matched prefix.
176
+ return { level, message: stripped.slice(match[0].length), plugin: match[2] ?? null, raw, timestamp: match[1] ?? null };
177
+ }
178
+ // Locate, in the RAW (still-escaped) line, the index just past the second `]` - the plugin bracket's close - so the severity-color scan starts at the message portion
179
+ // and never sees the timestamp's or plugin's own color. The closing bracket `]` never appears inside the SGR sequences the server emits, so scanning the raw line for it
180
+ // is unambiguous. Returns 0 when fewer than two `]` are present (no recognizable prefix), letting the caller scan from the start.
181
+ function messageColorStart(raw) {
182
+ const firstClose = raw.indexOf("]");
183
+ if (firstClose === -1) {
184
+ return 0;
185
+ }
186
+ const secondClose = raw.indexOf("]", firstClose + 1);
187
+ return secondClose === -1 ? 0 : secondClose + 1;
188
+ }
189
+ // Read the severity level from the first severity-mapping SGR color sequence at or after `from` in the raw line, or null when none is present. Scanning from `from`
190
+ // (past the bracket prefix) excludes the timestamp/plugin colors; the first SGR whose parameter maps to a severity wins. An SGR parameter may be a semicolon-separated
191
+ // list (e.g., `1;31` for bold red), so each component is consulted against the severity map. Resets and default-color codes (0, 39) never map, so they are skipped
192
+ // transparently and the genuine severity color is found.
193
+ function readLevel(raw, from) {
194
+ // Reset the shared global regex's cursor to the scan start. `SGR_PATTERN` carries the `g` flag so successive `.exec` calls walk every SGR sequence in the line; setting
195
+ // `lastIndex` bounds the walk to the message portion and keeps the module-scope regex stateless across `parseLogLine` calls.
196
+ SGR_PATTERN.lastIndex = from;
197
+ for (let match = SGR_PATTERN.exec(raw); match !== null; match = SGR_PATTERN.exec(raw)) {
198
+ // `match[1]` is the captured parameter string; split on semicolons so a compound SGR like `1;31m` is checked component-by-component against the severity map.
199
+ const parameters = (match[1] ?? "").split(";");
200
+ for (const parameter of parameters) {
201
+ const mapped = SGR_LEVEL[parameter];
202
+ if (mapped !== undefined) {
203
+ return mapped;
204
+ }
205
+ }
206
+ }
207
+ return null;
208
+ }
209
+ /**
210
+ * Parse a Homebridge log timestamp into epoch milliseconds, or `null` when the text is not a recognized timestamp.
211
+ *
212
+ * The Homebridge UI renders the first bracketed field as the host's locale/clock string. This recognizes the en-US default in both its renderings - the 12-hour
213
+ * `M/D/YYYY, h:mm:ss AM` and the 24-hour `M/D/YYYY, HH:mm:ss` - via a single regex, constructs the instant in LOCAL time (matching how the server formats it in the
214
+ * host's own timezone), and returns its epoch milliseconds. The shared {@link normalizeClock} helper owns the meridiem-to-24-hour conversion and the clock-component
215
+ * range check, so that rule lives in exactly one place.
216
+ *
217
+ * Deliberate limitations, in the same register as the rest of the client: only the en-US 12h/24h default is recognized, so a server running another locale yields
218
+ * `null` (the time-window stage carries forward the most recent parsed instant, so a null-epoch line is kept iff its parent record is in-window, never dropped merely for
219
+ * lacking a parseable epoch); and the interpretation is LOCAL time, so a client whose timezone differs from the server's skews the absolute values. A well-formed but
220
+ * impossible calendar date (for example `2/30`) returns `null`; a wall-clock time that falls in the client's DST spring-forward gap is best-effort accepted rather than
221
+ * rejected, because only the calendar fields are round-trip-validated.
222
+ *
223
+ * @param text - The raw timestamp text from a {@link LogRecord.timestamp} (the first bracketed field), for example `"6/29/2026, 7:00:00 AM"`.
224
+ *
225
+ * @returns The instant as epoch milliseconds, or `null` when the text is not a recognized en-US timestamp or names an impossible calendar date.
226
+ *
227
+ * @category Log Client
228
+ */
229
+ export function parseLogTimestamp(text) {
230
+ const match = LOG_TIMESTAMP_PATTERN.exec(text);
231
+ if (match === null) {
232
+ return null;
233
+ }
234
+ // The numeric groups are guaranteed present by a successful match; the optional meridiem group is undefined for a 24-hour rendering. We parse the calendar fields
235
+ // here and delegate the clock fields to `normalizeClock`, which owns the range check and the 12-hour-to-24-hour conversion.
236
+ const month = Number.parseInt(match[1] ?? "", 10);
237
+ const day = Number.parseInt(match[2] ?? "", 10);
238
+ const year = Number.parseInt(match[3] ?? "", 10);
239
+ const clock = normalizeClock({ hour: Number.parseInt(match[4] ?? "", 10), meridiem: match[7], minute: Number.parseInt(match[5] ?? "", 10),
240
+ second: Number.parseInt(match[6] ?? "", 10) });
241
+ // An out-of-range clock component (an impossible hour/minute/second) invalidates the whole timestamp.
242
+ if (clock === null) {
243
+ return null;
244
+ }
245
+ // Construct the instant in LOCAL time, then round-trip the CALENDAR fields only. The round-trip rejects a well-formed-but-impossible date (the Date constructor rolls
246
+ // `2/30` forward into March, so the day/month no longer match); we intentionally do NOT round-trip the hour/minute/second, so a wall-clock time in the DST spring-
247
+ // forward gap survives rather than being rejected. Because both the clock and the calendar are validated, the result is total and needs no `NaN` guard.
248
+ const date = new Date(year, month - 1, day, clock.hour, clock.minute, clock.second);
249
+ if ((date.getFullYear() !== year) || (date.getMonth() !== (month - 1)) || (date.getDate() !== day)) {
250
+ return null;
251
+ }
252
+ return date.getTime();
253
+ }
254
+ /**
255
+ * Normalize a raw clock reading - the captured hour/minute/second plus an optional meridiem token - into a 24-hour `{ hour, minute, second }`, or `null` when any
256
+ * component is out of range. This is the SINGLE source of truth for the meridiem-to-24-hour conversion and the clock-component range check, shared by
257
+ * {@link parseLogTimestamp} and the CLI-layer time-expression parser so neither re-implements the rule. It is a named export for those consumers and its own test, but it
258
+ * is intentionally not part of the package barrel.
259
+ *
260
+ * Range rules: minute and second are 0-59 in every rendering; when a meridiem is present the hour is a 12-hour value (1-12), and without one it is a 24-hour value
261
+ * (0-23). An out-of-range component yields `null`. The conversion maps `12am` to 0 and `12pm` to 12, adds 12 to any other PM hour, and leaves any other AM hour and every
262
+ * 24-hour reading unchanged.
263
+ *
264
+ * @param parts - The raw clock components.
265
+ * @param parts.hour - The hour as written (1-12 with a meridiem, 0-23 without).
266
+ * @param parts.meridiem - The matched `AM`/`PM` token (any case) when the source carried one, or `undefined` for a 24-hour rendering.
267
+ * @param parts.minute - The minute (0-59).
268
+ * @param parts.second - The second (0-59).
269
+ *
270
+ * @returns The normalized 24-hour `{ hour, minute, second }`, or `null` when any component is out of range.
271
+ *
272
+ * @category Log Client
273
+ */
274
+ export function normalizeClock(parts) {
275
+ const { hour, meridiem, minute, second } = parts;
276
+ // Minute and second are 0-59 regardless of rendering; an out-of-range component invalidates the reading.
277
+ if ((minute < 0) || (minute > 59) || (second < 0) || (second > 59)) {
278
+ return null;
279
+ }
280
+ // No meridiem means a 24-hour reading (0-23) that is already normalized; validate the range and pass it through verbatim.
281
+ if (meridiem === undefined) {
282
+ if ((hour < 0) || (hour > 23)) {
283
+ return null;
284
+ }
285
+ return { hour, minute, second };
286
+ }
287
+ // A meridiem is present, so the hour is a 12-hour value (1-12).
288
+ if ((hour < 1) || (hour > 12)) {
289
+ return null;
290
+ }
291
+ // Convert to 24-hour: `12am` is midnight (0) and `12pm` is noon (12); any other PM hour is `+12` and any other AM hour is unchanged.
292
+ const isPm = meridiem.charAt(0).toLowerCase() === "p";
293
+ if (hour === 12) {
294
+ return { hour: isPm ? 12 : 0, minute, second };
295
+ }
296
+ return { hour: isPm ? hour + 12 : hour, minute, second };
297
+ }
298
+ /**
299
+ * Determine whether a raw log line begins a new Homebridge log entry - that is, whether it opens with a parseable bracketed timestamp.
300
+ *
301
+ * Every genuine entry the server emits starts with a `[timestamp]` field; a `[plugin]` bracket may or may not follow, because Homebridge's own core lines (for example
302
+ * `[7/3/2026, 4:31:46 PM] Homebridge v1.11.3 ... is running`) carry a timestamp but no plugin prefix. Only the FIRST bracket is inspected: requiring a second `[plugin]`
303
+ * bracket would wrongly reject those core lines. A line that is not an entry start - the server's `Loading logs...` / `File: ...` seed preamble, a byte-truncated seed
304
+ * fragment, or a continuation line such as a stack frame or a wrapped object dump - has no leading parseable timestamp and returns `false`. ANSI is stripped first
305
+ * because the server wraps the timestamp in an SGR color, so the raw line begins with an escape sequence rather than the `[`.
306
+ *
307
+ * @param raw - A single raw log line (escapes intact, terminator already removed by {@link LogLineSplitter}).
308
+ *
309
+ * @returns `true` when the line opens with a parseable en-US timestamp bracket, `false` otherwise.
310
+ *
311
+ * @category Log Client
312
+ */
313
+ export function isLogLineStart(raw) {
314
+ const match = LOG_LINE_START_PATTERN.exec(stripAnsi(raw));
315
+ return (match !== null) && (parseLogTimestamp(match[1] ?? "") !== null);
316
+ }
317
+ /**
318
+ * A one-way admission latch that suppresses the unusable leading lines of a live socket log seed until the first genuine log entry.
319
+ *
320
+ * The homebridge-config-ui-x native/file log method seeds a live tail by streaming the tail of the log file from a BYTE offset rather than a line boundary, so the first
321
+ * line of file content is a fragment - the tail end of whatever line the offset fell inside - preceded by the server's own `Loading logs...` / `File: ...` preamble. None
322
+ * of that is a showable entry. This gate drops every line until the first that begins a real entry - a parseable leading `[timestamp]`, per {@link isLogLineStart} - then
323
+ * latches OPEN and admits everything thereafter, so the continuation lines and plugin-less core lines that legitimately lack a full prefix flow through untouched.
324
+ *
325
+ * It is composed per stream, exactly as {@link LogLineSplitter} is: the consumer owns WHEN to apply it (only on the byte-seeded socket seed - the REST whole-file
326
+ * download starts at byte 0 and needs no gate), while this owns the latch policy. Once open it never closes, so steady-state admission is a single boolean read and the
327
+ * recognition predicate runs only across the short leading prefix, never the live stream.
328
+ *
329
+ * A bounded safety valve guards the pathological case of a stream whose timestamps are not the recognized en-US rendering (a non-default server locale): if no entry
330
+ * start is seen within `maxSkip` lines, the gate concludes the format is unrecognized, latches open, and admits the rest rather than suppressing the stream forever.
331
+ * `maxSkip` is sized above any plausible leading-noise run yet well below the ~500-line seed, so it never fires for a normal log and bounds worst-case loss when it does.
332
+ *
333
+ * @example
334
+ *
335
+ * ```ts
336
+ * const gate = new SeedGate(SEED_GATE_MAX_SKIP);
337
+ *
338
+ * for(const line of splitter.consume(chunk)) {
339
+ *
340
+ * if(gate.admit(line)) {
341
+ *
342
+ * emit(line);
343
+ * }
344
+ * }
345
+ * ```
346
+ *
347
+ * @category Log Client
348
+ */
349
+ export class SeedGate {
350
+ // Whether the latch has opened. Once true, every line is admitted with a single boolean read and the recognition predicate is never consulted again.
351
+ #open = false;
352
+ // The number of leading non-entry lines dropped so far while the latch is still closed. Bounded by `#maxSkip`, at which point the gate gives up and opens.
353
+ #skipped = 0;
354
+ // The safety-valve bound: the most leading non-entry lines to drop before concluding the stream carries no recognizable entry start and opening the latch regardless.
355
+ #maxSkip;
356
+ /**
357
+ * @param maxSkip - The safety-valve bound: the most leading non-entry lines to drop before opening the latch unconditionally. The consumer injects it (production
358
+ * passes `SEED_GATE_MAX_SKIP`) so a test can drive the bounded-open path with a small value.
359
+ */
360
+ constructor(maxSkip) {
361
+ this.#maxSkip = maxSkip;
362
+ }
363
+ /**
364
+ * Decide whether a raw line is admitted, advancing the latch. Once the latch has opened every line is admitted; before then, only the first line that begins a genuine
365
+ * entry (which opens the latch) or the line at which the skip bound is reached (the unrecognized-format safety valve) is admitted, and every leading non-entry line is
366
+ * dropped.
367
+ *
368
+ * @param line - A single raw log line from the seed stream.
369
+ *
370
+ * @returns `true` if the line should be emitted, `false` if it is leading seed noise to drop.
371
+ */
372
+ admit(line) {
373
+ // Once the first real entry has opened the latch, every subsequent line is admitted with a single boolean read - no re-parsing across the live stream.
374
+ if (this.#open) {
375
+ return true;
376
+ }
377
+ // The first line that begins a genuine entry opens the latch and is itself admitted.
378
+ if (isLogLineStart(line)) {
379
+ this.#open = true;
380
+ return true;
381
+ }
382
+ // Still leading seed noise (the byte-seek fragment, the server preamble, or an orphaned continuation of the truncated entry): drop it - unless we have already
383
+ // dropped `#maxSkip` lines without seeing an entry start, which means the stream carries no recognized timestamp (an unrecognized locale/format). In that case
384
+ // stop gating and admit the rest so the stream is never suppressed indefinitely.
385
+ if (this.#skipped >= this.#maxSkip) {
386
+ this.#open = true;
387
+ return true;
388
+ }
389
+ this.#skipped++;
390
+ return false;
391
+ }
392
+ }
393
+ //# sourceMappingURL=parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.js","sourceRoot":"","sources":["../../src/logclient/parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA0BH,sKAAsK;AACtK,sKAAsK;AACtK,sHAAsH;AACtH,4CAA4C;AAC5C,MAAM,YAAY,GAAG,yBAAyB,CAAC;AAE/C,qKAAqK;AACrK,qKAAqK;AACrK,yKAAyK;AACzK,+JAA+J;AAC/J,sKAAsK;AACtK,MAAM,SAAS,GAAuC,MAAM,CAAC,MAAM,CAAC;IAElE,IAAI,EAAE,OAAO;IACb,IAAI,EAAE,SAAS;IACf,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,OAAO;CACd,CAAC,CAAC;AAEH,+JAA+J;AAC/J,yKAAyK;AACzK,0IAA0I;AAC1I,4CAA4C;AAC5C,MAAM,WAAW,GAAG,gBAAgB,CAAC;AAErC,qKAAqK;AACrK,uKAAuK;AACvK,kKAAkK;AAClK,4BAA4B;AAC5B,MAAM,sBAAsB,GAAG,6BAA6B,CAAC;AAE7D,0KAA0K;AAC1K,qKAAqK;AACrK,mFAAmF;AACnF,MAAM,qBAAqB,GAAG,kFAAkF,CAAC;AAEjH,uKAAuK;AACvK,uKAAuK;AACvK,6GAA6G;AAC7G,MAAM,sBAAsB,GAAG,eAAe,CAAC;AAE/C,oKAAoK;AACpK,iFAAiF;AACjF,SAAS,SAAS,CAAC,IAAY;IAE7B,OAAO,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;AACxC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,OAAO,eAAe;IAE1B,kKAAkK;IAClK,gDAAgD;IAChD,MAAM,GAAG,EAAE,CAAC;IAEZ;;;;;;;;;OASG;IACI,CAAC,OAAO,CAAC,KAAa;QAE3B,kKAAkK;QAClK,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;QAEpE,qKAAqK;QACrK,6EAA6E;QAC7E,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,IAAI,MAAM,GAAG,CAAC,CAAC;QAEf,OAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;YAE7B,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAExC,gGAAgG;YAChG,IAAG,CAAC,SAAS,KAAK,IAAI,CAAC,IAAI,CAAC,SAAS,KAAK,IAAI,CAAC,EAAE,CAAC;gBAEhD,MAAM,EAAE,CAAC;gBAET,SAAS;YACX,CAAC;YAED,iKAAiK;YACjK,uHAAuH;YACvH,MAAM,OAAO,GAAG,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;YACjD,MAAM,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;YAE7C,iKAAiK;YACjK,mKAAmK;YACnK,yEAAyE;YACzE,IAAG,CAAC,OAAO,EAAE,CAAC;gBAEZ,MAAM;YACR,CAAC;YAED,+JAA+J;YAC/J,wDAAwD;YACxD,MAAM,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YAElC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,OAAO,CAAC;YAErD,MAAM,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACzB,KAAK,GAAG,MAAM,CAAC;QACjB,CAAC;QAED,mKAAmK;QACnK,+GAA+G;QAC/G,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC;IAED;;;;;;;;OAQG;IACI,CAAC,KAAK;QAEX,IAAG,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAE5B,OAAO;QACT,CAAC;QAED,oKAAoK;QACpK,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QAEpD,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QAEjB,MAAM,QAAQ,CAAC;IACjB,CAAC;CACF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW;IAEtC,iKAAiK;IACjK,8CAA8C;IAC9C,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;IAChC,MAAM,KAAK,GAAG,sBAAsB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAEpD,IAAG,KAAK,KAAK,IAAI,EAAE,CAAC;QAElB,qKAAqK;QACrK,uHAAuH;QACvH,OAAO,EAAE,KAAK,EAAE,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAC7F,CAAC;IAED,uKAAuK;IACvK,oKAAoK;IACpK,sKAAsK;IACtK,uKAAuK;IACvK,0EAA0E;IAC1E,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,KAAK,GAAG,CAAC,MAAM,CAAC;IAC/C,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,EAAE,iBAAiB,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAElF,6JAA6J;IAC7J,0CAA0C;IAC1C,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;AACzH,CAAC;AAED,sKAAsK;AACtK,yKAAyK;AACzK,kIAAkI;AAClI,SAAS,iBAAiB,CAAC,GAAW;IAEpC,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAEpC,IAAG,UAAU,KAAK,CAAC,CAAC,EAAE,CAAC;QAErB,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,WAAW,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,CAAC,CAAC,CAAC;IAErD,OAAO,WAAW,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC;AAClD,CAAC;AAED,oKAAoK;AACpK,uKAAuK;AACvK,mKAAmK;AACnK,yDAAyD;AACzD,SAAS,SAAS,CAAC,GAAW,EAAE,IAAY;IAE1C,wKAAwK;IACxK,6HAA6H;IAC7H,WAAW,CAAC,SAAS,GAAG,IAAI,CAAC;IAE7B,KAAI,IAAI,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,IAAI,EAAE,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAErF,8JAA8J;QAC9J,MAAM,UAAU,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAE/C,KAAI,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;YAElC,MAAM,MAAM,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC;YAEpC,IAAG,MAAM,KAAK,SAAS,EAAE,CAAC;gBAExB,OAAO,MAAM,CAAC;YAChB,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAE5C,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAE/C,IAAG,KAAK,KAAK,IAAI,EAAE,CAAC;QAElB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,kKAAkK;IAClK,4HAA4H;IAC5H,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IAClD,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IAChD,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,cAAc,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC;QACvI,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;IAEjD,sGAAsG;IACtG,IAAG,KAAK,KAAK,IAAI,EAAE,CAAC;QAElB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,sKAAsK;IACtK,mKAAmK;IACnK,wJAAwJ;IACxJ,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAEpF,IAAG,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC;QAElG,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC;AACxB,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,cAAc,CAAC,KAA0E;IAEvG,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IAEjD,yGAAyG;IACzG,IAAG,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC,EAAE,CAAC;QAElE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,0HAA0H;IAC1H,IAAG,QAAQ,KAAK,SAAS,EAAE,CAAC;QAE1B,IAAG,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC,EAAE,CAAC;YAE7B,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAClC,CAAC;IAED,gEAAgE;IAChE,IAAG,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC,EAAE,CAAC;QAE7B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,qIAAqI;IACrI,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,GAAG,CAAC;IAEtD,IAAG,IAAI,KAAK,EAAE,EAAE,CAAC;QAEf,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjD,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC3D,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IAExC,MAAM,KAAK,GAAG,sBAAsB,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;IAE1D,OAAO,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC,CAAC;AAC1E,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,OAAO,QAAQ;IAEnB,qJAAqJ;IACrJ,KAAK,GAAG,KAAK,CAAC;IAEd,2JAA2J;IAC3J,QAAQ,GAAG,CAAC,CAAC;IAEb,sKAAsK;IAC7J,QAAQ,CAAS;IAE1B;;;OAGG;IACH,YAAY,OAAe;QAEzB,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;IAC1B,CAAC;IAED;;;;;;;;OAQG;IACI,KAAK,CAAC,IAAY;QAEvB,uJAAuJ;QACvJ,IAAG,IAAI,CAAC,KAAK,EAAE,CAAC;YAEd,OAAO,IAAI,CAAC;QACd,CAAC;QAED,qFAAqF;QACrF,IAAG,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;YAExB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;YAElB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,+JAA+J;QAC/J,+JAA+J;QAC/J,iFAAiF;QACjF,IAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAElC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;YAElB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,CAAC,QAAQ,EAAE,CAAC;QAEhB,OAAO,KAAK,CAAC;IACf,CAAC;CACF"}
@@ -0,0 +1,41 @@
1
+ import type { SocketTarget } from "./endpoints.ts";
2
+ /**
3
+ * Options accepted by {@link downloadLog}: the connection target plus the raw token, an injectable `fetch` seam, and an optional abort signal.
4
+ *
5
+ * @property fetch - The fetch implementation to use. Defaults to the global `fetch`. Injected so the download flow is testable without a live server.
6
+ * @property host - The hostname or IP of the homebridge-config-ui-x server.
7
+ * @property port - The TCP port the server listens on.
8
+ * @property signal - Optional abort signal forwarded to the underlying `fetch`. When it aborts, the in-flight request and the body reader are both cancelled so the
9
+ * connection is released immediately rather than draining to completion in the background. A pre-aborted signal short-circuits the download before any
10
+ * request is made (the platform `fetch` rejects synchronously).
11
+ * @property tls - When `true`, use the secure (`https`) scheme; when `false` or omitted, plaintext (`http`).
12
+ * @property token - The raw bearer token, sent as `Authorization: Bearer <token>`.
13
+ *
14
+ * @category Log Client
15
+ */
16
+ export interface DownloadLogOptions extends SocketTarget {
17
+ readonly fetch?: typeof fetch;
18
+ readonly signal?: AbortSignal;
19
+ }
20
+ /**
21
+ * Download the entire Homebridge log file over REST and yield it as raw lines.
22
+ *
23
+ * Streams `GET /api/platform-tools/hb-service/log/download?colour=yes` through the shared {@link LogLineSplitter}, yielding each raw line (ANSI intact) as it becomes
24
+ * available, and flushing the splitter at end-of-response so the file's final line is never stranded. The whole-file download is the deep-history channel: the server
25
+ * exposes no tail/range parameter, so a caller that only wants the most recent N lines drains this iterable and retains the tail (e.g., via `takeLast`).
26
+ *
27
+ * When `options.signal` is supplied, the download is abortable: aborting the signal cancels the in-flight `fetch` and the body reader so the connection is released
28
+ * immediately rather than draining in the background, and a pre-aborted signal short-circuits before any request is made. This is what lets the windowed hedge supersede
29
+ * a speculative deep-history download the moment the socket seed is shown to cover the window.
30
+ *
31
+ * @param options - The connection target, the raw token, the injectable `fetch` seam, and the optional abort signal. See {@link DownloadLogOptions}.
32
+ *
33
+ * @returns An async iterable of raw log lines (escapes intact, terminators removed), in file order.
34
+ *
35
+ * @throws `Error` when the server returns a 400 (the log method is not file-backed, so there is no file to download - the message advises `--follow`), or any other
36
+ * non-2xx status, or when the response carries no body. Propagates `options.signal`'s reason (typically an `HbpuAbortError`) when the signal aborts.
37
+ *
38
+ * @category Log Client
39
+ */
40
+ export declare function downloadLog(options: DownloadLogOptions): AsyncIterable<string>;
41
+ //# sourceMappingURL=rest.d.ts.map
@@ -0,0 +1,111 @@
1
+ /* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * logclient/rest.ts: Streamed whole-file log download from the homebridge-config-ui-x REST API.
4
+ */
5
+ /**
6
+ * Streamed REST log retrieval for the Homebridge UI log client.
7
+ *
8
+ * {@link downloadLog} fetches the entire log file from `GET /api/platform-tools/hb-service/log/download?colour=yes` and yields it line by line as an
9
+ * `AsyncIterable<string>` of raw lines (ANSI escapes intact). The server has no range/tail parameter - it always streams the whole file - so this is the deep-history
10
+ * channel paid only when the user explicitly asks for history beyond the socket's ~500-line seed (see the cost model on `TailRequest` in `types.ts`).
11
+ *
12
+ * Two details matter here:
13
+ *
14
+ * - The response body is streamed, not buffered. We feed each chunk through the shared {@link LogLineSplitter} so a multi-MB log never has to be materialized in memory
15
+ * as one string, and the consumer can begin processing lines as they arrive. The splitter handles lines split across chunk boundaries and the mixed newline
16
+ * conventions transparently.
17
+ * - We MUST call `splitter.flush()` at end-of-response. The splitter withholds a chunk-final lone line-feed pending a possible split `\n\r`/`\r\n` pair in the next
18
+ * chunk; at end-of-response no next chunk arrives, so without a flush the final line of the file would be stranded in the carry and silently lost.
19
+ *
20
+ * The transport asks for `colour=yes` (the raw file with ANSI intact) rather than `colour=no`, because the server's color stripping shifts byte offsets and, more
21
+ * importantly, the ANSI color IS the severity-level signal the parser reads. When the log method is `systemd`/`custom` the endpoint returns 400; we map that to a clear
22
+ * "no log file on disk; use --follow" error rather than surfacing a bare HTTP status.
23
+ *
24
+ * The `fetch` implementation is injected (defaulting to the global `fetch`) so the whole flow is exercised in tests without a live server.
25
+ *
26
+ * @module
27
+ */
28
+ import { LogLineSplitter } from "./parser.js";
29
+ import { httpBaseUrl } from "./endpoints.js";
30
+ // Decode a streamed byte chunk to text. We hold a single `TextDecoder` across the whole response with `{ stream: true }` so a multi-byte UTF-8 sequence split across two
31
+ // chunks is reassembled correctly rather than producing a replacement character at the boundary. The final `decode()` with no argument flushes any trailing partial
32
+ // sequence.
33
+ async function* readLines(body, signal) {
34
+ const splitter = new LogLineSplitter();
35
+ const decoder = new TextDecoder();
36
+ const reader = body.getReader();
37
+ try {
38
+ for (;;) {
39
+ // eslint-disable-next-line no-await-in-loop
40
+ const { done, value } = await reader.read();
41
+ if (done) {
42
+ break;
43
+ }
44
+ // Decode this chunk (streaming, so a split multi-byte sequence carries into the next read) and feed it to the splitter, yielding every complete raw line the chunk
45
+ // contained or completed.
46
+ for (const line of splitter.consume(decoder.decode(value, { stream: true }))) {
47
+ yield line;
48
+ }
49
+ }
50
+ // Flush the decoder's trailing partial sequence (if any) into the splitter, then flush the splitter's held final line. The splitter withholds a chunk-final lone
51
+ // line-feed pending a possible cross-chunk pair; at end-of-response no further chunk arrives, so this flush is what surfaces the file's last line rather than
52
+ // stranding it in the carry.
53
+ const tail = decoder.decode();
54
+ if (tail.length > 0) {
55
+ for (const line of splitter.consume(tail)) {
56
+ yield line;
57
+ }
58
+ }
59
+ for (const line of splitter.flush()) {
60
+ yield line;
61
+ }
62
+ }
63
+ finally {
64
+ // When the download was aborted (the hedge superseded it, or the caller tore the stream down), actively cancel the body first so undici aborts the request and the
65
+ // connection is released immediately rather than draining to completion in the background as a pinned connection; the cancel's rejection on an already-aborted body
66
+ // is expected, so it is swallowed. In every case release the reader lock - normal completion, an early `break` by the consumer, or a thrown error - so the underlying
67
+ // stream is never left locked.
68
+ if (signal?.aborted) {
69
+ void reader.cancel().catch(() => { });
70
+ }
71
+ reader.releaseLock();
72
+ }
73
+ }
74
+ /**
75
+ * Download the entire Homebridge log file over REST and yield it as raw lines.
76
+ *
77
+ * Streams `GET /api/platform-tools/hb-service/log/download?colour=yes` through the shared {@link LogLineSplitter}, yielding each raw line (ANSI intact) as it becomes
78
+ * available, and flushing the splitter at end-of-response so the file's final line is never stranded. The whole-file download is the deep-history channel: the server
79
+ * exposes no tail/range parameter, so a caller that only wants the most recent N lines drains this iterable and retains the tail (e.g., via `takeLast`).
80
+ *
81
+ * When `options.signal` is supplied, the download is abortable: aborting the signal cancels the in-flight `fetch` and the body reader so the connection is released
82
+ * immediately rather than draining in the background, and a pre-aborted signal short-circuits before any request is made. This is what lets the windowed hedge supersede
83
+ * a speculative deep-history download the moment the socket seed is shown to cover the window.
84
+ *
85
+ * @param options - The connection target, the raw token, the injectable `fetch` seam, and the optional abort signal. See {@link DownloadLogOptions}.
86
+ *
87
+ * @returns An async iterable of raw log lines (escapes intact, terminators removed), in file order.
88
+ *
89
+ * @throws `Error` when the server returns a 400 (the log method is not file-backed, so there is no file to download - the message advises `--follow`), or any other
90
+ * non-2xx status, or when the response carries no body. Propagates `options.signal`'s reason (typically an `HbpuAbortError`) when the signal aborts.
91
+ *
92
+ * @category Log Client
93
+ */
94
+ export async function* downloadLog(options) {
95
+ const fetchImpl = options.fetch ?? fetch;
96
+ const url = httpBaseUrl(options) + "/api/platform-tools/hb-service/log/download?colour=yes";
97
+ const response = await fetchImpl(url, { headers: { "Authorization": "Bearer " + options.token }, method: "GET", signal: options.signal });
98
+ // A 400 from this endpoint means the configured log method is not file-backed (systemd/custom), so there is no file on disk to download. Map it to an actionable
99
+ // message that points the user at the live-tail channel rather than surfacing a bare status code.
100
+ if (response.status === 400) {
101
+ throw new Error("No log file is available to download on disk (the Homebridge log method is not file-backed); use --follow to live-tail instead.");
102
+ }
103
+ if (!response.ok) {
104
+ throw new Error("Log download failed - the server returned HTTP " + response.status.toString() + " (" + response.statusText + ").");
105
+ }
106
+ if (response.body === null) {
107
+ throw new Error("Log download failed - the server returned an empty response body.");
108
+ }
109
+ yield* readLines(response.body, options.signal);
110
+ }
111
+ //# sourceMappingURL=rest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rest.js","sourceRoot":"","sources":["../../src/logclient/rest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ export declare const DEFAULT_HOST = "localhost";
2
+ export declare const DEFAULT_PORT = 8581;
3
+ export declare const LOG_NAMESPACE = "log";
4
+ export declare const SOCKET_PATH = "/socket.io/";
5
+ export declare const PTY_COLUMNS = 80;
6
+ export declare const PTY_ROWS = 24;
7
+ export declare const RECONNECT_BASE_MS = 500;
8
+ export declare const RECONNECT_CAP_MS = 5000;
9
+ export declare const JITTER_FRACTION = 0.5;
10
+ export declare const MARGIN_MS = 5000;
11
+ export declare const SEED_QUIESCENCE_MS = 250;
12
+ export declare const SEED_SETTLE_MS = 1000;
13
+ export declare const SEED_WINDOW_MAX_MS = 5000;
14
+ export declare const SEED_GATE_MAX_SKIP = 100;
15
+ //# sourceMappingURL=settings.d.ts.map