comm-scope 0.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.
- package/LICENSE +21 -0
- package/README.md +24 -0
- package/dist/index.js +603 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 comm-scope AndyFree96
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# comm-scope
|
|
2
|
+
|
|
3
|
+
Serial / TCP / UDP traffic monitor for developers — watch, filter, record, replay and search byte-level communication.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g comm-scope
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
comm-scope monitor udp-listen:9999 # live monitor
|
|
15
|
+
comm-scope record serial:COM3:115200 --out session.jsonl
|
|
16
|
+
comm-scope view session.jsonl # offline replay
|
|
17
|
+
comm-scope search session.jsonl --string "error" -C 2
|
|
18
|
+
comm-scope replay session.jsonl --to serial:COM3
|
|
19
|
+
comm-scope list-serial
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Full documentation
|
|
23
|
+
|
|
24
|
+
See the [project README](https://github.com/AndyFree96/comm-scope) for the complete option reference, transport spec syntax, examples, recording format and architecture.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/monitor.ts
|
|
7
|
+
import chalk3 from "chalk";
|
|
8
|
+
import { parseSpec, createTransport, Session, Analyzer, toDesc as toDesc2 } from "@anthonyfree96/core";
|
|
9
|
+
|
|
10
|
+
// src/render/stream.ts
|
|
11
|
+
import chalk2 from "chalk";
|
|
12
|
+
|
|
13
|
+
// src/render/line.ts
|
|
14
|
+
import chalk from "chalk";
|
|
15
|
+
import { toHex } from "@anthonyfree96/core";
|
|
16
|
+
function fmtTime(ts) {
|
|
17
|
+
const d = new Date(ts);
|
|
18
|
+
const p = (n, w = 2) => String(n).padStart(w, "0");
|
|
19
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}`;
|
|
20
|
+
}
|
|
21
|
+
function fmtAscii(buf) {
|
|
22
|
+
let out = "";
|
|
23
|
+
for (let i = 0; i < buf.length; i++) {
|
|
24
|
+
const b = buf[i];
|
|
25
|
+
if (b === 10) out += "\\n";
|
|
26
|
+
else if (b === 13) out += "\\r";
|
|
27
|
+
else if (b === 9) out += "\\t";
|
|
28
|
+
else if (b >= 32 && b <= 126) out += String.fromCharCode(b);
|
|
29
|
+
else out += ".";
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function dir(dir2) {
|
|
34
|
+
return dir2 === "rx" ? chalk.green("rx") : chalk.yellow("tx");
|
|
35
|
+
}
|
|
36
|
+
function peer(e) {
|
|
37
|
+
if (e.transport.kind === "tcp-server" || e.transport.kind === "udp-listen") {
|
|
38
|
+
return chalk.dim(`[${e.transport.id}] `);
|
|
39
|
+
}
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
function formatEventLine(e, opts) {
|
|
43
|
+
const ts = opts.timestamp ? chalk.gray(fmtTime(e.ts)) + " " : "";
|
|
44
|
+
const body = opts.mode === "hex" ? chalk.white(toHex(e.data)) + chalk.gray(" | ") + chalk.dim(fmtAscii(e.data)) + chalk.gray(" |") : chalk.white(fmtAscii(e.data));
|
|
45
|
+
return `${ts}${dir(e.dir)} ${peer(e)}${body}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/render/stream.ts
|
|
49
|
+
var StreamRenderer = class {
|
|
50
|
+
constructor(opts, out = process.stdout) {
|
|
51
|
+
this.out = out;
|
|
52
|
+
this.opts = opts;
|
|
53
|
+
if (!opts.color) chalk2.level = 0;
|
|
54
|
+
}
|
|
55
|
+
out;
|
|
56
|
+
opts;
|
|
57
|
+
onEvent(e) {
|
|
58
|
+
if (this.opts.mode === "raw") {
|
|
59
|
+
this.out.write(e.data);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this.out.write(formatEventLine(e, this.opts) + "\n");
|
|
63
|
+
}
|
|
64
|
+
onClose() {
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/render/tui.ts
|
|
69
|
+
import blessed from "neo-blessed";
|
|
70
|
+
import { toHex as toHex2 } from "@anthonyfree96/core";
|
|
71
|
+
var TuiRenderer = class {
|
|
72
|
+
constructor(opts) {
|
|
73
|
+
this.opts = opts;
|
|
74
|
+
this.screen = blessed.screen({ smartCSR: true, title: "comm-scope", fullUnicode: true });
|
|
75
|
+
blessed.box({
|
|
76
|
+
parent: this.screen,
|
|
77
|
+
top: 0,
|
|
78
|
+
left: 0,
|
|
79
|
+
width: "100%",
|
|
80
|
+
height: 1,
|
|
81
|
+
content: ` comm-scope \u2014 ${opts.desc} (q to quit)`,
|
|
82
|
+
style: { bg: "blue", fg: "white" }
|
|
83
|
+
});
|
|
84
|
+
this.stats = blessed.box({
|
|
85
|
+
parent: this.screen,
|
|
86
|
+
top: 1,
|
|
87
|
+
left: 0,
|
|
88
|
+
width: "100%",
|
|
89
|
+
height: 3,
|
|
90
|
+
content: this.statsText(),
|
|
91
|
+
border: { type: "line" },
|
|
92
|
+
style: { fg: "cyan" }
|
|
93
|
+
});
|
|
94
|
+
this.log = blessed.log({
|
|
95
|
+
parent: this.screen,
|
|
96
|
+
top: 4,
|
|
97
|
+
left: 0,
|
|
98
|
+
width: "100%",
|
|
99
|
+
height: "100%-4",
|
|
100
|
+
border: { type: "line" },
|
|
101
|
+
scrollback: 1e4,
|
|
102
|
+
mouse: true,
|
|
103
|
+
keys: true,
|
|
104
|
+
vi: true
|
|
105
|
+
});
|
|
106
|
+
this.screen.key(["q", "C-c"], () => {
|
|
107
|
+
this.screen.destroy();
|
|
108
|
+
this.opts.onExit();
|
|
109
|
+
});
|
|
110
|
+
this.screen.render();
|
|
111
|
+
}
|
|
112
|
+
opts;
|
|
113
|
+
screen;
|
|
114
|
+
log;
|
|
115
|
+
stats;
|
|
116
|
+
rxBytes = 0;
|
|
117
|
+
txBytes = 0;
|
|
118
|
+
rxPackets = 0;
|
|
119
|
+
txPackets = 0;
|
|
120
|
+
startTs = 0;
|
|
121
|
+
onEvent(e) {
|
|
122
|
+
if (this.startTs === 0) this.startTs = e.ts;
|
|
123
|
+
if (e.dir === "rx") {
|
|
124
|
+
this.rxBytes += e.data.length;
|
|
125
|
+
this.rxPackets++;
|
|
126
|
+
} else {
|
|
127
|
+
this.txBytes += e.data.length;
|
|
128
|
+
this.txPackets++;
|
|
129
|
+
}
|
|
130
|
+
const peer2 = e.transport.kind === "tcp-server" || e.transport.kind === "udp-listen" ? `[${e.transport.id}] ` : "";
|
|
131
|
+
this.log.log(
|
|
132
|
+
`${fmtTime(e.ts)} ${e.dir} ${peer2}${toHex2(e.data)} | ${fmtAscii(e.data)}`
|
|
133
|
+
);
|
|
134
|
+
this.stats.setContent(this.statsText());
|
|
135
|
+
this.screen.render();
|
|
136
|
+
}
|
|
137
|
+
/** Append a status/error line to the log. */
|
|
138
|
+
logLine(msg) {
|
|
139
|
+
this.log.log(msg);
|
|
140
|
+
this.screen.render();
|
|
141
|
+
}
|
|
142
|
+
onClose() {
|
|
143
|
+
}
|
|
144
|
+
statsText() {
|
|
145
|
+
const durationMs = this.startTs === 0 ? 0 : Date.now() - this.startTs;
|
|
146
|
+
const secs = durationMs / 1e3 || 1;
|
|
147
|
+
const rxRate = this.rxBytes / secs;
|
|
148
|
+
const txRate = this.txBytes / secs;
|
|
149
|
+
return ` rx ${this.rxPackets} pkts / ${this.rxBytes} B (${rxRate.toFixed(0)} B/s) tx ${this.txPackets} pkts / ${this.txBytes} B (${txRate.toFixed(0)} B/s)`;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// src/args.ts
|
|
154
|
+
import { Matcher, hexToBuffer } from "@anthonyfree96/core";
|
|
155
|
+
function buildMatcher(f) {
|
|
156
|
+
if (f.dir && f.dir !== "rx" && f.dir !== "tx") {
|
|
157
|
+
throw new Error(`--dir must be "rx" or "tx", got "${f.dir}"`);
|
|
158
|
+
}
|
|
159
|
+
if (!f.string && !f.hex && !f.regex && !f.dir) return void 0;
|
|
160
|
+
return new Matcher({
|
|
161
|
+
dir: f.dir,
|
|
162
|
+
hex: f.hex ? hexToBuffer(f.hex) : void 0,
|
|
163
|
+
string: f.string ? Buffer.from(f.string, "utf8") : void 0,
|
|
164
|
+
regex: f.regex ? new RegExp(f.regex) : void 0
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/recording.ts
|
|
169
|
+
import fs from "fs";
|
|
170
|
+
import { JsonlRecorder, toDesc, SCHEMA_VERSION } from "@anthonyfree96/core";
|
|
171
|
+
function makeRecorder(spec, outFile) {
|
|
172
|
+
const desc = toDesc(spec);
|
|
173
|
+
return new JsonlRecorder(fs.createWriteStream(outFile), {
|
|
174
|
+
version: SCHEMA_VERSION,
|
|
175
|
+
kind: spec.kind,
|
|
176
|
+
id: desc,
|
|
177
|
+
desc,
|
|
178
|
+
started: Date.now()
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/render/stats.ts
|
|
183
|
+
function formatSummary(s) {
|
|
184
|
+
const totalPackets = s.rxPackets + s.txPackets;
|
|
185
|
+
const totalBytes = s.rxBytes + s.txBytes;
|
|
186
|
+
return [
|
|
187
|
+
`duration: ${s.durationMs} ms`,
|
|
188
|
+
`rx: ${s.rxPackets} packets, ${s.rxBytes} bytes (${s.rxRate.toFixed(1)} B/s)`,
|
|
189
|
+
`tx: ${s.txPackets} packets, ${s.txBytes} bytes (${s.txRate.toFixed(1)} B/s)`,
|
|
190
|
+
`total: ${totalPackets} packets, ${totalBytes} bytes`
|
|
191
|
+
].join("\n");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/stdio.ts
|
|
195
|
+
function flushStdio() {
|
|
196
|
+
return new Promise((resolve) => {
|
|
197
|
+
let pending = 2;
|
|
198
|
+
const done = () => {
|
|
199
|
+
if (--pending <= 0) resolve();
|
|
200
|
+
};
|
|
201
|
+
process.stdout.write("", done);
|
|
202
|
+
process.stderr.write("", done);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/commands/monitor.ts
|
|
207
|
+
function registerMonitor(program2) {
|
|
208
|
+
program2.command("monitor").argument(
|
|
209
|
+
"<spec>",
|
|
210
|
+
"transport spec: serial:PORT[:BAUD] | tcp:HOST:PORT | tcp-listen:PORT | udp:HOST:PORT | udp-listen:PORT"
|
|
211
|
+
).option("--format <mode>", "hex | ascii | raw", "hex").option("--no-timestamp", "omit timestamps").option("--no-color", "disable ANSI colors").option("--string <s>", "only show events containing this UTF-8 substring").option("--hex <h>", "only show events containing this hex byte sequence").option("--regex <re>", "only show events whose UTF-8 text matches this regex").option("--dir <dir>", "only show rx or tx").option("--record <file>", "also record traffic to file").option("--stats", "print traffic statistics on exit").option("--timeout <s>", "stop after N seconds").option("--tui", "interactive dashboard").action(async (spec, opts) => {
|
|
212
|
+
let specParsed;
|
|
213
|
+
try {
|
|
214
|
+
specParsed = parseSpec(spec);
|
|
215
|
+
} catch (err) {
|
|
216
|
+
process.stderr.write(chalk3.red(`error: ${err.message}
|
|
217
|
+
`));
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
let filter;
|
|
221
|
+
try {
|
|
222
|
+
filter = buildMatcher({
|
|
223
|
+
string: opts.string,
|
|
224
|
+
hex: opts.hex,
|
|
225
|
+
regex: opts.regex,
|
|
226
|
+
dir: opts.dir
|
|
227
|
+
});
|
|
228
|
+
} catch (err) {
|
|
229
|
+
process.stderr.write(chalk3.red(`error: ${err.message}
|
|
230
|
+
`));
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
const transport = createTransport(specParsed);
|
|
234
|
+
const analyzer = opts.stats ? new Analyzer() : void 0;
|
|
235
|
+
let tui;
|
|
236
|
+
let renderer;
|
|
237
|
+
if (opts.tui) {
|
|
238
|
+
tui = new TuiRenderer({ desc: toDesc2(specParsed), onExit: () => void shutdown() });
|
|
239
|
+
renderer = tui;
|
|
240
|
+
} else {
|
|
241
|
+
renderer = new StreamRenderer({
|
|
242
|
+
mode: opts.format,
|
|
243
|
+
timestamp: Boolean(opts.timestamp),
|
|
244
|
+
color: Boolean(opts.color)
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
const sinks = [renderer];
|
|
248
|
+
if (opts.record) sinks.push(makeRecorder(specParsed, opts.record));
|
|
249
|
+
if (analyzer) sinks.push(analyzer);
|
|
250
|
+
const session = new Session(transport, { filter, sinks });
|
|
251
|
+
transport.on("open", ({ meta }) => {
|
|
252
|
+
if (tui) tui.logLine(`monitoring ${meta.desc}`);
|
|
253
|
+
else process.stderr.write(chalk3.dim(`# monitoring ${meta.desc}
|
|
254
|
+
`));
|
|
255
|
+
});
|
|
256
|
+
transport.on("error", ({ error }) => {
|
|
257
|
+
if (tui) tui.logLine(`error: ${error.message}`);
|
|
258
|
+
else process.stderr.write(chalk3.red(`error: ${error.message}
|
|
259
|
+
`));
|
|
260
|
+
});
|
|
261
|
+
let shuttingDown = false;
|
|
262
|
+
transport.on("close", () => {
|
|
263
|
+
if (shuttingDown) return;
|
|
264
|
+
if (tui) tui.logLine("closed");
|
|
265
|
+
else process.stderr.write(chalk3.dim("# closed\n"));
|
|
266
|
+
process.exit(0);
|
|
267
|
+
});
|
|
268
|
+
async function shutdown() {
|
|
269
|
+
if (shuttingDown) return;
|
|
270
|
+
shuttingDown = true;
|
|
271
|
+
await session.stop();
|
|
272
|
+
if (analyzer) {
|
|
273
|
+
const summary = formatSummary(analyzer.summarize());
|
|
274
|
+
if (tui) tui.logLine(summary);
|
|
275
|
+
else process.stderr.write(chalk3.dim(summary + "\n"));
|
|
276
|
+
}
|
|
277
|
+
await flushStdio();
|
|
278
|
+
process.exit(0);
|
|
279
|
+
}
|
|
280
|
+
process.on("SIGINT", shutdown);
|
|
281
|
+
process.on("SIGTERM", shutdown);
|
|
282
|
+
if (opts.timeout) {
|
|
283
|
+
const secs = Number(opts.timeout);
|
|
284
|
+
if (!Number.isFinite(secs) || secs <= 0) {
|
|
285
|
+
process.stderr.write(chalk3.red(`error: invalid --timeout "${opts.timeout}"
|
|
286
|
+
`));
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
setTimeout(shutdown, secs * 1e3);
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
await session.run();
|
|
293
|
+
} catch (err) {
|
|
294
|
+
process.stderr.write(chalk3.red(`error: ${err.message}
|
|
295
|
+
`));
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/commands/list-serial.ts
|
|
302
|
+
import chalk4 from "chalk";
|
|
303
|
+
import { listSerialPorts } from "@anthonyfree96/core";
|
|
304
|
+
function registerListSerial(program2) {
|
|
305
|
+
program2.command("list-serial").description("list attached serial ports").action(async () => {
|
|
306
|
+
try {
|
|
307
|
+
const ports = await listSerialPorts();
|
|
308
|
+
if (ports.length === 0) {
|
|
309
|
+
process.stdout.write("no serial ports found\n");
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
for (const p of ports) {
|
|
313
|
+
process.stdout.write(`${p.path}${p.manufacturer ? ` ${p.manufacturer}` : ""}
|
|
314
|
+
`);
|
|
315
|
+
}
|
|
316
|
+
} catch (err) {
|
|
317
|
+
process.stderr.write(chalk4.red(`error: ${err.message}
|
|
318
|
+
`));
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/commands/record.ts
|
|
325
|
+
import chalk5 from "chalk";
|
|
326
|
+
import { parseSpec as parseSpec2, createTransport as createTransport2, Session as Session2, Analyzer as Analyzer2 } from "@anthonyfree96/core";
|
|
327
|
+
function registerRecord(program2) {
|
|
328
|
+
program2.command("record").argument("<spec>", "transport spec: serial:PORT[:BAUD] | tcp:HOST:PORT | tcp-listen:PORT | udp:HOST:PORT | udp-listen:PORT").requiredOption("--out <file>", "recording output file").option("--string <s>", "only record events containing this UTF-8 substring").option("--hex <h>", "only record events containing this hex byte sequence").option("--regex <re>", "only record events whose UTF-8 text matches this regex").option("--dir <dir>", "only record rx or tx").option("--stats", "print traffic statistics on exit").action(async (spec, opts) => {
|
|
329
|
+
let specParsed;
|
|
330
|
+
try {
|
|
331
|
+
specParsed = parseSpec2(spec);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
process.stderr.write(chalk5.red(`error: ${err.message}
|
|
334
|
+
`));
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
let filter;
|
|
338
|
+
try {
|
|
339
|
+
filter = buildMatcher({
|
|
340
|
+
string: opts.string,
|
|
341
|
+
hex: opts.hex,
|
|
342
|
+
regex: opts.regex,
|
|
343
|
+
dir: opts.dir
|
|
344
|
+
});
|
|
345
|
+
} catch (err) {
|
|
346
|
+
process.stderr.write(chalk5.red(`error: ${err.message}
|
|
347
|
+
`));
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
const transport = createTransport2(specParsed);
|
|
351
|
+
const recorder = makeRecorder(specParsed, opts.out);
|
|
352
|
+
let count = 0;
|
|
353
|
+
const counter = {
|
|
354
|
+
onEvent: () => {
|
|
355
|
+
count++;
|
|
356
|
+
},
|
|
357
|
+
onClose: () => {
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
const analyzer = opts.stats ? new Analyzer2() : void 0;
|
|
361
|
+
const sinks = [recorder, counter];
|
|
362
|
+
if (analyzer) sinks.push(analyzer);
|
|
363
|
+
const session = new Session2(transport, { filter, sinks });
|
|
364
|
+
transport.on(
|
|
365
|
+
"open",
|
|
366
|
+
({ meta }) => process.stderr.write(chalk5.dim(`# recording ${meta.desc} -> ${opts.out}
|
|
367
|
+
`))
|
|
368
|
+
);
|
|
369
|
+
transport.on(
|
|
370
|
+
"error",
|
|
371
|
+
({ error }) => process.stderr.write(chalk5.red(`error: ${error.message}
|
|
372
|
+
`))
|
|
373
|
+
);
|
|
374
|
+
let shuttingDown = false;
|
|
375
|
+
const shutdown = async () => {
|
|
376
|
+
if (shuttingDown) return;
|
|
377
|
+
shuttingDown = true;
|
|
378
|
+
await session.stop();
|
|
379
|
+
process.stderr.write(chalk5.dim(`# recorded ${count} events to ${opts.out}
|
|
380
|
+
`));
|
|
381
|
+
if (analyzer) process.stderr.write(chalk5.dim(formatSummary(analyzer.summarize()) + "\n"));
|
|
382
|
+
await flushStdio();
|
|
383
|
+
process.exit(0);
|
|
384
|
+
};
|
|
385
|
+
process.on("SIGINT", shutdown);
|
|
386
|
+
process.on("SIGTERM", shutdown);
|
|
387
|
+
try {
|
|
388
|
+
await session.run();
|
|
389
|
+
} catch (err) {
|
|
390
|
+
process.stderr.write(chalk5.red(`error: ${err.message}
|
|
391
|
+
`));
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/commands/view.ts
|
|
398
|
+
import chalk6 from "chalk";
|
|
399
|
+
import { FileSource, Session as Session3 } from "@anthonyfree96/core";
|
|
400
|
+
function registerView(program2) {
|
|
401
|
+
program2.command("view").argument("<file>", "recording file (JSON Lines)").option("--speed <n>", "replay speed multiplier (0 = as fast as possible)", "1").option("--format <mode>", "hex | ascii | raw", "hex").option("--no-timestamp", "omit timestamps").option("--no-color", "disable ANSI colors").option("--dir <dir>", "only show rx or tx").option("--tui", "interactive dashboard").action(async (file, opts) => {
|
|
402
|
+
const dir2 = opts.dir;
|
|
403
|
+
if (dir2 && dir2 !== "rx" && dir2 !== "tx") {
|
|
404
|
+
process.stderr.write(chalk6.red(`error: --dir must be "rx" or "tx", got "${dir2}"
|
|
405
|
+
`));
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
const speed = Number(opts.speed);
|
|
409
|
+
if (Number.isNaN(speed) || speed < 0) {
|
|
410
|
+
process.stderr.write(chalk6.red(`error: invalid --speed "${opts.speed}"
|
|
411
|
+
`));
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
const source = new FileSource(file, { speed, dir: dir2 });
|
|
415
|
+
let tui;
|
|
416
|
+
let renderer;
|
|
417
|
+
if (opts.tui) {
|
|
418
|
+
tui = new TuiRenderer({ desc: `file: ${file}`, onExit: () => void shutdown() });
|
|
419
|
+
renderer = tui;
|
|
420
|
+
} else {
|
|
421
|
+
renderer = new StreamRenderer({
|
|
422
|
+
mode: opts.format,
|
|
423
|
+
timestamp: Boolean(opts.timestamp),
|
|
424
|
+
color: Boolean(opts.color)
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
const session = new Session3(source, { sinks: [renderer] });
|
|
428
|
+
source.on("open", ({ meta }) => {
|
|
429
|
+
if (tui) tui.logLine(`replaying ${meta.desc}`);
|
|
430
|
+
else process.stderr.write(chalk6.dim(`# replaying ${meta.desc}
|
|
431
|
+
`));
|
|
432
|
+
});
|
|
433
|
+
source.on("error", ({ error }) => {
|
|
434
|
+
if (tui) tui.logLine(`error: ${error.message}`);
|
|
435
|
+
else process.stderr.write(chalk6.red(`error: ${error.message}
|
|
436
|
+
`));
|
|
437
|
+
});
|
|
438
|
+
async function shutdown() {
|
|
439
|
+
await session.stop();
|
|
440
|
+
await flushStdio();
|
|
441
|
+
process.exit(0);
|
|
442
|
+
}
|
|
443
|
+
process.on("SIGINT", shutdown);
|
|
444
|
+
process.on("SIGTERM", shutdown);
|
|
445
|
+
try {
|
|
446
|
+
await session.run();
|
|
447
|
+
} catch (err) {
|
|
448
|
+
process.stderr.write(chalk6.red(`error: ${err.message}
|
|
449
|
+
`));
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/commands/search.ts
|
|
456
|
+
import fs2 from "fs";
|
|
457
|
+
import readline from "readline";
|
|
458
|
+
import chalk7 from "chalk";
|
|
459
|
+
import { decodeHeader, decodeEvent } from "@anthonyfree96/core";
|
|
460
|
+
function registerSearch(program2) {
|
|
461
|
+
program2.command("search").argument("<file>", "recording file (JSON Lines)").option("--string <s>", "match events containing this UTF-8 substring").option("--hex <h>", "match events containing this hex byte sequence").option("--regex <re>", "match events whose UTF-8 text matches this regex").option("--dir <dir>", "only match rx or tx").option("-C, --context <n>", "context events to show around each match", "2").option("--no-color", "disable ANSI colors").option("--no-timestamp", "omit timestamps").action(async (file, opts) => {
|
|
462
|
+
const matcher = buildMatcher({
|
|
463
|
+
string: opts.string,
|
|
464
|
+
hex: opts.hex,
|
|
465
|
+
regex: opts.regex,
|
|
466
|
+
dir: opts.dir
|
|
467
|
+
});
|
|
468
|
+
if (!matcher) {
|
|
469
|
+
process.stderr.write(
|
|
470
|
+
chalk7.red("error: provide a filter: --string, --hex, --regex or --dir\n")
|
|
471
|
+
);
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
const events = [];
|
|
475
|
+
const rl = readline.createInterface({
|
|
476
|
+
input: fs2.createReadStream(file),
|
|
477
|
+
crlfDelay: Infinity
|
|
478
|
+
});
|
|
479
|
+
let headerSeen = false;
|
|
480
|
+
let lineNo = 0;
|
|
481
|
+
for await (const line of rl) {
|
|
482
|
+
lineNo++;
|
|
483
|
+
if (!headerSeen) {
|
|
484
|
+
decodeHeader(line);
|
|
485
|
+
headerSeen = true;
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
events.push({ line: lineNo, e: decodeEvent(line) });
|
|
489
|
+
}
|
|
490
|
+
const matchIdx = /* @__PURE__ */ new Set();
|
|
491
|
+
events.forEach((item, i) => {
|
|
492
|
+
if (matcher.matches(item.e)) matchIdx.add(i);
|
|
493
|
+
});
|
|
494
|
+
if (matchIdx.size === 0) {
|
|
495
|
+
process.stdout.write("no matches\n");
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const ctx = Math.max(0, Number(opts.context) || 0);
|
|
499
|
+
const color = Boolean(opts.color);
|
|
500
|
+
chalk7.level = color ? 1 : 0;
|
|
501
|
+
const lineOpts = {
|
|
502
|
+
mode: "hex",
|
|
503
|
+
timestamp: Boolean(opts.timestamp),
|
|
504
|
+
color
|
|
505
|
+
};
|
|
506
|
+
const inWindow = new Array(events.length).fill(false);
|
|
507
|
+
for (const m of matchIdx) {
|
|
508
|
+
const from = Math.max(0, m - ctx);
|
|
509
|
+
const to = Math.min(events.length - 1, m + ctx);
|
|
510
|
+
for (let i = from; i <= to; i++) inWindow[i] = true;
|
|
511
|
+
}
|
|
512
|
+
process.stderr.write(chalk7.dim(`# ${matchIdx.size} matches
|
|
513
|
+
`));
|
|
514
|
+
let prev = -1;
|
|
515
|
+
for (let i = 0; i < events.length; i++) {
|
|
516
|
+
if (!inWindow[i]) continue;
|
|
517
|
+
if (prev !== -1 && i > prev + 1) process.stdout.write(chalk7.dim("--\n"));
|
|
518
|
+
const item = events[i];
|
|
519
|
+
const isMatch = matchIdx.has(i);
|
|
520
|
+
const marker = isMatch ? chalk7.cyan("* ") : " ";
|
|
521
|
+
const lineNum = chalk7.gray(String(item.line).padStart(5)) + " ";
|
|
522
|
+
process.stdout.write(marker + lineNum + formatEventLine(item.e, lineOpts) + "\n");
|
|
523
|
+
prev = i;
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/commands/replay.ts
|
|
529
|
+
import chalk8 from "chalk";
|
|
530
|
+
import { parseSpec as parseSpec3, createTransport as createTransport3, replayFileToTransport } from "@anthonyfree96/core";
|
|
531
|
+
function registerReplay(program2) {
|
|
532
|
+
program2.command("replay").argument("<file>", "recording file (JSON Lines)").requiredOption("--to <spec>", "target transport spec to send to").option("--speed <n>", "replay speed multiplier (0 = as fast as possible)", "1").option("--loop", "re-send continuously until interrupted").option("--dir <dir>", "only replay rx or tx (default: all, chronological)").action(async (file, opts) => {
|
|
533
|
+
const dir2 = opts.dir;
|
|
534
|
+
if (dir2 && dir2 !== "rx" && dir2 !== "tx") {
|
|
535
|
+
process.stderr.write(chalk8.red(`error: --dir must be "rx" or "tx", got "${dir2}"
|
|
536
|
+
`));
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
const speed = Number(opts.speed);
|
|
540
|
+
if (Number.isNaN(speed) || speed < 0) {
|
|
541
|
+
process.stderr.write(chalk8.red(`error: invalid --speed "${opts.speed}"
|
|
542
|
+
`));
|
|
543
|
+
process.exit(1);
|
|
544
|
+
}
|
|
545
|
+
let specParsed;
|
|
546
|
+
try {
|
|
547
|
+
specParsed = parseSpec3(opts.to);
|
|
548
|
+
} catch (err) {
|
|
549
|
+
process.stderr.write(chalk8.red(`error: ${err.message}
|
|
550
|
+
`));
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
const target = createTransport3(specParsed);
|
|
554
|
+
target.on(
|
|
555
|
+
"open",
|
|
556
|
+
({ meta }) => process.stderr.write(chalk8.dim(`# replaying ${file} -> ${meta.desc}
|
|
557
|
+
`))
|
|
558
|
+
);
|
|
559
|
+
target.on(
|
|
560
|
+
"error",
|
|
561
|
+
({ error }) => process.stderr.write(chalk8.red(`error: ${error.message}
|
|
562
|
+
`))
|
|
563
|
+
);
|
|
564
|
+
let stopping = false;
|
|
565
|
+
const stop = async () => {
|
|
566
|
+
if (stopping) return;
|
|
567
|
+
stopping = true;
|
|
568
|
+
await target.stop();
|
|
569
|
+
await flushStdio();
|
|
570
|
+
process.exit(0);
|
|
571
|
+
};
|
|
572
|
+
process.on("SIGINT", stop);
|
|
573
|
+
process.on("SIGTERM", stop);
|
|
574
|
+
try {
|
|
575
|
+
await target.start();
|
|
576
|
+
const sent = await replayFileToTransport(file, target, {
|
|
577
|
+
speed,
|
|
578
|
+
loop: Boolean(opts.loop),
|
|
579
|
+
dir: dir2
|
|
580
|
+
});
|
|
581
|
+
process.stderr.write(chalk8.dim(`# replayed ${sent} events
|
|
582
|
+
`));
|
|
583
|
+
await stop();
|
|
584
|
+
} catch (err) {
|
|
585
|
+
process.stderr.write(chalk8.red(`error: ${err.message}
|
|
586
|
+
`));
|
|
587
|
+
await flushStdio();
|
|
588
|
+
process.exit(1);
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/index.ts
|
|
594
|
+
var program = new Command();
|
|
595
|
+
program.name("comm-scope").description("Serial / TCP / UDP traffic monitor for developers").version("0.1.0");
|
|
596
|
+
registerMonitor(program);
|
|
597
|
+
registerRecord(program);
|
|
598
|
+
registerView(program);
|
|
599
|
+
registerSearch(program);
|
|
600
|
+
registerReplay(program);
|
|
601
|
+
registerListSerial(program);
|
|
602
|
+
await program.parseAsync(process.argv);
|
|
603
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/commands/monitor.ts","../src/render/stream.ts","../src/render/line.ts","../src/render/tui.ts","../src/args.ts","../src/recording.ts","../src/render/stats.ts","../src/stdio.ts","../src/commands/list-serial.ts","../src/commands/record.ts","../src/commands/view.ts","../src/commands/search.ts","../src/commands/replay.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { registerMonitor } from './commands/monitor.js';\nimport { registerListSerial } from './commands/list-serial.js';\nimport { registerRecord } from './commands/record.js';\nimport { registerView } from './commands/view.js';\nimport { registerSearch } from './commands/search.js';\nimport { registerReplay } from './commands/replay.js';\n\nconst program = new Command();\n\nprogram\n .name('comm-scope')\n .description('Serial / TCP / UDP traffic monitor for developers')\n .version('0.1.0');\n\nregisterMonitor(program);\nregisterRecord(program);\nregisterView(program);\nregisterSearch(program);\nregisterReplay(program);\nregisterListSerial(program);\n\nawait program.parseAsync(process.argv);\n","import chalk from 'chalk';\nimport type { Command } from 'commander';\nimport { parseSpec, createTransport, Session, Analyzer, toDesc } from '@anthonyfree96/core';\nimport type { Direction, Sink } from '@anthonyfree96/core';\nimport { StreamRenderer } from '../render/stream.js';\nimport { TuiRenderer } from '../render/tui.js';\nimport { buildMatcher } from '../args.js';\nimport { makeRecorder } from '../recording.js';\nimport { formatSummary } from '../render/stats.js';\nimport { flushStdio } from '../stdio.js';\n\nexport function registerMonitor(program: Command): void {\n program\n .command('monitor')\n .argument(\n '<spec>',\n 'transport spec: serial:PORT[:BAUD] | tcp:HOST:PORT | tcp-listen:PORT | udp:HOST:PORT | udp-listen:PORT',\n )\n .option('--format <mode>', 'hex | ascii | raw', 'hex')\n .option('--no-timestamp', 'omit timestamps')\n .option('--no-color', 'disable ANSI colors')\n .option('--string <s>', 'only show events containing this UTF-8 substring')\n .option('--hex <h>', 'only show events containing this hex byte sequence')\n .option('--regex <re>', 'only show events whose UTF-8 text matches this regex')\n .option('--dir <dir>', 'only show rx or tx')\n .option('--record <file>', 'also record traffic to file')\n .option('--stats', 'print traffic statistics on exit')\n .option('--timeout <s>', 'stop after N seconds')\n .option('--tui', 'interactive dashboard')\n .action(async (spec: string, opts: Record<string, unknown>) => {\n let specParsed;\n try {\n specParsed = parseSpec(spec);\n } catch (err) {\n process.stderr.write(chalk.red(`error: ${(err as Error).message}\\n`));\n process.exit(1);\n }\n\n let filter;\n try {\n filter = buildMatcher({\n string: opts.string as string | undefined,\n hex: opts.hex as string | undefined,\n regex: opts.regex as string | undefined,\n dir: opts.dir as Direction | undefined,\n });\n } catch (err) {\n process.stderr.write(chalk.red(`error: ${(err as Error).message}\\n`));\n process.exit(1);\n }\n\n const transport = createTransport(specParsed);\n const analyzer = opts.stats ? new Analyzer() : undefined;\n\n let tui: TuiRenderer | undefined;\n let renderer: Sink;\n if (opts.tui) {\n tui = new TuiRenderer({ desc: toDesc(specParsed), onExit: () => void shutdown() });\n renderer = tui;\n } else {\n renderer = new StreamRenderer({\n mode: opts.format as 'hex' | 'ascii' | 'raw',\n timestamp: Boolean(opts.timestamp),\n color: Boolean(opts.color),\n });\n }\n\n const sinks: Sink[] = [renderer];\n if (opts.record) sinks.push(makeRecorder(specParsed, opts.record as string));\n if (analyzer) sinks.push(analyzer);\n const session = new Session(transport, { filter, sinks });\n\n transport.on('open', ({ meta }) => {\n if (tui) tui.logLine(`monitoring ${meta.desc}`);\n else process.stderr.write(chalk.dim(`# monitoring ${meta.desc}\\n`));\n });\n transport.on('error', ({ error }) => {\n if (tui) tui.logLine(`error: ${error.message}`);\n else process.stderr.write(chalk.red(`error: ${error.message}\\n`));\n });\n\n let shuttingDown = false;\n transport.on('close', () => {\n if (shuttingDown) return;\n if (tui) tui.logLine('closed');\n else process.stderr.write(chalk.dim('# closed\\n'));\n process.exit(0);\n });\n\n async function shutdown(): Promise<void> {\n if (shuttingDown) return;\n shuttingDown = true;\n await session.stop();\n if (analyzer) {\n const summary = formatSummary(analyzer.summarize());\n if (tui) tui.logLine(summary);\n else process.stderr.write(chalk.dim(summary + '\\n'));\n }\n await flushStdio();\n process.exit(0);\n }\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n\n if (opts.timeout) {\n const secs = Number(opts.timeout);\n if (!Number.isFinite(secs) || secs <= 0) {\n process.stderr.write(chalk.red(`error: invalid --timeout \"${opts.timeout}\"\\n`));\n process.exit(1);\n }\n setTimeout(shutdown, secs * 1000);\n }\n\n try {\n await session.run();\n } catch (err) {\n process.stderr.write(chalk.red(`error: ${(err as Error).message}\\n`));\n process.exit(1);\n }\n });\n}\n","import chalk from 'chalk';\nimport type { Sink, TrafficEvent } from '@anthonyfree96/core';\nimport { formatEventLine } from './line.js';\nimport type { LineOptions, StreamMode } from './line.js';\n\nexport type { StreamMode } from './line.js';\n\nexport interface StreamOptions {\n mode: StreamMode;\n timestamp: boolean;\n color: boolean;\n}\n\n/**\n * Streaming, line-oriented renderer. Writes one line per event to `out`\n * (stdout), leaving stderr free for banners/status so output stays pipeable.\n */\nexport class StreamRenderer implements Sink {\n private readonly opts: LineOptions;\n\n constructor(\n opts: StreamOptions,\n private readonly out: NodeJS.WriteStream = process.stdout,\n ) {\n this.opts = opts;\n if (!opts.color) chalk.level = 0;\n }\n\n onEvent(e: TrafficEvent): void {\n if (this.opts.mode === 'raw') {\n this.out.write(e.data);\n return;\n }\n this.out.write(formatEventLine(e, this.opts) + '\\n');\n }\n\n onClose(): void {}\n}\n","import chalk from 'chalk';\nimport { toHex } from '@anthonyfree96/core';\nimport type { TrafficEvent } from '@anthonyfree96/core';\n\nexport type StreamMode = 'hex' | 'ascii' | 'raw';\n\nexport interface LineOptions {\n mode: StreamMode;\n timestamp: boolean;\n color: boolean;\n}\n\nexport function fmtTime(ts: number): string {\n const d = new Date(ts);\n const p = (n: number, w = 2) => String(n).padStart(w, '0');\n return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}`;\n}\n\nexport function fmtAscii(buf: Buffer): string {\n let out = '';\n for (let i = 0; i < buf.length; i++) {\n const b = buf[i]!;\n if (b === 0x0a) out += '\\\\n';\n else if (b === 0x0d) out += '\\\\r';\n else if (b === 0x09) out += '\\\\t';\n else if (b >= 0x20 && b <= 0x7e) out += String.fromCharCode(b);\n else out += '.';\n }\n return out;\n}\n\nfunction dir(dir: 'rx' | 'tx'): string {\n return dir === 'rx' ? chalk.green('rx') : chalk.yellow('tx');\n}\n\nfunction peer(e: TrafficEvent): string {\n if (e.transport.kind === 'tcp-server' || e.transport.kind === 'udp-listen') {\n return chalk.dim(`[${e.transport.id}] `);\n }\n return '';\n}\n\n/** Render a single event as one line (hex or ascii). */\nexport function formatEventLine(e: TrafficEvent, opts: LineOptions): string {\n const ts = opts.timestamp ? chalk.gray(fmtTime(e.ts)) + ' ' : '';\n const body =\n opts.mode === 'hex'\n ? chalk.white(toHex(e.data)) + chalk.gray(' | ') + chalk.dim(fmtAscii(e.data)) + chalk.gray(' |')\n : chalk.white(fmtAscii(e.data));\n return `${ts}${dir(e.dir)} ${peer(e)}${body}`;\n}\n","import blessed from 'neo-blessed';\nimport type { BlessedScreen, BlessedWidget } from 'neo-blessed';\nimport { toHex } from '@anthonyfree96/core';\nimport type { Sink, TrafficEvent } from '@anthonyfree96/core';\nimport { fmtTime, fmtAscii } from './line.js';\n\nexport interface TuiOptions {\n desc: string;\n onExit: () => void;\n}\n\n/**\n * Interactive full-screen dashboard built on neo-blessed: a scrolling traffic\n * log plus a live stats strip. Press `q` or `Ctrl-C` to quit (handled here,\n * since blessed takes over the terminal).\n */\nexport class TuiRenderer implements Sink {\n private readonly screen: BlessedScreen;\n private readonly log: BlessedWidget;\n private readonly stats: BlessedWidget;\n\n private rxBytes = 0;\n private txBytes = 0;\n private rxPackets = 0;\n private txPackets = 0;\n private startTs = 0;\n\n constructor(private readonly opts: TuiOptions) {\n this.screen = blessed.screen({ smartCSR: true, title: 'comm-scope', fullUnicode: true });\n\n blessed.box({\n parent: this.screen,\n top: 0,\n left: 0,\n width: '100%',\n height: 1,\n content: ` comm-scope — ${opts.desc} (q to quit)`,\n style: { bg: 'blue', fg: 'white' },\n });\n\n this.stats = blessed.box({\n parent: this.screen,\n top: 1,\n left: 0,\n width: '100%',\n height: 3,\n content: this.statsText(),\n border: { type: 'line' },\n style: { fg: 'cyan' },\n });\n\n this.log = blessed.log({\n parent: this.screen,\n top: 4,\n left: 0,\n width: '100%',\n height: '100%-4',\n border: { type: 'line' },\n scrollback: 10000,\n mouse: true,\n keys: true,\n vi: true,\n });\n\n this.screen.key(['q', 'C-c'], () => {\n this.screen.destroy();\n this.opts.onExit();\n });\n\n this.screen.render();\n }\n\n onEvent(e: TrafficEvent): void {\n if (this.startTs === 0) this.startTs = e.ts;\n if (e.dir === 'rx') {\n this.rxBytes += e.data.length;\n this.rxPackets++;\n } else {\n this.txBytes += e.data.length;\n this.txPackets++;\n }\n const peer =\n e.transport.kind === 'tcp-server' || e.transport.kind === 'udp-listen'\n ? `[${e.transport.id}] `\n : '';\n this.log.log(\n `${fmtTime(e.ts)} ${e.dir} ${peer}${toHex(e.data)} | ${fmtAscii(e.data)}`,\n );\n this.stats.setContent(this.statsText());\n this.screen.render();\n }\n\n /** Append a status/error line to the log. */\n logLine(msg: string): void {\n this.log.log(msg);\n this.screen.render();\n }\n\n onClose(): void {}\n\n private statsText(): string {\n const durationMs = this.startTs === 0 ? 0 : Date.now() - this.startTs;\n const secs = durationMs / 1000 || 1;\n const rxRate = this.rxBytes / secs;\n const txRate = this.txBytes / secs;\n return (\n ` rx ${this.rxPackets} pkts / ${this.rxBytes} B (${rxRate.toFixed(0)} B/s) ` +\n `tx ${this.txPackets} pkts / ${this.txBytes} B (${txRate.toFixed(0)} B/s)`\n );\n }\n}\n","import { Matcher, hexToBuffer } from '@anthonyfree96/core';\nimport type { Direction } from '@anthonyfree96/core';\n\n/** Filter flags shared by `monitor` and `search`. */\nexport interface FilterFlags {\n string?: string;\n hex?: string;\n regex?: string;\n dir?: Direction;\n}\n\n/** Build a {@link Matcher} from parsed flags, or undefined if no filter given. */\nexport function buildMatcher(f: FilterFlags): Matcher | undefined {\n if (f.dir && f.dir !== 'rx' && f.dir !== 'tx') {\n throw new Error(`--dir must be \"rx\" or \"tx\", got \"${f.dir}\"`);\n }\n if (!f.string && !f.hex && !f.regex && !f.dir) return undefined;\n return new Matcher({\n dir: f.dir,\n hex: f.hex ? hexToBuffer(f.hex) : undefined,\n string: f.string ? Buffer.from(f.string, 'utf8') : undefined,\n regex: f.regex ? new RegExp(f.regex) : undefined,\n });\n}\n","import fs from 'node:fs';\nimport { JsonlRecorder, toDesc, SCHEMA_VERSION } from '@anthonyfree96/core';\nimport type { TransportSpec } from '@anthonyfree96/core';\n\n/** Build a recorder wired to `outFile`, with a header derived from the spec. */\nexport function makeRecorder(spec: TransportSpec, outFile: string): JsonlRecorder {\n const desc = toDesc(spec);\n return new JsonlRecorder(fs.createWriteStream(outFile), {\n version: SCHEMA_VERSION,\n kind: spec.kind,\n id: desc,\n desc,\n started: Date.now(),\n });\n}\n","import type { TrafficSummary } from '@anthonyfree96/core';\n\n/** Plain-text summary of a captured session. */\nexport function formatSummary(s: TrafficSummary): string {\n const totalPackets = s.rxPackets + s.txPackets;\n const totalBytes = s.rxBytes + s.txBytes;\n return [\n `duration: ${s.durationMs} ms`,\n `rx: ${s.rxPackets} packets, ${s.rxBytes} bytes (${s.rxRate.toFixed(1)} B/s)`,\n `tx: ${s.txPackets} packets, ${s.txBytes} bytes (${s.txRate.toFixed(1)} B/s)`,\n `total: ${totalPackets} packets, ${totalBytes} bytes`,\n ].join('\\n');\n}\n","/**\n * Flush pending stdout/stderr writes. Needed before `process.exit()` because\n * exit does not flush piped streams, so a summary written just before exit\n * would otherwise be lost.\n */\nexport function flushStdio(): Promise<void> {\n return new Promise((resolve) => {\n let pending = 2;\n const done = () => {\n if (--pending <= 0) resolve();\n };\n process.stdout.write('', done);\n process.stderr.write('', done);\n });\n}\n","import chalk from 'chalk';\nimport type { Command } from 'commander';\nimport { listSerialPorts } from '@anthonyfree96/core';\n\nexport function registerListSerial(program: Command): void {\n program\n .command('list-serial')\n .description('list attached serial ports')\n .action(async () => {\n try {\n const ports = await listSerialPorts();\n if (ports.length === 0) {\n process.stdout.write('no serial ports found\\n');\n return;\n }\n for (const p of ports) {\n process.stdout.write(`${p.path}${p.manufacturer ? `\\t${p.manufacturer}` : ''}\\n`);\n }\n } catch (err) {\n process.stderr.write(chalk.red(`error: ${(err as Error).message}\\n`));\n process.exit(1);\n }\n });\n}\n","import chalk from 'chalk';\nimport type { Command } from 'commander';\nimport { parseSpec, createTransport, Session, Analyzer } from '@anthonyfree96/core';\nimport type { Direction, Sink } from '@anthonyfree96/core';\nimport { buildMatcher } from '../args.js';\nimport { makeRecorder } from '../recording.js';\nimport { formatSummary } from '../render/stats.js';\nimport { flushStdio } from '../stdio.js';\n\nexport function registerRecord(program: Command): void {\n program\n .command('record')\n .argument('<spec>', 'transport spec: serial:PORT[:BAUD] | tcp:HOST:PORT | tcp-listen:PORT | udp:HOST:PORT | udp-listen:PORT')\n .requiredOption('--out <file>', 'recording output file')\n .option('--string <s>', 'only record events containing this UTF-8 substring')\n .option('--hex <h>', 'only record events containing this hex byte sequence')\n .option('--regex <re>', 'only record events whose UTF-8 text matches this regex')\n .option('--dir <dir>', 'only record rx or tx')\n .option('--stats', 'print traffic statistics on exit')\n .action(async (spec: string, opts: Record<string, unknown>) => {\n let specParsed;\n try {\n specParsed = parseSpec(spec);\n } catch (err) {\n process.stderr.write(chalk.red(`error: ${(err as Error).message}\\n`));\n process.exit(1);\n }\n\n let filter;\n try {\n filter = buildMatcher({\n string: opts.string as string | undefined,\n hex: opts.hex as string | undefined,\n regex: opts.regex as string | undefined,\n dir: opts.dir as Direction | undefined,\n });\n } catch (err) {\n process.stderr.write(chalk.red(`error: ${(err as Error).message}\\n`));\n process.exit(1);\n }\n\n const transport = createTransport(specParsed);\n const recorder = makeRecorder(specParsed, opts.out as string);\n\n let count = 0;\n const counter: Sink = {\n onEvent: () => {\n count++;\n },\n onClose: () => {},\n };\n\n const analyzer = opts.stats ? new Analyzer() : undefined;\n const sinks: Sink[] = [recorder, counter];\n if (analyzer) sinks.push(analyzer);\n const session = new Session(transport, { filter, sinks });\n\n transport.on('open', ({ meta }) =>\n process.stderr.write(chalk.dim(`# recording ${meta.desc} -> ${opts.out}\\n`)),\n );\n transport.on('error', ({ error }) =>\n process.stderr.write(chalk.red(`error: ${error.message}\\n`)),\n );\n\n let shuttingDown = false;\n const shutdown = async () => {\n if (shuttingDown) return;\n shuttingDown = true;\n await session.stop();\n process.stderr.write(chalk.dim(`# recorded ${count} events to ${opts.out}\\n`));\n if (analyzer) process.stderr.write(chalk.dim(formatSummary(analyzer.summarize()) + '\\n'));\n await flushStdio();\n process.exit(0);\n };\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n\n try {\n await session.run();\n } catch (err) {\n process.stderr.write(chalk.red(`error: ${(err as Error).message}\\n`));\n process.exit(1);\n }\n });\n}\n","import chalk from 'chalk';\nimport type { Command } from 'commander';\nimport { FileSource, Session } from '@anthonyfree96/core';\nimport type { Direction, Sink } from '@anthonyfree96/core';\nimport { StreamRenderer } from '../render/stream.js';\nimport { TuiRenderer } from '../render/tui.js';\nimport { flushStdio } from '../stdio.js';\n\nexport function registerView(program: Command): void {\n program\n .command('view')\n .argument('<file>', 'recording file (JSON Lines)')\n .option('--speed <n>', 'replay speed multiplier (0 = as fast as possible)', '1')\n .option('--format <mode>', 'hex | ascii | raw', 'hex')\n .option('--no-timestamp', 'omit timestamps')\n .option('--no-color', 'disable ANSI colors')\n .option('--dir <dir>', 'only show rx or tx')\n .option('--tui', 'interactive dashboard')\n .action(async (file: string, opts: Record<string, unknown>) => {\n const dir = opts.dir as Direction | undefined;\n if (dir && dir !== 'rx' && dir !== 'tx') {\n process.stderr.write(chalk.red(`error: --dir must be \"rx\" or \"tx\", got \"${dir}\"\\n`));\n process.exit(1);\n }\n const speed = Number(opts.speed);\n if (Number.isNaN(speed) || speed < 0) {\n process.stderr.write(chalk.red(`error: invalid --speed \"${opts.speed}\"\\n`));\n process.exit(1);\n }\n\n const source = new FileSource(file, { speed, dir });\n\n let tui: TuiRenderer | undefined;\n let renderer: Sink;\n if (opts.tui) {\n tui = new TuiRenderer({ desc: `file: ${file}`, onExit: () => void shutdown() });\n renderer = tui;\n } else {\n renderer = new StreamRenderer({\n mode: opts.format as 'hex' | 'ascii' | 'raw',\n timestamp: Boolean(opts.timestamp),\n color: Boolean(opts.color),\n });\n }\n\n const session = new Session(source, { sinks: [renderer] });\n\n source.on('open', ({ meta }) => {\n if (tui) tui.logLine(`replaying ${meta.desc}`);\n else process.stderr.write(chalk.dim(`# replaying ${meta.desc}\\n`));\n });\n source.on('error', ({ error }) => {\n if (tui) tui.logLine(`error: ${error.message}`);\n else process.stderr.write(chalk.red(`error: ${error.message}\\n`));\n });\n\n async function shutdown(): Promise<void> {\n await session.stop();\n await flushStdio();\n process.exit(0);\n }\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n\n try {\n await session.run();\n } catch (err) {\n process.stderr.write(chalk.red(`error: ${(err as Error).message}\\n`));\n process.exit(1);\n }\n });\n}\n","import fs from 'node:fs';\nimport readline from 'node:readline';\nimport chalk from 'chalk';\nimport type { Command } from 'commander';\nimport { decodeHeader, decodeEvent } from '@anthonyfree96/core';\nimport type { Direction, TrafficEvent } from '@anthonyfree96/core';\nimport { buildMatcher } from '../args.js';\nimport { formatEventLine } from '../render/line.js';\nimport type { LineOptions } from '../render/line.js';\n\ninterface IndexedEvent {\n line: number;\n e: TrafficEvent;\n}\n\nexport function registerSearch(program: Command): void {\n program\n .command('search')\n .argument('<file>', 'recording file (JSON Lines)')\n .option('--string <s>', 'match events containing this UTF-8 substring')\n .option('--hex <h>', 'match events containing this hex byte sequence')\n .option('--regex <re>', 'match events whose UTF-8 text matches this regex')\n .option('--dir <dir>', 'only match rx or tx')\n .option('-C, --context <n>', 'context events to show around each match', '2')\n .option('--no-color', 'disable ANSI colors')\n .option('--no-timestamp', 'omit timestamps')\n .action(async (file: string, opts: Record<string, unknown>) => {\n const matcher = buildMatcher({\n string: opts.string as string | undefined,\n hex: opts.hex as string | undefined,\n regex: opts.regex as string | undefined,\n dir: opts.dir as Direction | undefined,\n });\n if (!matcher) {\n process.stderr.write(\n chalk.red('error: provide a filter: --string, --hex, --regex or --dir\\n'),\n );\n process.exit(1);\n }\n\n const events: IndexedEvent[] = [];\n const rl = readline.createInterface({\n input: fs.createReadStream(file),\n crlfDelay: Infinity,\n });\n let headerSeen = false;\n let lineNo = 0;\n for await (const line of rl) {\n lineNo++;\n if (!headerSeen) {\n decodeHeader(line);\n headerSeen = true;\n continue;\n }\n events.push({ line: lineNo, e: decodeEvent(line) });\n }\n\n const matchIdx = new Set<number>();\n events.forEach((item, i) => {\n if (matcher.matches(item.e)) matchIdx.add(i);\n });\n if (matchIdx.size === 0) {\n process.stdout.write('no matches\\n');\n return;\n }\n\n const ctx = Math.max(0, Number(opts.context) || 0);\n const color = Boolean(opts.color);\n chalk.level = color ? 1 : 0;\n const lineOpts: LineOptions = {\n mode: 'hex',\n timestamp: Boolean(opts.timestamp),\n color,\n };\n\n const inWindow = new Array<boolean>(events.length).fill(false);\n for (const m of matchIdx) {\n const from = Math.max(0, m - ctx);\n const to = Math.min(events.length - 1, m + ctx);\n for (let i = from; i <= to; i++) inWindow[i] = true;\n }\n\n process.stderr.write(chalk.dim(`# ${matchIdx.size} matches\\n`));\n\n let prev = -1;\n for (let i = 0; i < events.length; i++) {\n if (!inWindow[i]) continue;\n if (prev !== -1 && i > prev + 1) process.stdout.write(chalk.dim('--\\n'));\n const item = events[i]!;\n const isMatch = matchIdx.has(i);\n const marker = isMatch ? chalk.cyan('* ') : ' ';\n const lineNum = chalk.gray(String(item.line).padStart(5)) + ' ';\n process.stdout.write(marker + lineNum + formatEventLine(item.e, lineOpts) + '\\n');\n prev = i;\n }\n });\n}\n","import chalk from 'chalk';\nimport type { Command } from 'commander';\nimport { parseSpec, createTransport, replayFileToTransport } from '@anthonyfree96/core';\nimport type { Direction } from '@anthonyfree96/core';\nimport { flushStdio } from '../stdio.js';\n\nexport function registerReplay(program: Command): void {\n program\n .command('replay')\n .argument('<file>', 'recording file (JSON Lines)')\n .requiredOption('--to <spec>', 'target transport spec to send to')\n .option('--speed <n>', 'replay speed multiplier (0 = as fast as possible)', '1')\n .option('--loop', 're-send continuously until interrupted')\n .option('--dir <dir>', 'only replay rx or tx (default: all, chronological)')\n .action(async (file: string, opts: Record<string, unknown>) => {\n const dir = opts.dir as Direction | undefined;\n if (dir && dir !== 'rx' && dir !== 'tx') {\n process.stderr.write(chalk.red(`error: --dir must be \"rx\" or \"tx\", got \"${dir}\"\\n`));\n process.exit(1);\n }\n const speed = Number(opts.speed);\n if (Number.isNaN(speed) || speed < 0) {\n process.stderr.write(chalk.red(`error: invalid --speed \"${opts.speed}\"\\n`));\n process.exit(1);\n }\n\n let specParsed;\n try {\n specParsed = parseSpec(opts.to as string);\n } catch (err) {\n process.stderr.write(chalk.red(`error: ${(err as Error).message}\\n`));\n process.exit(1);\n }\n\n const target = createTransport(specParsed);\n target.on('open', ({ meta }) =>\n process.stderr.write(chalk.dim(`# replaying ${file} -> ${meta.desc}\\n`)),\n );\n target.on('error', ({ error }) =>\n process.stderr.write(chalk.red(`error: ${error.message}\\n`)),\n );\n\n let stopping = false;\n const stop = async () => {\n if (stopping) return;\n stopping = true;\n await target.stop();\n await flushStdio();\n process.exit(0);\n };\n process.on('SIGINT', stop);\n process.on('SIGTERM', stop);\n\n try {\n await target.start();\n const sent = await replayFileToTransport(file, target, {\n speed,\n loop: Boolean(opts.loop),\n dir,\n });\n process.stderr.write(chalk.dim(`# replayed ${sent} events\\n`));\n await stop();\n } catch (err) {\n process.stderr.write(chalk.red(`error: ${(err as Error).message}\\n`));\n await flushStdio();\n process.exit(1);\n }\n });\n}\n"],"mappings":"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","names":["chalk","toDesc","chalk","dir","chalk","toHex","peer","toHex","program","chalk","toDesc","chalk","program","chalk","parseSpec","createTransport","Session","Analyzer","program","parseSpec","chalk","createTransport","Analyzer","Session","chalk","Session","program","dir","chalk","Session","fs","chalk","program","chalk","fs","chalk","parseSpec","createTransport","program","dir","chalk","parseSpec","createTransport"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "comm-scope",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Serial / TCP / UDP traffic monitor for developers — CLI",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"comm-scope": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"serial",
|
|
19
|
+
"tcp",
|
|
20
|
+
"udp",
|
|
21
|
+
"monitor",
|
|
22
|
+
"traffic",
|
|
23
|
+
"replay",
|
|
24
|
+
"sniffer",
|
|
25
|
+
"debug"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/AndyFree96/comm-scope.git"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsup",
|
|
33
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@anthonyfree96/core": "^0.1.0",
|
|
37
|
+
"chalk": "^5.3.0",
|
|
38
|
+
"commander": "^12.1.0",
|
|
39
|
+
"neo-blessed": "^0.2.0"
|
|
40
|
+
}
|
|
41
|
+
}
|