react-hooks-global-states-debug 16.0.4 → 16.0.5

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 (4) hide show
  1. package/cli.mjs +706 -0
  2. package/debug.js +45 -32
  3. package/index.d.ts +1 -1
  4. package/package.json +22 -3
package/cli.mjs ADDED
@@ -0,0 +1,706 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/agent/protocol.ts
4
+ var AGENT_DEFAULT_PORT = 7787;
5
+ var AGENT_CLOSE_SUPERSEDED = 4001;
6
+ var ALL_STORES = "*";
7
+
8
+ // src/cli/args.ts
9
+ var DEFAULT_TIMEOUT_SECONDS = 10;
10
+ var HELP = `rgsh: see what happens inside your stores from a terminal, and drive them
11
+
12
+ WHAT IT DOES
13
+ Connects to the React Global State Hooks DevTools panel and streams one block per store action:
14
+ the store, the action and its input, the state changes it caused, the result or error, and the
15
+ duration. Other commands read a store, run an action, or change its state.
16
+
17
+ BEFORE YOU START
18
+ 1. The app must import the debug package (import 'react-hooks-global-states/debug').
19
+ 2. Chrome has the DevTools extension, with DevTools open on the app tab and the
20
+ react-global-state-hooks panel opened once. rgsh cannot start without it.
21
+ 3. Same port on both sides: rgsh listens on 7787 by default; change it with --port and in the
22
+ panel under gear > Terminal connection.
23
+
24
+ DISCOVER
25
+ rgsh --list every store: name or creation location, state preview, actions
26
+ rgsh pick stores with the arrow keys (a list when there is no terminal)
27
+ rgsh state <store> [path] current state and metadata. path: todos[0].done, user.name
28
+ A store is a name, or the creation location of an unnamed store
29
+ (ShoppingCart.tsx:23), as printed by --list.
30
+
31
+ WATCH
32
+ rgsh --store todos one store
33
+ rgsh --store todos,auth several (comma separated, or repeat --store)
34
+ rgsh --store "*" every store: DevTools does more work and the stream is much larger
35
+
36
+ CHANGE
37
+ rgsh action <store> <name> [args...] run an action: rgsh action todos add "Write the docs"
38
+ rgsh patch <store> <json> objects merge into the state, anything else replaces it
39
+ rgsh patch todos '{"filter":"done"}' rgsh patch counter 5
40
+ rgsh set <store> <json> replace the whole state (like the State tab editor)
41
+ Each command prints the resulting block (state changes, result or error) and exits.
42
+
43
+ VALUES
44
+ Arguments and states are JSON: 5, true, null, {"a":1}, ["x"], "text". A plain word is text.
45
+ A value that starts with { [ or " must be valid JSON. Never code: nothing is evaluated.
46
+ Numbers and booleans that should be text need quotes: '"5"'.
47
+
48
+ OPTIONS
49
+ --store, -s <names> stores to watch
50
+ --list, -l list stores and exit
51
+ --port <number> port to listen on (default ${AGENT_DEFAULT_PORT}); must match the panel
52
+ --timeout <seconds> how long action/patch/set wait for the result (default ${DEFAULT_TIMEOUT_SECONDS})
53
+ --help, -h
54
+
55
+ READING THE OUTPUT
56
+ 12:03:41.221 [todos] add("Write the docs") time, store, action(input)
57
+ state: state (1/2), (2/2)... when the action set state several times
58
+ todos[2]: undefined \u2192 {"id":3} path: before \u2192 after; undefined = did not exist / removed
59
+ result: {"ok":true} omitted when the action returned nothing
60
+ error: Error: boom instead of result when the action threw
61
+ duration: 6ms
62
+ [name #2] means several live instances share that store. "setState" is a direct state change.
63
+ "store created" / "store removed": the store appeared or disappeared (mount, unmount, reload).
64
+ Events print when an action finishes, so a slow async action appears after it settles.
65
+
66
+ EXIT CODES
67
+ 0 done 1 failed (unknown store or action, not allowed, action threw) 2 no --store given, no terminal
68
+ 3 sent, but the page reported nothing within --timeout
69
+
70
+ GOOD TO KNOW
71
+ - Unnamed stores are identified by where they were created; name your stores in minified builds.
72
+ - Metadata shown by "state" is what the page announced when the store was created. Changes made
73
+ later with setMetadata are not reported by the debug package.
74
+ - Values that cannot be serialized (functions, DOM nodes) appear as {"__non_serializable__": ...}
75
+ and are kept as they are when you patch or set.
76
+ - Large values are cut with an explicit marker such as \u2026(+120 chars) or \u2026(+8 items).
77
+ - Only one store at a time for action, set, patch and state. A store with several live
78
+ instances is refused.
79
+ - Ctrl+C stops watching; DevTools goes back to normal and the terminal work stops.`;
80
+ var UsageError = class extends Error {
81
+ };
82
+ var parseValue = (text) => {
83
+ try {
84
+ return JSON.parse(text);
85
+ } catch {
86
+ if (/^\s*[{["]/.test(text)) throw new UsageError(`Not valid JSON: ${text}`);
87
+ return text;
88
+ }
89
+ };
90
+ var parseCommand = (positionals) => {
91
+ const [name, store, ...rest] = positionals;
92
+ if (name === void 0) return null;
93
+ if (name === "action") {
94
+ const [action, ...args] = rest;
95
+ if (!store || !action) throw new UsageError("Usage: rgsh action <store> <action> [args...]");
96
+ return { kind: "action", store, action, args: args.map(parseValue) };
97
+ }
98
+ if (name === "set" || name === "patch") {
99
+ if (!store || rest.length !== 1) throw new UsageError(`Usage: rgsh ${name} <store> <json>`);
100
+ const value = parseValue(rest[0]);
101
+ return name === "set" ? { kind: "set", store, state: value } : { kind: "patch", store, patch: value };
102
+ }
103
+ if (name === "state") {
104
+ if (!store || rest.length > 1) throw new UsageError("Usage: rgsh state <store> [path]");
105
+ return { kind: "state", store, path: rest[0] ?? "" };
106
+ }
107
+ throw new UsageError(`Unknown command "${name}"`);
108
+ };
109
+ var parseArgs = (argv) => {
110
+ const options = {
111
+ targets: [],
112
+ command: null,
113
+ timeoutSeconds: DEFAULT_TIMEOUT_SECONDS,
114
+ list: false,
115
+ help: false,
116
+ port: AGENT_DEFAULT_PORT
117
+ };
118
+ const positionals = [];
119
+ const takeValue = (flag, inline, index) => {
120
+ if (inline !== void 0) return [inline, index];
121
+ const value = argv[index + 1];
122
+ if (value === void 0 || value.startsWith("-") && value !== ALL_STORES) {
123
+ throw new UsageError(`${flag} needs a value`);
124
+ }
125
+ return [value, index + 1];
126
+ };
127
+ for (let index = 0; index < argv.length; index++) {
128
+ const [flag, inline] = argv[index].split(/=(.*)/s, 2);
129
+ if (flag === "--help" || flag === "-h") options.help = true;
130
+ else if (flag === "--list" || flag === "-l") options.list = true;
131
+ else if (flag === "--store" || flag === "-s") {
132
+ const [value, next] = takeValue(flag, inline, index);
133
+ index = next;
134
+ options.targets.push(...value.split(",").map((target) => target.trim()).filter(Boolean));
135
+ } else if (flag === "--timeout") {
136
+ const [value, next] = takeValue(flag, inline, index);
137
+ index = next;
138
+ options.timeoutSeconds = Number(value);
139
+ if (!(options.timeoutSeconds > 0)) throw new UsageError(`--timeout must be a positive number, got "${value}"`);
140
+ } else if (flag === "--port") {
141
+ const [value, next] = takeValue(flag, inline, index);
142
+ index = next;
143
+ options.port = Number(value);
144
+ if (!Number.isInteger(options.port) || options.port < 1 || options.port > 65535) {
145
+ throw new UsageError(`--port must be a number between 1 and 65535, got "${value}"`);
146
+ }
147
+ } else if (argv[index].startsWith("-") && argv[index] !== ALL_STORES && !/^-\d/.test(argv[index])) {
148
+ throw new UsageError(`Unknown option "${argv[index]}"`);
149
+ } else {
150
+ positionals.push(argv[index]);
151
+ }
152
+ }
153
+ options.command = parseCommand(positionals);
154
+ if (options.command && options.targets.length) {
155
+ throw new UsageError("--store does not combine with state/action/set/patch: name the store after the command");
156
+ }
157
+ return options;
158
+ };
159
+
160
+ // src/cli/format.ts
161
+ var INLINE_MAX = 100;
162
+ var pad = (value, size = 2) => String(value).padStart(size, "0");
163
+ var formatTime = (at) => {
164
+ const date = new Date(at);
165
+ return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
166
+ };
167
+ var formatStoreRef = ({ label, instance }) => `[${label}${instance ? ` #${instance}` : ""}]`;
168
+ var formatValue = (value, indent) => {
169
+ if (value === void 0) return "undefined";
170
+ const inline = JSON.stringify(value);
171
+ if (inline.length <= INLINE_MAX) return inline;
172
+ return JSON.stringify(value, null, 2).replace(/\n/g, `
173
+ ${indent}`);
174
+ };
175
+ var formatChange = (change, indent) => {
176
+ const { path, before, after } = change;
177
+ if (!("before" in change) && !("after" in change)) return path;
178
+ return `${path || "(state)"}: ${formatValue(before, indent)} \u2192 ${formatValue(after, indent)}`;
179
+ };
180
+ var formatSteps = (event) => {
181
+ const { steps, stateCalls } = event;
182
+ if (!stateCalls) return [];
183
+ if (!steps.length) return [" state: no change"];
184
+ return steps.flatMap((changes, index) => {
185
+ const title = steps.length > 1 ? ` state (${index + 1}/${steps.length}):` : " state:";
186
+ return [title, ...changes.map((change) => ` ${formatChange(change, " ")}`)];
187
+ });
188
+ };
189
+ var formatAction = (event) => {
190
+ const { store, action, input, result, durationMs } = event;
191
+ const call = action === "setState" ? "setState" : `${action}(${(input ?? []).map((arg) => JSON.stringify(arg)).join(", ")})`;
192
+ const lines = [`${formatTime(event.at)} ${formatStoreRef(store)} ${call}`, ...formatSteps(event)];
193
+ if (result && "error" in result) lines.push(` error: ${result.error}`);
194
+ else if (result && result.value !== void 0) lines.push(` result: ${formatValue(result.value, " ")}`);
195
+ if (durationMs !== void 0) lines.push(` duration: ${durationMs}ms`);
196
+ return lines;
197
+ };
198
+ var formatEvent = (event) => {
199
+ if (event.kind === "action") return formatAction(event).join("\n");
200
+ const what = event.kind === "store-created" ? "store created" : "store removed";
201
+ return `${formatTime(event.at)} ${formatStoreRef(event.store)} ${what}`;
202
+ };
203
+ var storeTitle = (store) => {
204
+ if (store.name) return store.instances > 1 ? `${store.name} (${store.instances} instances)` : store.name;
205
+ return `unnamed \u2014 ${store.location ?? "unknown location"}`;
206
+ };
207
+ var storeLabel = (store) => store.name ?? `unnamed ${store.location ?? ""}`.trim();
208
+ var oneLine = (text, max = 120) => {
209
+ const flat = text.replace(/\s*\n\s*/g, " ");
210
+ return flat.length > max ? `${flat.slice(0, max)}\u2026` : flat;
211
+ };
212
+ var formatStoreList = (stores) => {
213
+ if (!stores.length) return "No stores known by DevTools yet. Load the app with the debug package imported.";
214
+ return stores.map(
215
+ (store) => [
216
+ storeTitle(store),
217
+ ...store.name && store.location ? [` at ${store.location}`] : [],
218
+ ` state: ${oneLine(store.preview)}`,
219
+ ...store.actions.length ? [` actions: ${store.actions.join(", ")}`] : []
220
+ ].join("\n")
221
+ ).join("\n\n");
222
+ };
223
+ var formatState = (reply) => {
224
+ const pretty = (value) => value === void 0 ? "undefined" : JSON.stringify(value, null, 2);
225
+ return [
226
+ `${formatStoreRef(reply.store)} state${reply.path ? ` at ${reply.path}` : ""}:`,
227
+ pretty(reply.state),
228
+ "",
229
+ "metadata (as announced when the store was created; later setMetadata calls are not reported):",
230
+ pretty(reply.metadata)
231
+ ].join("\n");
232
+ };
233
+
234
+ // src/cli/resolve.ts
235
+ var MAX_CANDIDATES = 8;
236
+ var editDistance = (a, b) => {
237
+ let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
238
+ for (let i = 1; i <= a.length; i++) {
239
+ const current = [i];
240
+ for (let j = 1; j <= b.length; j++) {
241
+ current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
242
+ }
243
+ previous = current;
244
+ }
245
+ return previous[b.length];
246
+ };
247
+ var findCandidates = (target, stores) => {
248
+ const needle = target.toLowerCase();
249
+ const labels = stores.map(storeLabel);
250
+ const close = labels.filter((label) => {
251
+ const candidate = label.toLowerCase();
252
+ return candidate.includes(needle) || needle.includes(candidate) || editDistance(candidate, needle) <= 2;
253
+ });
254
+ return (close.length ? close : labels).slice(0, MAX_CANDIDATES);
255
+ };
256
+ var resolveTargets = (targets, stores) => {
257
+ const selectors = /* @__PURE__ */ new Set();
258
+ const unknown = [];
259
+ for (const target of targets) {
260
+ const byName = stores.filter((store) => store.name === target);
261
+ const matches = byName.length ? byName : stores.filter((store) => store.location?.endsWith(target));
262
+ if (!matches.length) {
263
+ unknown.push({ target, candidates: findCandidates(target, stores) });
264
+ continue;
265
+ }
266
+ for (const store of matches) selectors.add(store.selector);
267
+ }
268
+ return { selectors: [...selectors], unknown };
269
+ };
270
+
271
+ // src/cli/server.ts
272
+ var REQUEST_TIMEOUT_MS = 1e4;
273
+ var PanelSession = class {
274
+ constructor(socket, hello) {
275
+ this.socket = socket;
276
+ this.hello = hello;
277
+ this.replyWaiters = /* @__PURE__ */ new Set();
278
+ this.eventListeners = [];
279
+ this.closeListeners = [];
280
+ socket.on("message", (data) => this.receive(data.toString()));
281
+ socket.on("close", () => {
282
+ for (const listener of this.closeListeners) listener();
283
+ });
284
+ }
285
+ receive(text) {
286
+ let message;
287
+ try {
288
+ message = JSON.parse(text);
289
+ } catch {
290
+ return;
291
+ }
292
+ if (message.type === "EVENT") {
293
+ for (const listener of this.eventListeners) listener(message.event);
294
+ return;
295
+ }
296
+ for (const waiter of [...this.replyWaiters]) waiter(message);
297
+ }
298
+ onEvent(listener) {
299
+ this.eventListeners.push(listener);
300
+ }
301
+ onClose(listener) {
302
+ this.closeListeners.push(listener);
303
+ }
304
+ send(message) {
305
+ this.socket.send(JSON.stringify(message));
306
+ }
307
+ /** Sends a request and resolves with the first reply of one of the expected types. */
308
+ request(message, expected) {
309
+ return new Promise((resolve, reject) => {
310
+ const finish = () => {
311
+ clearTimeout(timer);
312
+ this.replyWaiters.delete(waiter);
313
+ this.closeListeners = this.closeListeners.filter((listener) => listener !== onClose);
314
+ };
315
+ const waiter = (reply) => {
316
+ if (!expected.includes(reply.type)) return;
317
+ finish();
318
+ resolve(reply);
319
+ };
320
+ const onClose = () => {
321
+ finish();
322
+ reject(new Error("DevTools panel disconnected"));
323
+ };
324
+ const timer = setTimeout(() => {
325
+ finish();
326
+ reject(new Error(`DevTools did not answer ${message.type} in ${REQUEST_TIMEOUT_MS / 1e3}s`));
327
+ }, REQUEST_TIMEOUT_MS);
328
+ this.replyWaiters.add(waiter);
329
+ this.closeListeners.push(onClose);
330
+ this.send(message);
331
+ });
332
+ }
333
+ };
334
+ var isAllowedOrigin = (origin) => origin === void 0 || origin.startsWith("chrome-extension://");
335
+ var MissingWsError = class extends Error {
336
+ constructor() {
337
+ super(
338
+ [
339
+ "rgsh needs the 'ws' package to talk to the DevTools extension, and it is not installed in this project.",
340
+ "Install it as a dev dependency:",
341
+ " yarn add -D ws",
342
+ " npm install -D ws",
343
+ " pnpm add -D ws"
344
+ ].join("\n")
345
+ );
346
+ this.name = "MissingWsError";
347
+ }
348
+ };
349
+ var loadWebSocketServer = async () => {
350
+ try {
351
+ const ws = await import("ws");
352
+ return ws.WebSocketServer ?? ws.default.Server;
353
+ } catch (error) {
354
+ if (error.code === "ERR_MODULE_NOT_FOUND") throw new MissingWsError();
355
+ throw error;
356
+ }
357
+ };
358
+ var startServer = async (port) => {
359
+ const WebSocketServer = await loadWebSocketServer();
360
+ return new Promise((resolve, reject) => {
361
+ const panelListeners = [];
362
+ let current = null;
363
+ const wss = new WebSocketServer({
364
+ host: "127.0.0.1",
365
+ port,
366
+ verifyClient: ({ origin }) => isAllowedOrigin(origin)
367
+ });
368
+ wss.once("error", reject);
369
+ wss.on("connection", (socket) => {
370
+ socket.once("message", (data) => {
371
+ let hello;
372
+ try {
373
+ hello = JSON.parse(data.toString());
374
+ } catch {
375
+ return socket.close();
376
+ }
377
+ if (hello.type !== "HELLO") return socket.close();
378
+ current?.close(AGENT_CLOSE_SUPERSEDED, "a newer DevTools panel connected");
379
+ current = socket;
380
+ socket.on("close", () => {
381
+ if (current === socket) current = null;
382
+ });
383
+ const panel = new PanelSession(socket, hello);
384
+ for (const listener of panelListeners) listener(panel);
385
+ });
386
+ });
387
+ wss.once("listening", () => {
388
+ wss.off("error", reject);
389
+ wss.on("error", () => void 0);
390
+ resolve({
391
+ port: wss.address().port,
392
+ onPanel: (listener) => panelListeners.push(listener),
393
+ close: () => new Promise((done) => {
394
+ for (const client of wss.clients) client.close();
395
+ wss.close(() => done());
396
+ })
397
+ });
398
+ });
399
+ });
400
+ };
401
+
402
+ // src/cli/run.ts
403
+ var EXIT = { ok: 0, failed: 1, needsTarget: 2, noOutcome: 3 };
404
+ var ALL_WARNING = 'warning: --store "*" listens to every store. DevTools does extra processing for every state change and this stream gets much larger. Naming the stores you need (--store todos,auth) is more efficient.';
405
+ var RESUBSCRIBE_ATTEMPTS = 5;
406
+ var RESUBSCRIBE_DELAY_MS = 1e3;
407
+ var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
408
+ var describePanel = ({ tabId, page }) => [tabId !== null ? `tab ${tabId}` : null, page].filter(Boolean).join(" ");
409
+ var run = async (options, io, signal) => {
410
+ let server;
411
+ try {
412
+ server = await startServer(options.port);
413
+ } catch (error) {
414
+ const { code } = error;
415
+ io.err(
416
+ error instanceof MissingWsError ? error.message : code === "EADDRINUSE" ? `Port ${options.port} is already in use. Is another rgsh running? Stop it or pass --port.` : `Could not start the listener on port ${options.port}: ${error.message}`
417
+ );
418
+ return EXIT.failed;
419
+ }
420
+ return new Promise((resolve) => {
421
+ let current = null;
422
+ let subscribed = null;
423
+ let finished = false;
424
+ let commandStarted = false;
425
+ const finish = async (code) => {
426
+ if (finished) return;
427
+ finished = true;
428
+ await server.close();
429
+ resolve(code);
430
+ };
431
+ signal.addEventListener("abort", () => void finish(EXIT.ok), { once: true });
432
+ const subscribe = async (panel, selectors) => {
433
+ const reply = await panel.request({ type: "SUBSCRIBE", selectors }, ["SUBSCRIBED", "REJECTED"]);
434
+ if (reply.type === "REJECTED") return reply;
435
+ subscribed = selectors;
436
+ const names = reply.matched.length ? reply.matched.map(formatStoreRef).join(" ") : "(no store yet)";
437
+ io.err(`Listening to ${reply.all ? "all stores" : names}. Press Ctrl+C to stop.`);
438
+ return null;
439
+ };
440
+ const runCommand = async (panel, command) => {
441
+ const { stores } = await panel.request({ type: "GET_STORES" }, ["AVAILABLE_STORES"]);
442
+ const resolved = resolveTargets([command.store], stores);
443
+ if (resolved.unknown.length) {
444
+ const [{ target, candidates }] = resolved.unknown;
445
+ io.err(`Store "${target}" was not found. Available: ${candidates.join(", ") || "(none)"}`);
446
+ return finish(EXIT.failed);
447
+ }
448
+ const [selector] = resolved.selectors;
449
+ if (resolved.selectors.length !== 1) {
450
+ io.err(`"${command.store}" matches ${resolved.selectors.length} stores. Name exactly one.`);
451
+ return finish(EXIT.failed);
452
+ }
453
+ if (command.kind === "state") {
454
+ const reply2 = await panel.request({ type: "GET_STATE", selector, path: command.path }, ["STATE", "REQUEST_REJECTED"]);
455
+ if (reply2.type === "REQUEST_REJECTED") {
456
+ io.err(reply2.reason);
457
+ return finish(EXIT.failed);
458
+ }
459
+ if (!reply2.found) {
460
+ io.err(`Nothing at "${reply2.path}" in ${formatStoreRef(reply2.store)}.`);
461
+ return finish(EXIT.failed);
462
+ }
463
+ io.out(`${formatState(reply2)}
464
+ `);
465
+ return finish(EXIT.ok);
466
+ }
467
+ const watch = await panel.request({ type: "SUBSCRIBE", selectors: [selector] }, ["SUBSCRIBED", "REJECTED"]);
468
+ if (watch.type === "REJECTED") {
469
+ io.err(`${watch.reason}: ${watch.unknown.join(", ")}`);
470
+ return finish(EXIT.failed);
471
+ }
472
+ const isWatched = ({ label, instance }) => watch.matched.some((store) => store.label === label && store.instance === instance);
473
+ const expectedAction = command.kind === "action" ? command.action : "setState";
474
+ const seen = [];
475
+ const outcome = new Promise((done) => {
476
+ const timer = setTimeout(() => done(null), options.timeoutSeconds * 1e3);
477
+ panel.onEvent((event2) => {
478
+ seen.push(event2);
479
+ if (event2.kind !== "action" || event2.action !== expectedAction || !isWatched(event2.store)) return;
480
+ clearTimeout(timer);
481
+ done(event2);
482
+ });
483
+ });
484
+ const request = (() => {
485
+ if (command.kind === "set") return { type: "SET_STATE", selector, state: command.state };
486
+ if (command.kind === "patch") return { type: "PATCH_STATE", selector, patch: command.patch };
487
+ return { type: "RUN_ACTION", selector, action: command.action, args: command.args };
488
+ })();
489
+ const reply = await panel.request(request, ["DISPATCHED", "REQUEST_REJECTED"]);
490
+ if (reply.type === "REQUEST_REJECTED") {
491
+ io.err(reply.reason);
492
+ return finish(EXIT.failed);
493
+ }
494
+ const event = await outcome;
495
+ for (const other of seen) if (other !== event) io.out(`${formatEvent(other)}
496
+
497
+ `);
498
+ if (!event) {
499
+ io.err(
500
+ `Sent, but the page reported nothing within ${options.timeoutSeconds}s. The action may still be running, or the store is not live on the page.`
501
+ );
502
+ return finish(EXIT.noOutcome);
503
+ }
504
+ io.out(`${formatEvent(event)}
505
+ `);
506
+ return finish(event.kind === "action" && event.result && "error" in event.result ? EXIT.failed : EXIT.ok);
507
+ };
508
+ const start = async (panel) => {
509
+ const { stores } = await panel.request({ type: "GET_STORES" }, ["AVAILABLE_STORES"]);
510
+ if (options.list) {
511
+ io.out(`${formatStoreList(stores)}
512
+ `);
513
+ return finish(EXIT.ok);
514
+ }
515
+ let selectors;
516
+ if (options.targets.length) {
517
+ if (options.targets.includes(ALL_STORES)) {
518
+ io.err(ALL_WARNING);
519
+ selectors = ALL_STORES;
520
+ } else {
521
+ const resolved = resolveTargets(options.targets, stores);
522
+ if (resolved.unknown.length) {
523
+ for (const { target, candidates } of resolved.unknown) {
524
+ io.err(`Store "${target}" was not found. Available: ${candidates.join(", ") || "(none)"}`);
525
+ }
526
+ return finish(EXIT.failed);
527
+ }
528
+ selectors = resolved.selectors;
529
+ }
530
+ } else if (io.interactive && stores.length) {
531
+ const picked = await io.pick(stores);
532
+ if (!picked) return finish(EXIT.ok);
533
+ if ("all" in picked) {
534
+ io.err(ALL_WARNING);
535
+ selectors = ALL_STORES;
536
+ } else {
537
+ selectors = picked.selectors;
538
+ }
539
+ } else {
540
+ io.out(`${formatStoreList(stores)}
541
+ `);
542
+ io.err('\nPass --store <names> (or --store "*") to stream one or more of these stores.');
543
+ return finish(EXIT.needsTarget);
544
+ }
545
+ const rejected = await subscribe(panel, selectors);
546
+ if (rejected) {
547
+ io.err(`${rejected.reason}: ${rejected.unknown.join(", ")}`);
548
+ return finish(EXIT.failed);
549
+ }
550
+ };
551
+ const restore = async (panel, selectors) => {
552
+ for (let attempt = 1; attempt <= RESUBSCRIBE_ATTEMPTS; attempt++) {
553
+ if (current !== panel) return;
554
+ if (!await subscribe(panel, selectors)) return;
555
+ await wait(RESUBSCRIBE_DELAY_MS);
556
+ }
557
+ io.err("Could not restore the subscription: the stores are not known by DevTools any more.");
558
+ return finish(EXIT.failed);
559
+ };
560
+ server.onPanel((panel) => {
561
+ const isFirst = subscribed === null;
562
+ current = panel;
563
+ io.err(`Connected to the DevTools panel${describePanel(panel.hello) ? ` (${describePanel(panel.hello)})` : ""}.`);
564
+ if (!options.command) panel.onEvent((event) => io.out(`${formatEvent(event)}
565
+
566
+ `));
567
+ panel.onClose(() => {
568
+ if (current !== panel || finished) return;
569
+ current = null;
570
+ io.err("DevTools panel disconnected. Waiting for it to reconnect...");
571
+ });
572
+ if (options.command) {
573
+ if (commandStarted) return;
574
+ commandStarted = true;
575
+ }
576
+ const flow = options.command ? runCommand(panel, options.command) : isFirst ? start(panel) : restore(panel, subscribed);
577
+ flow.catch((error) => {
578
+ io.err(error.message);
579
+ return finish(EXIT.failed);
580
+ });
581
+ });
582
+ io.err(
583
+ `Waiting for the DevTools panel on port ${options.port}. Open DevTools on the app tab and select the react-global-state-hooks panel.`
584
+ );
585
+ });
586
+ };
587
+
588
+ // src/cli/select.ts
589
+ var ALL_ROW = "All stores";
590
+ var renderSelector = (stores, { cursor, marked }, rows) => {
591
+ const rowTitles = [
592
+ ...stores.map((store2, index) => `${marked.has(index) ? "\u25C9" : " "} ${storeTitle(store2)}`),
593
+ ` ${ALL_ROW}`
594
+ ];
595
+ const chrome = 4;
596
+ const visible = rows ? Math.max(3, Math.min(rowTitles.length, Math.floor((rows - chrome) / 2))) : rowTitles.length;
597
+ const start = Math.min(Math.max(0, cursor - Math.floor(visible / 2)), rowTitles.length - visible);
598
+ const lines = ["Select a store (\u2191/\u2193 move, space mark several, enter confirm, q quit):", ""];
599
+ for (let index = start; index < start + visible; index++) {
600
+ lines.push(`${index === cursor ? "\u276F" : " "} ${rowTitles[index]}`);
601
+ }
602
+ lines.push("");
603
+ const store = stores[cursor];
604
+ if (!store) {
605
+ lines.push("Listen to every store. This is slower and noisier than picking specific stores.");
606
+ return lines;
607
+ }
608
+ const preview = [store.name ?? "unnamed"];
609
+ if (store.location) preview.push(store.location);
610
+ preview.push("", "State:", ...store.preview.split("\n"));
611
+ if (store.actions.length) preview.push("", "Actions:", ...store.actions);
612
+ const room = rows ? Math.max(0, rows - chrome - visible) : preview.length;
613
+ if (preview.length > room) return [...lines, ...preview.slice(0, Math.max(0, room - 1)), "\u2026"];
614
+ return [...lines, ...preview];
615
+ };
616
+ var KEYS = {
617
+ up: "\x1B[A",
618
+ down: "\x1B[B",
619
+ ctrlC: "",
620
+ enter: "\r",
621
+ space: " "
622
+ };
623
+ var pickStores = (stores) => new Promise((resolve) => {
624
+ const { stdin, stdout } = process;
625
+ const rowCount = stores.length + 1;
626
+ const view = { cursor: 0, marked: /* @__PURE__ */ new Set() };
627
+ const draw = () => {
628
+ const lines = renderSelector(stores, view, stdout.rows);
629
+ stdout.write(`\x1B[H${lines.map((line) => `\x1B[2K${line}`).join("\n")}\x1B[J`);
630
+ };
631
+ const finish = (selection) => {
632
+ stdin.off("data", onData);
633
+ stdin.setRawMode(false);
634
+ stdin.pause();
635
+ stdout.write("\x1B[?25h\x1B[?1049l");
636
+ resolve(selection);
637
+ };
638
+ const onData = (chunk) => {
639
+ const key = chunk.toString();
640
+ if (key === KEYS.ctrlC || key === "q" || key === "\x1B") return finish(null);
641
+ if (key === KEYS.up || key === "k") view.cursor = (view.cursor + rowCount - 1) % rowCount;
642
+ else if (key === KEYS.down || key === "j") view.cursor = (view.cursor + 1) % rowCount;
643
+ else if (key === KEYS.space && view.cursor < stores.length) {
644
+ if (!view.marked.delete(view.cursor)) view.marked.add(view.cursor);
645
+ } else if (key === KEYS.enter) {
646
+ if (view.cursor === stores.length) return finish({ all: true });
647
+ const picked = view.marked.size ? [...view.marked] : [view.cursor];
648
+ return finish({ selectors: picked.map((index) => stores[index].selector) });
649
+ }
650
+ draw();
651
+ };
652
+ stdout.write("\x1B[?1049h\x1B[?25l");
653
+ stdin.setRawMode(true);
654
+ stdin.resume();
655
+ stdin.on("data", onData);
656
+ draw();
657
+ });
658
+
659
+ // src/cli/main.ts
660
+ var main = async () => {
661
+ let options;
662
+ try {
663
+ options = parseArgs(process.argv.slice(2));
664
+ } catch (error) {
665
+ if (!(error instanceof UsageError)) throw error;
666
+ process.stderr.write(`
667
+
668
+
669
+ ${error.message}
670
+
671
+ ${HELP}
672
+ `);
673
+ return EXIT.failed;
674
+ }
675
+ if (options.help) {
676
+ process.stdout.write(`
677
+
678
+
679
+ ${HELP}
680
+ `);
681
+ return EXIT.ok;
682
+ }
683
+ const controller = new AbortController();
684
+ process.once("SIGINT", () => controller.abort());
685
+ process.once("SIGTERM", () => controller.abort());
686
+ process.stdout.on("error", () => controller.abort());
687
+ return run(
688
+ options,
689
+ {
690
+ out: (text) => void process.stdout.write(text),
691
+ err: (text) => void process.stderr.write(`${text}
692
+ `),
693
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
694
+ pick: pickStores
695
+ },
696
+ controller.signal
697
+ );
698
+ };
699
+ main().then(
700
+ (code) => process.exit(code),
701
+ (error) => {
702
+ process.stderr.write(`${error instanceof Error ? error.stack : String(error)}
703
+ `);
704
+ process.exit(EXIT.failed);
705
+ }
706
+ );