2g 0.3.1 → 0.4.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.
@@ -2,17 +2,19 @@ import { fileURLToPath as e } from "node:url";
2
2
 
3
3
  import { parseArgs as t } from "node:util";
4
4
 
5
- import { a as n, b as s, c as o, d as i, m as r, e as a, p as l } from "./sessionSockets-chunk.mjs";
5
+ import { c as n, a as s, b as o, p as r, m as i, d as a, e as l } from "./sessionSockets-chunk.mjs";
6
6
 
7
- import { r as p, s as c, f as u } from "./pretty-chunk.mjs";
7
+ import { r as c, s as p, f as u } from "./pretty-chunk.mjs";
8
8
 
9
9
  import d from "node:fs";
10
10
 
11
- import { r as f, t as m, l as h } from "./tap-chunk.mjs";
11
+ import { d as m, r as f, t as h, l as g, c as v } from "./api-chunk.mjs";
12
12
 
13
- import { createInterface as g } from "node:readline";
13
+ import { createInterface as y } from "node:readline";
14
14
 
15
- import v from "node:crypto";
15
+ import w from "node:crypto";
16
+
17
+ import { spawn as b } from "node:child_process";
16
18
 
17
19
  function runCleanCli(e = []) {
18
20
  const o = function parseCleanOptions(e) {
@@ -38,15 +40,15 @@ function runCleanCli(e = []) {
38
40
  };
39
41
  }(e);
40
42
  if (o.json) {
41
- p();
43
+ c();
42
44
  }
43
- const i = o.all ? n() : s();
45
+ const r = o.all ? n() : s();
44
46
  process.stdout.write(o.json ? `${JSON.stringify({
45
- removed: i
46
- })}\n` : `Removed ${i} ${o.all ? "exited" : "stale"} sessions.\n`);
47
+ removed: r
48
+ })}\n` : `Removed ${r} ${o.all ? "exited" : "stale"} sessions.\n`);
47
49
  }
48
50
 
49
- const y = {
51
+ const x = {
50
52
  since: {
51
53
  type: "string"
52
54
  },
@@ -65,8 +67,15 @@ const y = {
65
67
  }
66
68
  };
67
69
 
70
+ async function warnOnRotationLoss(e, t) {
71
+ if (!await m(e)) {
72
+ return;
73
+ }
74
+ console.warn(`⚠ Earlier events were rotated out of the retained window.\n For a complete trace, start \`${t}\` before the workload.`);
75
+ }
76
+
68
77
  async function* readJsonlStream(e) {
69
- const t = g({
78
+ const t = y({
70
79
  input: e
71
80
  });
72
81
  for await (const e of t) {
@@ -82,6 +91,21 @@ async function* readJsonlStream(e) {
82
91
  }
83
92
  }
84
93
 
94
+ async function* filterEvents(e, t) {
95
+ const n = o(t.filter);
96
+ const s = r(t.since);
97
+ for await (const o of e) {
98
+ if (i(o, {
99
+ filter: t.filter,
100
+ since: s,
101
+ spans: t.spans,
102
+ debug: t.debug
103
+ }, n)) {
104
+ yield o;
105
+ }
106
+ }
107
+ }
108
+
85
109
  function parseSharedTapOptions(e) {
86
110
  return {
87
111
  since: e.since,
@@ -106,7 +130,15 @@ function parseHelp(e) {
106
130
  }).values.help;
107
131
  }
108
132
 
109
- const w = [ ":started", ":done", ":failed" ];
133
+ const S = [ ":started", ":done", ":failed" ];
134
+
135
+ async function convertToChromeTrace(e, t = {}) {
136
+ const n = new TraceConverter(t);
137
+ for await (const t of e) {
138
+ n.add(t);
139
+ }
140
+ return n.toTraceFile();
141
+ }
110
142
 
111
143
  class TraceConverter {
112
144
  #e=[];
@@ -141,7 +173,7 @@ class TraceConverter {
141
173
  name: e.slice(t + 1)
142
174
  };
143
175
  }(e._e);
144
- const n = this.#i(t.category, "string" == typeof e._w ? e._w : void 0);
176
+ const n = this.#r(t.category, "string" == typeof e._w ? e._w : void 0);
145
177
  const s = function extractArgs(e) {
146
178
  const t = {};
147
179
  for (const n of Object.keys(e)) {
@@ -192,31 +224,31 @@ class TraceConverter {
192
224
  const n = new Map;
193
225
  let s = 1;
194
226
  for (const [e, o] of this.#t) {
195
- const i = [];
227
+ const r = [];
196
228
  for (const t of this.#e) {
197
229
  if ("span" === t.kind && t.track === e) {
198
- i.push(t);
230
+ r.push(t);
199
231
  }
200
232
  }
201
- const r = Math.max(assignLanes(i), 1);
202
- for (let i = 0; i < r; i++) {
203
- const r = s++;
204
- n.set(`${e}\0${i}`, r);
233
+ const i = Math.max(assignLanes(r), 1);
234
+ for (let r = 0; r < i; r++) {
235
+ const i = s++;
236
+ n.set(`${e}\0${r}`, i);
205
237
  t.push({
206
238
  ph: "M",
207
239
  name: "thread_name",
208
240
  pid: this.#n,
209
- tid: r,
241
+ tid: i,
210
242
  args: {
211
- name: 0 === i ? o : `${o} #${i + 1}`
243
+ name: 0 === r ? o : `${o} #${r + 1}`
212
244
  }
213
245
  }, {
214
246
  ph: "M",
215
247
  name: "thread_sort_index",
216
248
  pid: this.#n,
217
- tid: r,
249
+ tid: i,
218
250
  args: {
219
- sort_index: r
251
+ sort_index: i
220
252
  }
221
253
  });
222
254
  }
@@ -258,7 +290,7 @@ class TraceConverter {
258
290
  }
259
291
  };
260
292
  }
261
- #i(e, t) {
293
+ #r(e, t) {
262
294
  const n = t ? `${e}:${t}` : e;
263
295
  if (!this.#t.has(n)) {
264
296
  this.#t.set(n, t ? `${e} ${t}` : e);
@@ -274,13 +306,13 @@ function assignLanes(e) {
274
306
  const t = e.ts + e.dur;
275
307
  let s = -1;
276
308
  for (let o = 0; o < n.length; o++) {
277
- const i = n[o];
278
- while (i.length && i[i.length - 1].ts + i[i.length - 1].dur <= e.ts) {
279
- i.pop();
309
+ const r = n[o];
310
+ while (r.length && r[r.length - 1].ts + r[r.length - 1].dur <= e.ts) {
311
+ r.pop();
280
312
  }
281
- const r = i[i.length - 1];
282
- if (!r || r.ts + r.dur >= t) {
283
- i.push(e);
313
+ const i = r[r.length - 1];
314
+ if (!i || i.ts + i.dur >= t) {
315
+ r.push(e);
284
316
  s = o;
285
317
  break;
286
318
  }
@@ -295,7 +327,7 @@ function assignLanes(e) {
295
327
  }
296
328
 
297
329
  function stripSuffix$1(e) {
298
- for (const t of w) {
330
+ for (const t of S) {
299
331
  if (e.endsWith(t)) {
300
332
  return e.slice(0, -t.length);
301
333
  }
@@ -303,16 +335,24 @@ function stripSuffix$1(e) {
303
335
  return e;
304
336
  }
305
337
 
306
- const b = [ ":started", ":done", ":failed" ];
338
+ const T = [ ":started", ":done", ":failed" ];
339
+
340
+ async function convertToOpenTelemetry(e, t = {}) {
341
+ const n = new OpenTelemetryConverter(t);
342
+ for await (const t of e) {
343
+ n.add(t);
344
+ }
345
+ return n.toFile();
346
+ }
307
347
 
308
348
  class OpenTelemetryConverter {
309
- #r=v.randomBytes(16).toString("hex");
349
+ #i=w.randomBytes(16).toString("hex");
310
350
  #a=createSpanId();
311
351
  #l=[];
312
- #p=[];
352
+ #c=[];
313
353
  #s;
314
354
  #o="0.1.0";
315
- #c;
355
+ #p;
316
356
  #u;
317
357
  constructor(e) {
318
358
  this.options = e;
@@ -320,7 +360,7 @@ class OpenTelemetryConverter {
320
360
  }
321
361
  add(e) {
322
362
  const t = unixNano(e._t);
323
- this.#c = null == this.#c || BigInt(t) < BigInt(this.#c) ? t : this.#c;
363
+ this.#p = null == this.#p || BigInt(t) < BigInt(this.#p) ? t : this.#p;
324
364
  this.#u = null == this.#u || BigInt(t) > BigInt(this.#u) ? t : this.#u;
325
365
  if ("root:init" === e._e) {
326
366
  if ("string" == typeof e.version) {
@@ -334,11 +374,11 @@ class OpenTelemetryConverter {
334
374
  if ("number" == typeof e._d) {
335
375
  this.#l.push(this.#d(e));
336
376
  } else {
337
- this.#p.push(this.#f(e));
377
+ this.#c.push(this.#m(e));
338
378
  }
339
379
  }
340
380
  toFile() {
341
- const e = this.#c ?? unixNano(Date.now());
381
+ const e = this.#p ?? unixNano(Date.now());
342
382
  const t = this.#u ?? e;
343
383
  return {
344
384
  resourceSpans: [ {
@@ -356,7 +396,7 @@ class OpenTelemetryConverter {
356
396
  name: "2g"
357
397
  },
358
398
  spans: [ {
359
- traceId: this.#r,
399
+ traceId: this.#i,
360
400
  spanId: this.#a,
361
401
  name: this.#s,
362
402
  startTimeUnixNano: e,
@@ -364,7 +404,7 @@ class OpenTelemetryConverter {
364
404
  attributes: attributes({
365
405
  "event_log.kind": "session"
366
406
  }),
367
- events: this.#p.length ? this.#p : void 0
407
+ events: this.#c.length ? this.#c : void 0
368
408
  }, ...this.#l ]
369
409
  } ]
370
410
  } ]
@@ -375,7 +415,7 @@ class OpenTelemetryConverter {
375
415
  const n = unixNano(e._t - e._d);
376
416
  const s = unixNano(e._t);
377
417
  return {
378
- traceId: this.#r,
418
+ traceId: this.#i,
379
419
  spanId: createSpanId(),
380
420
  parentSpanId: this.#a,
381
421
  name: t ? stripSuffix(t.name) : e._e,
@@ -384,7 +424,7 @@ class OpenTelemetryConverter {
384
424
  attributes: eventAttributes(e, t)
385
425
  };
386
426
  }
387
- #f(e) {
427
+ #m(e) {
388
428
  const t = splitEventName(e._e);
389
429
  return {
390
430
  timeUnixNano: unixNano(e._t),
@@ -470,7 +510,7 @@ function anyValue(e) {
470
510
  }
471
511
 
472
512
  function createSpanId() {
473
- return v.randomBytes(8).toString("hex");
513
+ return w.randomBytes(8).toString("hex");
474
514
  }
475
515
 
476
516
  function unixNano(e) {
@@ -489,7 +529,7 @@ function splitEventName(e) {
489
529
  }
490
530
 
491
531
  function stripSuffix(e) {
492
- for (const t of b) {
532
+ for (const t of T) {
493
533
  if (e.endsWith(t)) {
494
534
  return e.slice(0, -t.length);
495
535
  }
@@ -500,7 +540,7 @@ function stripSuffix(e) {
500
540
  async function runExportCli(e) {
501
541
  if (parseHelp(e)) {
502
542
  !function printExportHelp() {
503
- process.stdout.write([ "Usage: 2g export [selector]", "", "Exports session events to Chrome Trace or OTLP JSON.", "", "Options:", " selector Substring of PID, CWD, or command; a running", " session wins ties", " --input <file> Read JSONL events from a file", " -o, --output <file> Write output to a file", " --format <format> chrome-trace or opentelemetry", " --json Print JSON output", " --since <time> Duration ago (5m, 90s), Unix time, or ISO date", " --filter <pattern> Event name prefix (whole segments), e.g. category,", " category:kind; * wildcards; comma-separated", " --spans Keep only span events (log.span() pairs with a _d", " duration); omit to include point events too. A", ' plain "ms" payload field is not a span', " --debug Include debug events (only recorded when the", " command ran with LOG_DEBUG set)", " --tail Follow live events after replaying history", " --timeout <duration> Stop tailing after an absolute duration", " Tail also stops after 30s without new events", "" ].join("\n"));
543
+ process.stdout.write([ "Usage: 2g export [selector]", "", "Exports session events to Chrome Trace or OTLP JSON.", "(Without --tail it reads only the retained window. Rotated out logs may be lost)", "", "--tail attaches to an already-running session and follows live from that point.", " Run it after the session is up and leave it attached while the workload runs.", " It will automatically stop on 30s idle.", "To trace a full command instead, use `2g record`.", "", "Options:", " selector Substring of PID, CWD, or command; a running", " session wins ties", " --input <file> Read JSONL events from a file", " -o, --output <file> Write output to a file", " --format <format> chrome-trace or opentelemetry", " --json Print JSON output", " --since <time> Duration ago (5m, 90s), Unix time, or ISO date", " --filter <pattern> Event name prefix (whole segments), e.g. category,", " category:kind; * wildcards; comma-separated", " --spans Keep only span events (log.span() pairs with a _d", " duration); omit to include point events too. A", ' plain "ms" payload field is not a span', " --debug Include debug events (only recorded when the", " command ran with LOG_DEBUG set)", " --tail Follow live events after replaying history; stops", " automatically on 30s idle", " --timeout <duration> Also stop tailing after this absolute duration", "" ].join("\n"));
504
544
  }();
505
545
  return;
506
546
  }
@@ -509,7 +549,7 @@ async function runExportCli(e) {
509
549
  args: e,
510
550
  allowPositionals: !0,
511
551
  options: {
512
- ...y,
552
+ ...x,
513
553
  input: {
514
554
  type: "string"
515
555
  },
@@ -543,11 +583,11 @@ async function runExportCli(e) {
543
583
  }
544
584
  return t;
545
585
  }(n.values.timeout);
546
- const i = n.values.format ?? "chrome-trace";
547
- if (!function isExportFormat(e) {
586
+ const r = n.values.format ?? "chrome-trace";
587
+ if (!function isExportFormat$1(e) {
548
588
  return "chrome-trace" === e || "opentelemetry" === e;
549
- }(i)) {
550
- throw new Error(`Unsupported format: ${i}`);
589
+ }(r)) {
590
+ throw new Error(`Unsupported format: ${r}`);
551
591
  }
552
592
  if (n.positionals[0] && n.values.input) {
553
593
  throw new Error("Use either a session selector or --input, not both");
@@ -559,7 +599,7 @@ async function runExportCli(e) {
559
599
  selector: n.positionals[0],
560
600
  inputFile: n.values.input,
561
601
  output: n.values.output,
562
- format: i,
602
+ format: r,
563
603
  since: s.since,
564
604
  filter: s.filter,
565
605
  spans: s.spans,
@@ -571,18 +611,19 @@ async function runExportCli(e) {
571
611
  };
572
612
  }(e);
573
613
  if (n.json) {
574
- p();
614
+ c();
575
615
  }
576
616
  let s;
577
- let l;
578
- let c;
617
+ let o;
618
+ let r;
579
619
  if (n.selector) {
580
620
  const e = await f(n.selector);
581
- l = function formatSessionProcessName(e) {
621
+ o = function formatSessionProcessName(e) {
582
622
  return e.version ? `${e.command} (v${e.version}, PID ${e.pid})` : `${e.command} (PID ${e.pid})`;
583
623
  }(e);
584
- c = e.pid;
585
- s = m(e.sessionDir, {
624
+ r = e.pid;
625
+ await warnOnRotationLoss(e.sessionDir, "2g export --tail");
626
+ s = h(e.sessionDir, {
586
627
  follow: n.follow,
587
628
  debug: n.debug,
588
629
  timeout: n.timeout,
@@ -599,44 +640,19 @@ async function runExportCli(e) {
599
640
  yield* readJsonlStream(process.stdin);
600
641
  }();
601
642
  }
602
- const u = async function* filterEvents(e, t) {
603
- const n = o(t.filter);
604
- const s = i(t.since);
605
- for await (const o of e) {
606
- if (r(o, {
607
- filter: t.filter,
608
- since: s,
609
- spans: t.spans,
610
- debug: t.debug
611
- }, n)) {
612
- yield o;
613
- }
614
- }
615
- }(s, n);
616
- const h = "opentelemetry" === n.format ? await async function convertToOpenTelemetry(e, t = {}) {
617
- const n = new OpenTelemetryConverter(t);
618
- for await (const t of e) {
619
- n.add(t);
620
- }
621
- return n.toFile();
622
- }(u, {
623
- pid: c,
624
- processName: l
625
- }) : await async function convertToChromeTrace(e, t = {}) {
626
- const n = new TraceConverter(t);
627
- for await (const t of e) {
628
- n.add(t);
629
- }
630
- return n.toTraceFile();
631
- }(u, {
632
- pid: c,
633
- processName: l
643
+ const i = filterEvents(s, n);
644
+ const l = "opentelemetry" === n.format ? await convertToOpenTelemetry(i, {
645
+ pid: r,
646
+ processName: o
647
+ }) : await convertToChromeTrace(i, {
648
+ pid: r,
649
+ processName: o
634
650
  });
635
- const g = JSON.stringify(h, null, 2);
651
+ const p = JSON.stringify(l, null, 2);
636
652
  if (n.output) {
637
- d.writeFileSync(n.output, `${g}\n`);
653
+ d.writeFileSync(n.output, `${p}\n`);
638
654
  } else {
639
- process.stdout.write(`${g}\n`);
655
+ process.stdout.write(`${p}\n`);
640
656
  }
641
657
  }
642
658
 
@@ -644,6 +660,99 @@ function escapeTsv(e) {
644
660
  return String(e).replace(/\\/g, "\\\\").replace(/\t/g, "\\t").replace(/\r/g, "\\r").replace(/\n/g, "\\n");
645
661
  }
646
662
 
663
+ async function runRecordCli(e) {
664
+ if (parseHelp(e)) {
665
+ !function printRecordHelp() {
666
+ process.stdout.write([ "Usage: 2g record [options] -- <command> [args...]", "", "Runs <command>, traces the events it emits, and writes the trace on exit.", "", "This is the reliable way to capture a one-off run (a build, a script): 2g", "owns the process, so the trace is complete end-to-end with no window to", "attach within and nothing to rotate out. To trace an already-running server", "instead, use `2g export <selector> --tail`.", "", "Options:", " -o, --output <file> Write the trace to a file (default: stdout)", " --format <format> chrome-trace or opentelemetry", " --json Print JSON output", " --filter <pattern> Event name prefix (whole segments), e.g. category,", " category:kind; * wildcards; comma-separated", " --spans Keep only span events (log.span() pairs with a _d", " duration); omit to include point events too", " --debug Enable and export debug events; no LOG_DEBUG needed", "", "Example:", " 2g record -o trace.json -- expo export # run & trace a build end-to-end", "" ].join("\n"));
667
+ }();
668
+ return;
669
+ }
670
+ const n = e.indexOf("--");
671
+ const s = -1 === n ? e : e.slice(0, n);
672
+ const o = -1 === n ? [] : e.slice(n + 1);
673
+ const r = function parseRecordOptions(e) {
674
+ const n = t({
675
+ args: e,
676
+ allowPositionals: !0,
677
+ options: {
678
+ filter: {
679
+ type: "string",
680
+ multiple: !0
681
+ },
682
+ spans: {
683
+ type: "boolean"
684
+ },
685
+ debug: {
686
+ type: "boolean"
687
+ },
688
+ output: {
689
+ type: "string",
690
+ short: "o"
691
+ },
692
+ format: {
693
+ type: "string"
694
+ },
695
+ json: {
696
+ type: "boolean"
697
+ },
698
+ help: {
699
+ type: "boolean",
700
+ short: "h"
701
+ }
702
+ }
703
+ });
704
+ const s = n.values.format ?? "chrome-trace";
705
+ if (!function isExportFormat(e) {
706
+ return "chrome-trace" === e || "opentelemetry" === e;
707
+ }(s)) {
708
+ throw new Error(`Unsupported format: ${s}`);
709
+ }
710
+ return {
711
+ output: n.values.output,
712
+ format: s,
713
+ filter: n.values.filter?.flatMap(e => e.split(",")).map(e => e.trim()).filter(Boolean),
714
+ spans: !0 === n.values.spans,
715
+ debug: !0 === n.values.debug,
716
+ json: n.values.json ?? !1
717
+ };
718
+ }(s);
719
+ if (!o.length) {
720
+ throw new Error("record needs a command to run: 2g record [options] -- <command> [args...]");
721
+ }
722
+ if (r.json) {
723
+ c();
724
+ }
725
+ const i = v({
726
+ debug: r.debug,
727
+ filter: r.filter
728
+ });
729
+ const a = b(o[0], o.slice(1), i.spawnOptions({
730
+ env: process.env,
731
+ stdio: [ "inherit", r.output ? "inherit" : 2, "inherit" ]
732
+ }));
733
+ i.attach(a);
734
+ const l = new Promise((e, t) => {
735
+ a.on("exit", t => e(t ?? 0));
736
+ a.on("error", e => t(new Error(`Failed to run ${o[0]}: ${e.message}`)));
737
+ });
738
+ const p = {
739
+ pid: a.pid,
740
+ processName: o.join(" ")
741
+ };
742
+ const u = filterEvents(i, r);
743
+ const m = "opentelemetry" === r.format ? await convertToOpenTelemetry(u, p) : await convertToChromeTrace(u, p);
744
+ const f = await l;
745
+ const h = JSON.stringify(m, null, 2);
746
+ if (r.output) {
747
+ d.writeFileSync(r.output, `${h}\n`);
748
+ } else {
749
+ process.stdout.write(`${h}\n`);
750
+ }
751
+ if (f) {
752
+ process.exitCode = f;
753
+ }
754
+ }
755
+
647
756
  async function runTapCli(e) {
648
757
  if (parseHelp(e)) {
649
758
  !function printTapHelp() {
@@ -656,7 +765,7 @@ async function runTapCli(e) {
656
765
  args: e,
657
766
  allowPositionals: !0,
658
767
  options: {
659
- ...y,
768
+ ...x,
660
769
  format: {
661
770
  type: "string"
662
771
  },
@@ -685,11 +794,12 @@ async function runTapCli(e) {
685
794
  };
686
795
  }(e);
687
796
  if (n.json) {
688
- p();
797
+ c();
689
798
  }
690
799
  const s = await f(n.selector);
691
- const o = c(process.stdout);
692
- for await (const e of m(s.sessionDir, n)) {
800
+ await warnOnRotationLoss(s.sessionDir, "2g tap --tail");
801
+ const o = p(process.stdout);
802
+ for await (const e of h(s.sessionDir, n)) {
693
803
  if (n.pretty) {
694
804
  process.stdout.write(`${u(e, {
695
805
  colorize: o,
@@ -703,9 +813,9 @@ async function runTapCli(e) {
703
813
 
704
814
  async function main(e = process.argv.slice(2)) {
705
815
  const [n, ...s] = e;
706
- if (!n || "--help" === n || "-h" === n) {
816
+ if (!n || "--help" === n || "-h" === n || "help" === n) {
707
817
  !function printHelp() {
708
- process.stdout.write([ "Usage: 2g <command>", "", "Reads structured JSONL event logs that instrumented CLIs record during runs", "into the system temp dir, with no setup: run a command, then tap it here.", "", "Commands:", " ps [selector] List recorded sessions", " --active, -a (running only) --json", "", " tap [selector] Replay session events as JSONL, oldest first", " --since <time> --filter <pattern> --spans --debug --format pretty", " --tail (follow live events after replay; runs until killed)", "", " export [selector] Export events to Chrome Trace or OTLP JSON", " --input <jsonl-file> -o, --output <file> --json", " --format chrome-trace|opentelemetry", " --since <time> --filter <pattern> --spans --debug", " --tail --timeout <duration> (absolute; tail also stops after 30s idle)", "", " typegen List every event name/payload a project can emit", " -p, --project <tsconfig> -o, --output <file> --format dts|json --json", "", " clean Remove stale sessions", " --all, -a (remove all exited sessions) --json", "", "Typical use:", " 2g ps -a # what is running now?", ' 2g tap "expo start" --tail # follow live events', ' 2g tap "expo start" --since 5m --format pretty # recent history, readable', ' 2g tap "expo start" --filter metro:bundling # narrow to one area', ' 2g export "expo start" -o trace.json # flame chart of a run', "", "Selectors match sessions by substring of PID, CWD, or command. If several", "match, and exactly one of them is running, it is chosen; otherwise 2g errors", "and lists the candidates.", "", "--filter takes comma-separated event-name prefixes matched on whole", 'segments: "category" and "category:kind" both match "category:kind:test";', "* matches anything. Discover names by tapping unfiltered or via typegen.", "", "--since accepts a duration ago (5m, 90s, 1h30m), Unix time, or ISO date.", '--spans keeps only span events (log.span() pairs with a "_d" duration in', "ms); without it, point events are included alongside spans.", "", 'Events are single JSON lines: {"_e":"category:kind","_t":<epoch ms>,...}.', '"_d" is a span duration, "_w" a worker id; debug events need --debug.', "", "Set LOG_EVENTS=<file|fd> on an instrumented command to redirect its events.", "Debug events are only recorded when the command runs with LOG_DEBUG set.", "", "Pass LOG_DEBUG=<filter> when debugging; it also mirrors matching events to", "stderr. Reading them back needs --debug.", "", "Run 2g <command> --help for per-command help.", "" ].join("\n"));
818
+ process.stdout.write([ "Usage: 2g <command>", "", "Reads structured JSONL event logs that instrumented CLIs record during runs", "into the system temp dir, with no setup: run a command, then tap it here.", "", "Commands:", " ps [selector] List recorded sessions", " --active, -a (running only) --json", "", " tap [selector] Replay session events as JSONL, oldest first", " --since <time> --filter <pattern> --spans --debug --format pretty", " --tail (follow live events after replay; runs until killed)", "", " record -- <command> Run a command, trace it end-to-end, write on exit", " -o, --output <file> --format chrome-trace|opentelemetry --json", " --filter <pattern> --spans --debug", "", " export [selector] Export a past or live (--tail) session", " --input <jsonl-file> -o, --output <file> --json", " --format chrome-trace|opentelemetry", " --since <time> --filter <pattern> --spans --debug", " --tail --timeout <duration> (absolute; tail also stops after 30s idle)", "", " typegen List every event name/payload a project can emit", " -p, --project <tsconfig> -o, --output <file> --format dts|json --json", "", " clean Remove stale sessions", " --all, -a (remove all exited sessions) --json", "", "Typical use:", " 2g ps -a # what is running now?", ' 2g tap "expo start" --tail # follow live events', ' 2g tap "expo start" --since 5m --format pretty # recent history, readable', ' 2g tap "expo start" --filter metro:bundling # narrow to one area', "", "Trace a run to a flame chart:", " 2g record -o trace.json -- expo export # run & trace a build", ' 2g export "expo start" --tail -o trace.json # attach to a running server', ' 2g export "expo start" -o trace.json # after the fact (partial)', "", "Selectors match sessions by substring of PID, CWD, or command. If several", "match, and exactly one of them is running, it is chosen; otherwise 2g errors", "and lists the candidates.", "", "--filter takes comma-separated event-name prefixes matched on whole", 'segments: "category" and "category:kind" both match "category:kind:test";', "* matches anything. Discover names by tapping unfiltered or via typegen.", "", "--since accepts a duration ago (5m, 90s, 1h30m), Unix time, or ISO date.", '--spans keeps only span events (log.span() pairs with a "_d" duration in', "ms); without it, point events are included alongside spans.", "", 'Events are single JSON lines: {"_e":"category:kind","_t":<epoch ms>,...}.', '"_d" is a span duration, "_w" a worker id; debug events need --debug.', "", "Set LOG_EVENTS=<file|fd> on an instrumented command to redirect its events.", "Debug events are only recorded when the command runs with LOG_DEBUG set.", "", "Pass LOG_DEBUG=<filter> when debugging; it also mirrors matching events to", "stderr. Reading them back needs --debug.", "", "Run 2g <command> --help for per-command help.", "" ].join("\n"));
709
819
  }();
710
820
  return 0;
711
821
  }
@@ -743,9 +853,9 @@ async function main(e = process.argv.slice(2)) {
743
853
  }
744
854
  });
745
855
  if (n.values.json) {
746
- p();
856
+ c();
747
857
  }
748
- const s = (await h({
858
+ const s = (await g({
749
859
  selector: n.positionals[0]
750
860
  })).filter(e => !n.values.active || e.alive);
751
861
  if (n.values.json) {
@@ -773,6 +883,10 @@ async function main(e = process.argv.slice(2)) {
773
883
  runCleanCli(s);
774
884
  return 0;
775
885
 
886
+ case "record":
887
+ await runRecordCli(s);
888
+ return 0;
889
+
776
890
  case "export":
777
891
  await runExportCli(s);
778
892
  return 0;