git-watchtower 2.3.19 → 2.3.21

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "2.3.19",
3
+ "version": "2.3.21",
4
4
  "description": "Terminal-based Git branch monitor with activity sparklines and optional dev server with live reload",
5
5
  "main": "bin/git-watchtower.js",
6
6
  "bin": {
@@ -209,15 +209,47 @@ class Store {
209
209
  }
210
210
 
211
211
  /**
212
- * Get a copy of the current state
212
+ * Get a snapshot of the current state.
213
+ *
214
+ * The top-level object is a shallow copy (so adding/removing top-level
215
+ * keys on the returned object does not mutate the store). Map values
216
+ * (sparklineCache, branchPrStatusMap, aheadBehindCache) are also
217
+ * shallow-cloned so that consumers — most importantly the web
218
+ * dashboard's getSerializableState — can iterate or mutate the
219
+ * snapshot without the store's live data shifting underneath them.
220
+ *
221
+ * If you need the live, by-reference Map for in-place mutation
222
+ * followed by setState() (the polling cache-prune pattern), use
223
+ * `get(key)` instead.
224
+ *
213
225
  * @returns {State}
214
226
  */
215
227
  getState() {
216
- return { ...this.state };
228
+ const snapshot = { ...this.state };
229
+ if (snapshot.sparklineCache instanceof Map) {
230
+ snapshot.sparklineCache = new Map(snapshot.sparklineCache);
231
+ }
232
+ if (snapshot.branchPrStatusMap instanceof Map) {
233
+ snapshot.branchPrStatusMap = new Map(snapshot.branchPrStatusMap);
234
+ }
235
+ if (snapshot.aheadBehindCache instanceof Map) {
236
+ snapshot.aheadBehindCache = new Map(snapshot.aheadBehindCache);
237
+ }
238
+ return snapshot;
217
239
  }
218
240
 
219
241
  /**
220
- * Get a specific state value
242
+ * Get a specific state value by-reference.
243
+ *
244
+ * IMPORTANT: returns the live value, not a copy. For Map values
245
+ * (sparklineCache, branchPrStatusMap, aheadBehindCache) this is
246
+ * deliberate — the polling layer mutates these in place and then
247
+ * calls setState({key: sameMap}) to notify subscribers. Code that
248
+ * doesn't intend to mutate should treat the result as read-only,
249
+ * or copy it first (e.g. `new Map(store.get('sparklineCache'))`).
250
+ *
251
+ * For an isolation-safe snapshot, prefer `getState()`.
252
+ *
221
253
  * @template {keyof State} K
222
254
  * @param {K} key - State key
223
255
  * @returns {State[K]}
package/src/ui/ansi.js CHANGED
@@ -296,8 +296,15 @@ const NON_SGR_CSI_RE = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x6c\x6e-\x7e]/g;
296
296
 
297
297
  // Match OSC sequences: ESC ] ... terminator (BEL or ESC \). These set
298
298
  // terminal title, hyperlinks, etc. — all undesirable in untrusted input.
299
+ // The trailing `|$` alternation also matches an UNTERMINATED OSC tail
300
+ // (`\x1b]0;evil-text` with no \x07 / \x1b\\ before EOF). Without it,
301
+ // the literal payload survived sanitisation as visible text — ESC_RE
302
+ // stripped the leading `\x1b]` since `]` is in the `\\-_` range, but
303
+ // "0;evil-text" remained. Per ECMA-48, an unterminated OSC at EOF is
304
+ // effectively an open control string, so dropping it entirely is the
305
+ // safe default.
299
306
  // eslint-disable-next-line no-control-regex
300
- const OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
307
+ const OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\|$)/g;
301
308
 
302
309
  // Match other 2-byte ESC sequences (Fe codes 0x40-0x5F) excluding CSI ([)
303
310
  // and OSC (]) which are handled separately above.