@lensmcp/node-instrumentation 1.0.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-hook.d.ts","sourceRoot":"","sources":["../../src/lib/require-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;GAQG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,aAAa,EAAE,OAAO,KAAK,OAAO,GAAG,IAAI,CAAC;AAanE,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,IAAI,CA2BxE"}
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ /**
3
+ * CJS require hook: the cluster's dev webpack config externalizes every real
4
+ * node_modules package (`commonjs <request>`), so at pod runtime `pg`,
5
+ * `ioredis`, `bullmq` and `@nestjs/core` arrive through `Module._load`. We
6
+ * intercept the EXACT package names and mutate their exports IN PLACE
7
+ * (prototype/static patches, never proxies) — ESM `import` of the same CJS
8
+ * package yields the same exports object, so both module systems observe the
9
+ * patched behavior.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.installRequireHook = installRequireHook;
13
+ const tapped = new Set();
14
+ function runTap(name, tap, exportsObject) {
15
+ try {
16
+ const installed = tap(exportsObject);
17
+ if (installed !== false)
18
+ tapped.add(name);
19
+ }
20
+ catch {
21
+ tapped.add(name); // a THROWING tap must not retry forever
22
+ }
23
+ }
24
+ function installRequireHook(taps) {
25
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
26
+ const Module = require('node:module');
27
+ const origLoad = Module._load;
28
+ Module._load = function (request, ...rest) {
29
+ const exportsObject = origLoad.call(this, request, ...rest);
30
+ const tap = taps[request];
31
+ if (tap && !tapped.has(request))
32
+ runTap(request, tap, exportsObject);
33
+ return exportsObject;
34
+ };
35
+ // Anything already loaded before the hook installed gets tapped from cache.
36
+ for (const name of Object.keys(taps)) {
37
+ if (tapped.has(name))
38
+ continue;
39
+ const tap = taps[name];
40
+ if (!tap)
41
+ continue;
42
+ try {
43
+ const resolved = require.resolve(name);
44
+ const cached = require.cache[resolved];
45
+ if (cached)
46
+ runTap(name, tap, cached.exports);
47
+ }
48
+ catch {
49
+ /* package not installed in this workspace — fine */
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,2 @@
1
+ export declare function bullmqTap(exportsObject: unknown): boolean;
2
+ //# sourceMappingURL=tap-bullmq.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tap-bullmq.d.ts","sourceRoot":"","sources":["../../src/lib/tap-bullmq.ts"],"names":[],"mappings":"AAoDA,wBAAgB,SAAS,CAAC,aAAa,EAAE,OAAO,GAAG,OAAO,CAczD"}
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bullmqTap = bullmqTap;
4
+ /**
5
+ * BullMQ tap (source `queue`): the queue node on the canvas, zero-touch.
6
+ *
7
+ * Everything hooks PROTOTYPES — bullmq's module exports are TypeScript
8
+ * getter-only bindings (`__createBinding`, non-configurable), so replacing
9
+ * `exports.Worker` with a subclass silently fails. Prototype mutation is the
10
+ * seam that actually works:
11
+ * - `Queue.prototype.add` → kind `queue-enqueued` (+ ensures the state
12
+ * publisher using THIS queue instance — no extra connection)
13
+ * - `Worker.prototype.emit` → observes the worker's own lifecycle events:
14
+ * `active`/`completed`/`failed` → kinds `queue-active/completed/failed`,
15
+ * and on first sight of a worker ensures the state publisher
16
+ * - per queue, a 2s-when-changed snapshot → kind `queue-state` with `counts`
17
+ * + the 8 most recent jobs ({id, name, state, to?, timestamp}) — the exact
18
+ * contract the dashboard's queue panel renders.
19
+ */
20
+ const emit_1 = require("./emit");
21
+ const flow_context_1 = require("./flow-context");
22
+ function jobFlow(job) {
23
+ const stamp = job?.opts?.['__lensmcpFlow'];
24
+ return stamp && typeof stamp === 'object' ? stamp : undefined;
25
+ }
26
+ /** The worker leg's own requestId — its block in the flow, distinct from the
27
+ * HTTP request that enqueued it. */
28
+ const jobLegId = (queue, job) => `job:${queue}#${job?.id ?? '?'}`;
29
+ const publishers = new Map();
30
+ function bullmqTap(exportsObject) {
31
+ const mod = exportsObject;
32
+ // Partial exports mid-load (circular require) — let the hook retry later.
33
+ if (typeof mod?.['Queue'] !== 'function' || typeof mod?.['Worker'] !== 'function')
34
+ return false;
35
+ try {
36
+ tapQueueAdd(mod);
37
+ }
38
+ catch { /* shape drift — skip */ }
39
+ try {
40
+ tapWorkerEmit(mod);
41
+ }
42
+ catch { /* shape drift — skip */ }
43
+ try {
44
+ tapWorkerProcess(mod);
45
+ }
46
+ catch { /* shape drift — skip */ }
47
+ return true;
48
+ }
49
+ /**
50
+ * The worker's PROCESSING leg: wrap `Worker.prototype.callProcessJob` so the
51
+ * job runs inside the flow context stamped at enqueue — every tap the
52
+ * processor hits (an egress call, a db write) inherits the flow — and emit a
53
+ * `queue-process` span that anchors the worker leg in the trace.
54
+ */
55
+ function tapWorkerProcess(mod) {
56
+ const Worker = mod['Worker'];
57
+ const proto = Worker?.prototype;
58
+ const orig = proto?.['callProcessJob'];
59
+ if (!proto || typeof orig !== 'function' || orig.__lensmcp)
60
+ return;
61
+ const wrapped = async function (job, ...rest) {
62
+ const stamp = jobFlow(job);
63
+ if (!stamp?.flowId)
64
+ return orig.call(this, job, ...rest);
65
+ const queueName = String(this?.name ?? job?.queueName ?? 'queue');
66
+ const legId = jobLegId(queueName, job);
67
+ const t0 = Date.now();
68
+ const fctx = { flowId: stamp.flowId, requestId: legId };
69
+ return (0, flow_context_1.runInFlowContext)(fctx, async () => {
70
+ try {
71
+ const result = await orig.call(this, job, ...rest);
72
+ (0, emit_1.lensEmit)('queue', 'info', `process ${job?.name}#${job?.id} (${Date.now() - t0}ms)`, {
73
+ queue: queueName, kind: 'queue-process', job: job?.name, jobId: job?.id,
74
+ startedAt: t0, durationMs: Date.now() - t0, status: 'ok',
75
+ }, `queue:${queueName}`, fctx);
76
+ return result;
77
+ }
78
+ catch (err) {
79
+ (0, emit_1.lensEmit)('queue', 'error', `process ${job?.name}#${job?.id} failed (${Date.now() - t0}ms)`, {
80
+ queue: queueName, kind: 'queue-process', job: job?.name, jobId: job?.id,
81
+ startedAt: t0, durationMs: Date.now() - t0, status: 'error',
82
+ error: String(err?.message ?? err),
83
+ }, `queue:${queueName}`, fctx);
84
+ throw err;
85
+ }
86
+ });
87
+ };
88
+ wrapped.__lensmcp = true;
89
+ proto['callProcessJob'] = wrapped;
90
+ }
91
+ function tapQueueAdd(mod) {
92
+ const Queue = mod['Queue'];
93
+ const proto = Queue?.prototype;
94
+ const orig = proto?.['add'];
95
+ if (!proto || typeof orig !== 'function' || orig.__lensmcp)
96
+ return;
97
+ const wrapped = async function (name, ...rest) {
98
+ const t0 = Date.now();
99
+ const ctx = (0, flow_context_1.flowContext)(); // the enqueue happens inside the request's scope
100
+ if (ctx?.flowId) {
101
+ // Ride the flow on the job itself (opts survive the redis round trip),
102
+ // so the WORKER's processing joins this flow as its own leg.
103
+ const opts = (rest[1] ?? {});
104
+ rest[1] = { ...opts, __lensmcpFlow: { flowId: ctx.flowId, ...(ctx.requestId ? { originRequestId: ctx.requestId } : {}) } };
105
+ }
106
+ const job = await orig.call(this, name, ...rest);
107
+ try {
108
+ const queueName = String(this?.name ?? 'queue');
109
+ const data = rest[0];
110
+ let dataPreview;
111
+ try {
112
+ const s = JSON.stringify(data);
113
+ dataPreview = s && s !== 'undefined' ? (s.length > 300 ? s.slice(0, 299) + '…' : s) : undefined;
114
+ }
115
+ catch { /* unserializable payload */ }
116
+ (0, emit_1.lensEmit)('queue', 'info', `enqueued ${name}#${job?.id} → ${queueName}`, {
117
+ queue: queueName, kind: 'queue-enqueued', job: name, jobId: job?.id,
118
+ ...(dataPreview ? { data: dataPreview } : {}),
119
+ startedAt: t0, durationMs: Date.now() - t0,
120
+ }, `queue:${queueName}`, ctx);
121
+ ensureStatePublisher(queueName, this); // producer side sees state too
122
+ }
123
+ catch { /* ignore */ }
124
+ return job;
125
+ };
126
+ wrapped.__lensmcp = true;
127
+ proto['add'] = wrapped;
128
+ }
129
+ function tapWorkerEmit(mod) {
130
+ const Worker = mod['Worker'];
131
+ const proto = Worker?.prototype;
132
+ const orig = proto?.['emit'];
133
+ if (!proto || typeof orig !== 'function' || orig.__lensmcp)
134
+ return;
135
+ const wrapped = function (event, ...args) {
136
+ try {
137
+ observeWorkerEvent(this, String(event), args);
138
+ }
139
+ catch { /* never break the worker */ }
140
+ return orig.call(this, event, ...args);
141
+ };
142
+ wrapped.__lensmcp = true;
143
+ proto['emit'] = wrapped;
144
+ }
145
+ function observeWorkerEvent(worker, event, args) {
146
+ if (event !== 'active' && event !== 'completed' && event !== 'failed')
147
+ return;
148
+ const name = String(worker['name'] ?? 'queue');
149
+ const job = args[0];
150
+ // Lifecycle events join the worker LEG of the originating flow when the
151
+ // job carries the stamp; un-stamped jobs stay flow-less (canvas only).
152
+ const stamp = jobFlow(job);
153
+ const fctx = stamp?.flowId ? { flowId: stamp.flowId, requestId: jobLegId(name, job) } : undefined;
154
+ if (event === 'active') {
155
+ (0, emit_1.lensEmit)('queue', 'info', `processing ${job?.name}#${job?.id}`, {
156
+ queue: name, kind: 'queue-active', jobId: job?.id,
157
+ }, `queue:${name}`, fctx);
158
+ }
159
+ else if (event === 'completed') {
160
+ let returned;
161
+ try {
162
+ const rv = job?.returnvalue ?? args[1];
163
+ const s = JSON.stringify(rv);
164
+ returned = s && s !== 'undefined' ? (s.length > 160 ? s.slice(0, 159) + '…' : s) : undefined;
165
+ }
166
+ catch { /* unserializable */ }
167
+ (0, emit_1.lensEmit)('queue', 'info', `completed ${job?.name}#${job?.id}`, {
168
+ queue: name, kind: 'queue-completed', jobId: job?.id,
169
+ ...(returned ? { returned } : {}),
170
+ }, `queue:${name}`, fctx);
171
+ }
172
+ else {
173
+ const err = args[1];
174
+ (0, emit_1.lensEmit)('queue', 'error', `job ${job?.id} failed: ${err?.message}`, {
175
+ queue: name, kind: 'queue-failed', jobId: job?.id, error: String(err?.message ?? err),
176
+ }, `queue:${name}`, fctx);
177
+ }
178
+ if (!publishers.has(name)) {
179
+ // Build a snapshot Queue from the worker's own connection opts.
180
+ try {
181
+ const opts = worker['opts'];
182
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
183
+ const mod = require('bullmq');
184
+ const Queue = mod['Queue'];
185
+ ensureStatePublisher(name, new Queue(name, { connection: opts?.['connection'] }));
186
+ }
187
+ catch { /* snapshots unavailable — lifecycle events still flow */ }
188
+ }
189
+ }
190
+ function ensureStatePublisher(name, queue) {
191
+ if (publishers.has(name))
192
+ return;
193
+ let lastSig = '';
194
+ const timer = setInterval(() => {
195
+ void (async () => {
196
+ try {
197
+ const counts = await queue.getJobCounts();
198
+ const recent = [];
199
+ for (const state of ['active', 'waiting', 'failed', 'completed', 'delayed']) {
200
+ for (const j of await queue.getJobs([state], 0, 7)) {
201
+ const to = j?.data?.['to'];
202
+ recent.push({
203
+ id: String(j.id), name: j.name, state, timestamp: j.timestamp ?? 0,
204
+ ...(typeof to === 'string' ? { to } : {}),
205
+ });
206
+ }
207
+ }
208
+ recent.sort((a, b) => Number(b['timestamp']) - Number(a['timestamp']));
209
+ const top = recent.slice(0, 8);
210
+ const sig = JSON.stringify([counts, top.map((r) => `${r['id']}${r['state']}`)]);
211
+ if (sig === lastSig)
212
+ return;
213
+ lastSig = sig;
214
+ (0, emit_1.lensEmit)('queue', 'info', `queue state: ${counts['active'] ?? 0} active · ${counts['waiting'] ?? 0} waiting · ${counts['failed'] ?? 0} failed`, { queue: name, kind: 'queue-state', counts, recent: top }, `queue:${name}`);
215
+ }
216
+ catch { /* redis hiccup — next tick */ }
217
+ })();
218
+ }, 2000);
219
+ timer.unref?.();
220
+ publishers.set(name, timer);
221
+ }
@@ -0,0 +1,2 @@
1
+ export declare function tapEgress(): void;
2
+ //# sourceMappingURL=tap-egress.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tap-egress.d.ts","sourceRoot":"","sources":["../../src/lib/tap-egress.ts"],"names":[],"mappings":"AA0BA,wBAAgB,SAAS,IAAI,IAAI,CAIhC"}
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tapEgress = tapEgress;
4
+ /**
5
+ * Egress watch, zero-touch: wraps global `fetch` AND `http`/`https.request`
6
+ * so ANY outbound call leaving the system (openai, stripe, a BSP…) is
7
+ * published to the lens (source `external`) and appears on the cluster
8
+ * canvas attributed to this service — observed traffic, not config.
9
+ *
10
+ * Local/cluster-internal destinations (*.local, localhost, loopback, RFC1918,
11
+ * unix sockets) are passed through untraced: service-to-service calls are
12
+ * already attributed by the gateway (`x-api-key-id`), and double edges lie.
13
+ */
14
+ const emit_1 = require("./emit");
15
+ const flow_context_1 = require("./flow-context");
16
+ const INTERNAL = [
17
+ /\.local$/i,
18
+ /^localhost$/i,
19
+ /^127\./,
20
+ /^\[?::1\]?$/,
21
+ /^0\.0\.0\.0$/,
22
+ /^10\./,
23
+ /^192\.168\./,
24
+ ];
25
+ const isInternal = (host) => host === '' || INTERNAL.some((p) => p.test(host));
26
+ function tapEgress() {
27
+ tapFetch();
28
+ tapHttpModule('http');
29
+ tapHttpModule('https');
30
+ }
31
+ function tapFetch() {
32
+ const g = globalThis;
33
+ if (typeof g.fetch !== 'function' || g['__lensmcpFetchTap'])
34
+ return;
35
+ g['__lensmcpFetchTap'] = true;
36
+ const original = g.fetch.bind(globalThis);
37
+ g.fetch = (async (input, init) => {
38
+ let host = '';
39
+ try {
40
+ host = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url).hostname;
41
+ }
42
+ catch {
43
+ /* relative URL — internal by definition */
44
+ }
45
+ if (isInternal(host))
46
+ return original(input, init);
47
+ const t0 = Date.now();
48
+ const ctx = (0, flow_context_1.flowContext)(); // snapshot at the call site, before any await
49
+ const liveF = (0, flow_context_1.liveFlowContext)();
50
+ if (liveF)
51
+ liveF.externalCallCount = (liveF.externalCallCount ?? 0) + 1; // fan-out detector
52
+ const method = String(init?.method ?? 'GET').toUpperCase();
53
+ try {
54
+ const res = await original(input, init);
55
+ (0, emit_1.lensEmit)('external', 'info', `egress ${method} ${host} → ${res.status}`, {
56
+ name: host, kind: 'egress', method, host, status: res.status,
57
+ startedAt: t0, durationMs: Date.now() - t0,
58
+ }, undefined, ctx);
59
+ return res;
60
+ }
61
+ catch (err) {
62
+ (0, emit_1.lensEmit)('external', 'error', `egress ${method} ${host} failed`, {
63
+ name: host, kind: 'egress', method, host,
64
+ error: String(err?.message ?? err),
65
+ startedAt: t0, durationMs: Date.now() - t0,
66
+ }, undefined, ctx);
67
+ throw err;
68
+ }
69
+ });
70
+ }
71
+ function tapHttpModule(proto) {
72
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
73
+ const mod = require(`node:${proto}`);
74
+ const origRequest = mod['request'];
75
+ if (typeof origRequest !== 'function' || origRequest.__lensmcp)
76
+ return;
77
+ const wrapped = function (...args) {
78
+ const req = origRequest.apply(this, args);
79
+ try {
80
+ observe(args, req);
81
+ }
82
+ catch {
83
+ /* never break the call */
84
+ }
85
+ return req;
86
+ };
87
+ wrapped.__lensmcp = true;
88
+ mod['request'] = wrapped;
89
+ const origGet = mod['get'];
90
+ if (typeof origGet === 'function') {
91
+ mod['get'] = function (...args) {
92
+ const req = wrapped.apply(this, args);
93
+ req.end();
94
+ return req;
95
+ };
96
+ }
97
+ }
98
+ function observe(args, req) {
99
+ const { host, method } = parseTarget(args);
100
+ if (isInternal(host))
101
+ return;
102
+ const t0 = Date.now();
103
+ const ctx = (0, flow_context_1.flowContext)(); // call-site scope, not response-callback scope
104
+ const liveH = (0, flow_context_1.liveFlowContext)();
105
+ if (liveH)
106
+ liveH.externalCallCount = (liveH.externalCallCount ?? 0) + 1; // fan-out detector
107
+ let done = false;
108
+ req.once('response', (...resArgs) => {
109
+ if (done)
110
+ return;
111
+ done = true;
112
+ const res = resArgs[0];
113
+ (0, emit_1.lensEmit)('external', 'info', `egress ${method} ${host} → ${res?.statusCode ?? '?'}`, {
114
+ name: host, kind: 'egress', method, host,
115
+ ...(res?.statusCode != null ? { status: res.statusCode } : {}),
116
+ startedAt: t0, durationMs: Date.now() - t0,
117
+ }, undefined, ctx);
118
+ });
119
+ req.once('error', (...errArgs) => {
120
+ if (done)
121
+ return;
122
+ done = true;
123
+ const err = errArgs[0];
124
+ (0, emit_1.lensEmit)('external', 'error', `egress ${method} ${host} failed`, {
125
+ name: host, kind: 'egress', method, host,
126
+ error: String(err?.message ?? err), startedAt: t0, durationMs: Date.now() - t0,
127
+ }, undefined, ctx);
128
+ });
129
+ }
130
+ function parseTarget(args) {
131
+ let host = '';
132
+ let method = 'GET';
133
+ for (const a of args) {
134
+ if (typeof a === 'string') {
135
+ try {
136
+ host = new URL(a).hostname;
137
+ }
138
+ catch {
139
+ /* not a URL */
140
+ }
141
+ }
142
+ else if (a instanceof URL) {
143
+ host = a.hostname;
144
+ }
145
+ else if (a && typeof a === 'object') {
146
+ const o = a;
147
+ if (typeof o['method'] === 'string')
148
+ method = o['method'].toUpperCase();
149
+ if (typeof o['socketPath'] === 'string')
150
+ return { host: '', method }; // unix sock → internal
151
+ if (typeof o['hostname'] === 'string' && o['hostname'])
152
+ host = o['hostname'];
153
+ else if (typeof o['host'] === 'string' && o['host'])
154
+ host = o['host'].split(':')[0] ?? '';
155
+ }
156
+ }
157
+ return { host, method };
158
+ }
@@ -0,0 +1,2 @@
1
+ export declare function tapExec(): void;
2
+ //# sourceMappingURL=tap-exec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tap-exec.d.ts","sourceRoot":"","sources":["../../src/lib/tap-exec.ts"],"names":[],"mappings":"AA4DA,wBAAgB,OAAO,IAAI,IAAI,CA4D9B"}
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tapExec = tapExec;
4
+ /**
5
+ * child_process tap (source `exec`): every spawn/exec/fork a pod performs is
6
+ * a fact worth seeing — shell-outs are the least visible side effects a
7
+ * service has. Async forms emit ONE event at exit (cmd, code, duration);
8
+ * sync forms time the call. Internal Node refs aren't double-counted:
9
+ * `exec` → internal `execFile` → internal `spawn` bypasses the patched
10
+ * exports, so each public call emits exactly once.
11
+ */
12
+ const emit_1 = require("./emit");
13
+ const fmtCmd = (file, args) => {
14
+ const a = Array.isArray(args) ? args.map(String).slice(0, 8).join(' ') : '';
15
+ return `${String(file)}${a ? ' ' + a : ''}`.slice(0, 200);
16
+ };
17
+ function wrap(obj, key, make) {
18
+ const orig = obj[key];
19
+ if (typeof orig !== 'function' || orig.__lensmcp)
20
+ return;
21
+ const wrapped = make(orig);
22
+ wrapped.__lensmcp = true;
23
+ obj[key] = wrapped;
24
+ }
25
+ function observeChild(via, cmd, child) {
26
+ if (!child || typeof child.once !== 'function')
27
+ return;
28
+ // On current Node `exec()` dispatches to the module-export `execFile`, which
29
+ // dispatches to `spawn` — all three are wrapped, so the SAME child passes
30
+ // through multiple wrappers. Tag it once: the OUTERMOST public call wins
31
+ // (richest `via`/cmd), inner re-observations bail → exactly one event.
32
+ const c = child;
33
+ if (c.__lensmcpObserved)
34
+ return;
35
+ c.__lensmcpObserved = true;
36
+ const t0 = Date.now();
37
+ child.once('exit', (...args) => {
38
+ const code = args[0];
39
+ const signal = args[1];
40
+ (0, emit_1.lensEmit)('exec', code === 0 ? 'info' : 'error', `exec ${cmd.split(' ')[0]} → ${code ?? signal}`, {
41
+ name: 'exec', kind: 'exec', cmd, via,
42
+ ...(code != null ? { code } : {}),
43
+ ...(signal ? { signal } : {}),
44
+ durationMs: Date.now() - t0,
45
+ ...(child.pid ? { pid: child.pid } : {}),
46
+ });
47
+ });
48
+ child.once('error', (...args) => {
49
+ const err = args[0];
50
+ (0, emit_1.lensEmit)('exec', 'error', `exec ${cmd.split(' ')[0]} failed to start`, {
51
+ name: 'exec', kind: 'exec', cmd, via,
52
+ error: String(err?.message ?? err), durationMs: Date.now() - t0,
53
+ });
54
+ });
55
+ }
56
+ function tapExec() {
57
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
58
+ const cp = require('node:child_process');
59
+ for (const key of ['spawn', 'fork']) {
60
+ wrap(cp, key, (orig) => function (file, ...rest) {
61
+ const args = Array.isArray(rest[0]) ? rest[0] : [];
62
+ const child = orig.call(this, file, ...rest);
63
+ try {
64
+ observeChild(key, fmtCmd(file, args), child);
65
+ }
66
+ catch { /* never break the spawn */ }
67
+ return child;
68
+ });
69
+ }
70
+ wrap(cp, 'exec', (orig) => function (cmd, ...rest) {
71
+ const child = orig.call(this, cmd, ...rest);
72
+ try {
73
+ observeChild('exec', String(cmd).slice(0, 200), child);
74
+ }
75
+ catch { /* ignore */ }
76
+ return child;
77
+ });
78
+ wrap(cp, 'execFile', (orig) => function (file, ...rest) {
79
+ const args = Array.isArray(rest[0]) ? rest[0] : [];
80
+ const child = orig.call(this, file, ...rest);
81
+ try {
82
+ observeChild('execFile', fmtCmd(file, args), child);
83
+ }
84
+ catch { /* ignore */ }
85
+ return child;
86
+ });
87
+ for (const key of ['execSync', 'execFileSync', 'spawnSync']) {
88
+ wrap(cp, key, (orig) => function (file, ...rest) {
89
+ const args = Array.isArray(rest[0]) ? rest[0] : [];
90
+ const cmd = key === 'execSync' ? String(file).slice(0, 200) : fmtCmd(file, args);
91
+ const t0 = Date.now();
92
+ try {
93
+ const r = orig.call(this, file, ...rest);
94
+ const status = r && typeof r === 'object' && 'status' in r
95
+ ? (r.status ?? 0)
96
+ : 0;
97
+ (0, emit_1.lensEmit)('exec', status === 0 ? 'info' : 'error', `exec ${cmd.split(' ')[0]} → ${status}`, {
98
+ name: 'exec', kind: 'exec', cmd, via: key, code: status, durationMs: Date.now() - t0, sync: true,
99
+ });
100
+ return r;
101
+ }
102
+ catch (err) {
103
+ (0, emit_1.lensEmit)('exec', 'error', `exec ${cmd.split(' ')[0]} failed`, {
104
+ name: 'exec', kind: 'exec', cmd, via: key,
105
+ error: String(err?.message ?? err), durationMs: Date.now() - t0, sync: true,
106
+ });
107
+ throw err;
108
+ }
109
+ });
110
+ }
111
+ }
@@ -0,0 +1,2 @@
1
+ export declare function tapFs(): void;
2
+ //# sourceMappingURL=tap-fs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tap-fs.d.ts","sourceRoot":"","sources":["../../src/lib/tap-fs.ts"],"names":[],"mappings":"AA6FA,wBAAgB,KAAK,IAAI,IAAI,CA6E5B"}