everything-dev 0.1.2 → 0.1.4

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,305 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { Effect, Logger, LogLevel } from "effect";
3
+ import {
4
+ assertAllPortsFree,
5
+ assertAllPortsFreeWithPlatform,
6
+ assertCleanState,
7
+ assertCleanStateWithPlatform,
8
+ assertMemoryDelta,
9
+ assertNoLeaks,
10
+ assertNoOrphanProcesses,
11
+ assertProcessesDead,
12
+ } from "./assertions";
13
+ import { diffSnapshots, formatDiff, formatSnapshotSummary, hasLeaks } from "./diff";
14
+ import {
15
+ MemoryLimitExceeded,
16
+ MemoryPercentExceeded,
17
+ OrphanedProcesses,
18
+ PortStillBound,
19
+ ProcessesStillAlive,
20
+ ResourceLeaks,
21
+ } from "./errors";
22
+ import { PlatformLive, PlatformService, withPlatform } from "./platform";
23
+ import {
24
+ createSnapshot,
25
+ createSnapshotWithPlatform,
26
+ findBosProcesses,
27
+ isProcessAlive,
28
+ isProcessAliveSync,
29
+ waitForPortFree,
30
+ waitForPortFreeWithPlatform,
31
+ waitForProcessDeath,
32
+ } from "./snapshot";
33
+ import type {
34
+ MemoryInfo,
35
+ MonitorConfig,
36
+ PortInfo,
37
+ ProcessInfo,
38
+ Snapshot,
39
+ SnapshotDiff,
40
+ } from "./types";
41
+
42
+ export class ResourceMonitor {
43
+ private config: MonitorConfig;
44
+ private baseline: Snapshot | null = null;
45
+ private snapshots: Snapshot[] = [];
46
+
47
+ private constructor(config: MonitorConfig) {
48
+ this.config = config;
49
+ }
50
+
51
+ static create = (
52
+ config?: MonitorConfig
53
+ ): Effect.Effect<ResourceMonitor, never, PlatformService> =>
54
+ Effect.gen(function* () {
55
+ yield* Effect.logInfo("Creating ResourceMonitor instance");
56
+ return new ResourceMonitor(config || {});
57
+ });
58
+
59
+ static createWithPlatform = (
60
+ config?: MonitorConfig
61
+ ): Effect.Effect<ResourceMonitor> =>
62
+ withPlatform(ResourceMonitor.create(config));
63
+
64
+ snapshot(): Effect.Effect<Snapshot, never, PlatformService> {
65
+ const self = this;
66
+ return Effect.gen(function* () {
67
+ const snap = yield* createSnapshot(self.config);
68
+ self.snapshots.push(snap);
69
+ return snap;
70
+ });
71
+ }
72
+
73
+ snapshotWithPlatform(): Effect.Effect<Snapshot> {
74
+ return withPlatform(this.snapshot());
75
+ }
76
+
77
+ setBaseline(): Effect.Effect<Snapshot, never, PlatformService> {
78
+ const self = this;
79
+ return Effect.gen(function* () {
80
+ yield* Effect.logInfo("Setting baseline snapshot");
81
+ self.baseline = yield* self.snapshot();
82
+ return self.baseline;
83
+ });
84
+ }
85
+
86
+ setBaselineWithPlatform(): Effect.Effect<Snapshot> {
87
+ return withPlatform(this.setBaseline());
88
+ }
89
+
90
+ getBaseline(): Effect.Effect<Snapshot | null> {
91
+ return Effect.succeed(this.baseline);
92
+ }
93
+
94
+ clearBaseline(): Effect.Effect<void> {
95
+ return Effect.sync(() => {
96
+ this.baseline = null;
97
+ });
98
+ }
99
+
100
+ getSnapshots(): Effect.Effect<Snapshot[]> {
101
+ return Effect.succeed([...this.snapshots]);
102
+ }
103
+
104
+ clearSnapshots(): Effect.Effect<void> {
105
+ return Effect.sync(() => {
106
+ this.snapshots = [];
107
+ });
108
+ }
109
+
110
+ diff(from: Snapshot, to: Snapshot): Effect.Effect<SnapshotDiff> {
111
+ return Effect.sync(() => diffSnapshots(from, to));
112
+ }
113
+
114
+ diffFromBaseline(to: Snapshot): Effect.Effect<SnapshotDiff | null> {
115
+ return Effect.sync(() => {
116
+ if (!this.baseline) return null;
117
+ return diffSnapshots(this.baseline, to);
118
+ });
119
+ }
120
+
121
+ hasLeaks(diff: SnapshotDiff): Effect.Effect<boolean> {
122
+ return Effect.sync(() => hasLeaks(diff));
123
+ }
124
+
125
+ assertAllPortsFree(
126
+ ports?: number[]
127
+ ): Effect.Effect<void, PortStillBound, PlatformService> {
128
+ const portsToCheck =
129
+ ports ||
130
+ Object.keys(this.baseline?.ports || {}).map((p) => parseInt(p, 10));
131
+ return assertAllPortsFree(portsToCheck);
132
+ }
133
+
134
+ assertAllPortsFreeWithPlatform(
135
+ ports?: number[]
136
+ ): Effect.Effect<void, PortStillBound> {
137
+ const portsToCheck =
138
+ ports ||
139
+ Object.keys(this.baseline?.ports || {}).map((p) => parseInt(p, 10));
140
+ return assertAllPortsFreeWithPlatform(portsToCheck);
141
+ }
142
+
143
+ assertNoOrphanProcesses(
144
+ running: Snapshot,
145
+ after: Snapshot
146
+ ): Effect.Effect<void, OrphanedProcesses> {
147
+ return assertNoOrphanProcesses(running, after);
148
+ }
149
+
150
+ assertMemoryDelta(
151
+ baseline: Snapshot,
152
+ after: Snapshot,
153
+ options: { maxDeltaMB?: number; maxDeltaPercent?: number }
154
+ ): Effect.Effect<void, MemoryLimitExceeded | MemoryPercentExceeded> {
155
+ return assertMemoryDelta(baseline, after, options);
156
+ }
157
+
158
+ assertProcessesDead(pids: number[]): Effect.Effect<void, ProcessesStillAlive> {
159
+ return assertProcessesDead(pids);
160
+ }
161
+
162
+ assertNoLeaks(diff: SnapshotDiff): Effect.Effect<void, ResourceLeaks> {
163
+ return assertNoLeaks(diff);
164
+ }
165
+
166
+ assertCleanState(
167
+ running: Snapshot
168
+ ): Effect.Effect<void, PortStillBound | ProcessesStillAlive, PlatformService> {
169
+ return assertCleanState(running);
170
+ }
171
+
172
+ assertCleanStateWithPlatform(
173
+ running: Snapshot
174
+ ): Effect.Effect<void, PortStillBound | ProcessesStillAlive> {
175
+ return assertCleanStateWithPlatform(running);
176
+ }
177
+
178
+ waitForPortFree(
179
+ port: number,
180
+ timeoutMs?: number
181
+ ): Effect.Effect<boolean, never, PlatformService> {
182
+ return waitForPortFree(port, timeoutMs);
183
+ }
184
+
185
+ waitForPortFreeWithPlatform(
186
+ port: number,
187
+ timeoutMs?: number
188
+ ): Effect.Effect<boolean> {
189
+ return waitForPortFreeWithPlatform(port, timeoutMs);
190
+ }
191
+
192
+ waitForProcessDeath(pid: number, timeoutMs?: number): Effect.Effect<boolean> {
193
+ return waitForProcessDeath(pid, timeoutMs);
194
+ }
195
+
196
+ formatSnapshot(snapshot: Snapshot): Effect.Effect<string> {
197
+ return Effect.sync(() => formatSnapshotSummary(snapshot));
198
+ }
199
+
200
+ formatDiff(diff: SnapshotDiff): Effect.Effect<string> {
201
+ return Effect.sync(() => formatDiff(diff));
202
+ }
203
+
204
+ export(filepath: string): Effect.Effect<void> {
205
+ const self = this;
206
+ return Effect.gen(function* () {
207
+ yield* Effect.logInfo(`Exporting monitor data to ${filepath}`);
208
+
209
+ const data = {
210
+ config: self.config,
211
+ baseline: self.baseline,
212
+ snapshots: self.snapshots,
213
+ exportedAt: new Date().toISOString(),
214
+ platform: process.platform,
215
+ };
216
+
217
+ yield* Effect.tryPromise({
218
+ try: () => writeFile(filepath, JSON.stringify(data, null, 2)),
219
+ catch: (e) => new Error(`Failed to export: ${e}`),
220
+ });
221
+
222
+ yield* Effect.logInfo(`Exported ${self.snapshots.length} snapshots`);
223
+ }).pipe(Effect.catchAll(() => Effect.void));
224
+ }
225
+
226
+ toJSON(): Effect.Effect<{
227
+ config: MonitorConfig;
228
+ baseline: Snapshot | null;
229
+ snapshots: Snapshot[];
230
+ }> {
231
+ return Effect.succeed({
232
+ config: this.config,
233
+ baseline: this.baseline,
234
+ snapshots: this.snapshots,
235
+ });
236
+ }
237
+ }
238
+
239
+ export const runWithLogging = <A, E>(
240
+ effect: Effect.Effect<A, E, PlatformService>
241
+ ): Promise<A> =>
242
+ effect.pipe(
243
+ Effect.provide(PlatformLive),
244
+ Logger.withMinimumLogLevel(LogLevel.Debug),
245
+ Effect.runPromise
246
+ );
247
+
248
+ export const runWithInfo = <A, E>(
249
+ effect: Effect.Effect<A, E, PlatformService>
250
+ ): Promise<A> =>
251
+ effect.pipe(
252
+ Effect.provide(PlatformLive),
253
+ Logger.withMinimumLogLevel(LogLevel.Info),
254
+ Effect.runPromise
255
+ );
256
+
257
+ export const runSilent = <A, E>(
258
+ effect: Effect.Effect<A, E, PlatformService>
259
+ ): Promise<A> =>
260
+ effect.pipe(
261
+ Effect.provide(PlatformLive),
262
+ Logger.withMinimumLogLevel(LogLevel.Error),
263
+ Effect.runPromise
264
+ );
265
+
266
+ export {
267
+ assertAllPortsFree,
268
+ assertAllPortsFreeWithPlatform,
269
+ assertCleanState,
270
+ assertCleanStateWithPlatform,
271
+ assertMemoryDelta,
272
+ assertNoLeaks,
273
+ assertNoOrphanProcesses,
274
+ assertProcessesDead,
275
+ createSnapshot,
276
+ createSnapshotWithPlatform,
277
+ diffSnapshots,
278
+ findBosProcesses,
279
+ formatDiff,
280
+ formatSnapshotSummary,
281
+ hasLeaks,
282
+ isProcessAlive,
283
+ isProcessAliveSync,
284
+ MemoryLimitExceeded,
285
+ MemoryPercentExceeded,
286
+ OrphanedProcesses,
287
+ PlatformLive,
288
+ PlatformService,
289
+ PortStillBound,
290
+ ProcessesStillAlive,
291
+ ResourceLeaks,
292
+ waitForPortFree,
293
+ waitForPortFreeWithPlatform,
294
+ waitForProcessDeath,
295
+ withPlatform,
296
+ };
297
+
298
+ export type {
299
+ MemoryInfo,
300
+ MonitorConfig,
301
+ PortInfo,
302
+ ProcessInfo,
303
+ Snapshot,
304
+ SnapshotDiff,
305
+ };
@@ -0,0 +1,293 @@
1
+ import { Effect, Layer } from "effect";
2
+ import { execShellSafe } from "../command";
3
+ import type {
4
+ MemoryInfo,
5
+ PlatformOperations,
6
+ PortInfo,
7
+ ProcessInfo,
8
+ } from "../types";
9
+ import { PlatformService } from "../types";
10
+
11
+ const parseLsofLine = (
12
+ line: string,
13
+ ports: number[]
14
+ ): { port: number; pid: number; command: string } | null => {
15
+ const parts = line.split(/\s+/);
16
+ if (parts.length < 9) return null;
17
+
18
+ const command = parts[0];
19
+ const pid = parseInt(parts[1], 10);
20
+ const nameCol = parts[8] || parts[7];
21
+
22
+ const portMatch = nameCol.match(/:(\d+)$/);
23
+ if (!portMatch) return null;
24
+
25
+ const port = parseInt(portMatch[1], 10);
26
+ if (!ports.includes(port)) return null;
27
+
28
+ return { port, pid, command };
29
+ };
30
+
31
+ const getPortInfo = (
32
+ ports: number[]
33
+ ): Effect.Effect<Record<number, PortInfo>, never> =>
34
+ Effect.gen(function* () {
35
+ yield* Effect.logInfo(`[darwin] Checking ${ports.length} ports`);
36
+
37
+ const result: Record<number, PortInfo> = {};
38
+ for (const port of ports) {
39
+ result[port] = { port, pid: null, command: null, state: "FREE" };
40
+ }
41
+
42
+ if (ports.length === 0) return result;
43
+
44
+ const output = yield* execShellSafe(
45
+ "lsof -i -P -n -sTCP:LISTEN 2>/dev/null || true"
46
+ );
47
+ if (!output) {
48
+ yield* Effect.logDebug("[darwin] No lsof output, all ports appear free");
49
+ return result;
50
+ }
51
+
52
+ const lines = output.split("\n").filter(Boolean);
53
+ yield* Effect.logDebug(`[darwin] Parsing ${lines.length} lsof lines`);
54
+
55
+ for (const line of lines.slice(1)) {
56
+ const parsed = parseLsofLine(line, ports);
57
+ if (parsed) {
58
+ result[parsed.port] = {
59
+ port: parsed.port,
60
+ pid: parsed.pid,
61
+ command: parsed.command,
62
+ state: "LISTEN",
63
+ };
64
+ yield* Effect.logDebug(
65
+ `[darwin] Port :${parsed.port} bound to PID ${parsed.pid} (${parsed.command})`
66
+ );
67
+ }
68
+ }
69
+
70
+ const boundCount = Object.values(result).filter(
71
+ (p) => p.state === "LISTEN"
72
+ ).length;
73
+ yield* Effect.logInfo(
74
+ `[darwin] Found ${boundCount}/${ports.length} ports in use`
75
+ );
76
+
77
+ return result;
78
+ });
79
+
80
+ const getProcessTree = (
81
+ rootPids: number[]
82
+ ): Effect.Effect<ProcessInfo[], never> =>
83
+ Effect.gen(function* () {
84
+ yield* Effect.logInfo(
85
+ `[darwin] Building process tree for ${rootPids.length} root PIDs`
86
+ );
87
+
88
+ const processes: ProcessInfo[] = [];
89
+ const visited = new Set<number>();
90
+
91
+ const getProcess = (
92
+ pid: number
93
+ ): Effect.Effect<ProcessInfo | null, never> =>
94
+ Effect.gen(function* () {
95
+ if (visited.has(pid)) return null;
96
+ visited.add(pid);
97
+
98
+ const output = yield* execShellSafe(
99
+ `ps -p ${pid} -o pid=,ppid=,rss=,comm=,args= 2>/dev/null || true`
100
+ );
101
+ if (!output) return null;
102
+
103
+ const parts = output.trim().split(/\s+/);
104
+ if (parts.length < 4) return null;
105
+
106
+ const [pidStr, ppidStr, rssStr, ...rest] = parts;
107
+ const command = rest[0] || "";
108
+ const args = rest.slice(1);
109
+
110
+ yield* Effect.logDebug(
111
+ `[darwin] Process ${pid}: ${command} (RSS: ${rssStr}KB)`
112
+ );
113
+
114
+ return {
115
+ pid: parseInt(pidStr, 10),
116
+ ppid: parseInt(ppidStr, 10),
117
+ command,
118
+ args,
119
+ rss: parseInt(rssStr, 10) * 1024,
120
+ children: [],
121
+ };
122
+ });
123
+
124
+ const getChildren = (pid: number): Effect.Effect<number[], never> =>
125
+ Effect.gen(function* () {
126
+ const output = yield* execShellSafe(
127
+ `pgrep -P ${pid} 2>/dev/null || true`
128
+ );
129
+ if (!output) return [];
130
+
131
+ const children = output
132
+ .split("\n")
133
+ .filter(Boolean)
134
+ .map((s) => parseInt(s, 10))
135
+ .filter((n) => !isNaN(n));
136
+
137
+ if (children.length > 0) {
138
+ yield* Effect.logDebug(
139
+ `[darwin] PID ${pid} has ${children.length} children: ${children.join(", ")}`
140
+ );
141
+ }
142
+
143
+ return children;
144
+ });
145
+
146
+ const traverse = (pid: number): Effect.Effect<void, never> =>
147
+ Effect.gen(function* () {
148
+ const proc = yield* getProcess(pid);
149
+ if (!proc) return;
150
+
151
+ const children = yield* getChildren(pid);
152
+ proc.children = children;
153
+ processes.push(proc);
154
+
155
+ for (const child of children) {
156
+ yield* traverse(child);
157
+ }
158
+ });
159
+
160
+ for (const pid of rootPids) {
161
+ yield* traverse(pid);
162
+ }
163
+
164
+ yield* Effect.logInfo(
165
+ `[darwin] Process tree contains ${processes.length} processes`
166
+ );
167
+
168
+ return processes;
169
+ });
170
+
171
+ const getMemoryInfo = (): Effect.Effect<MemoryInfo, never> =>
172
+ Effect.gen(function* () {
173
+ yield* Effect.logDebug("[darwin] Getting memory info");
174
+
175
+ const vmStat = yield* execShellSafe("vm_stat 2>/dev/null || true");
176
+ const pageSize = 16384;
177
+
178
+ let free = 0;
179
+ let active = 0;
180
+ let inactive = 0;
181
+ let wired = 0;
182
+ let speculative = 0;
183
+
184
+ for (const line of vmStat.split("\n")) {
185
+ const match = line.match(/^(.+):\s+(\d+)/);
186
+ if (!match) continue;
187
+
188
+ const [, key, value] = match;
189
+ const pages = parseInt(value, 10);
190
+
191
+ if (key.includes("free")) free = pages * pageSize;
192
+ else if (key.includes("active")) active = pages * pageSize;
193
+ else if (key.includes("inactive")) inactive = pages * pageSize;
194
+ else if (key.includes("wired")) wired = pages * pageSize;
195
+ else if (key.includes("speculative")) speculative = pages * pageSize;
196
+ }
197
+
198
+ const sysctlMem = yield* execShellSafe(
199
+ "sysctl -n hw.memsize 2>/dev/null || echo 0"
200
+ );
201
+ const total = parseInt(sysctlMem, 10) || 16 * 1024 * 1024 * 1024;
202
+
203
+ const used = active + inactive + wired + speculative;
204
+
205
+ const totalMB = (total / 1024 / 1024).toFixed(0);
206
+ const usedMB = (used / 1024 / 1024).toFixed(0);
207
+ yield* Effect.logDebug(
208
+ `[darwin] Memory: ${usedMB}MB used / ${totalMB}MB total`
209
+ );
210
+
211
+ return {
212
+ total,
213
+ used,
214
+ free: total - used,
215
+ processRss: 0,
216
+ };
217
+ });
218
+
219
+ const getAllProcesses = (): Effect.Effect<ProcessInfo[], never> =>
220
+ Effect.gen(function* () {
221
+ yield* Effect.logDebug("[darwin] Getting all processes");
222
+
223
+ const processes: ProcessInfo[] = [];
224
+
225
+ const output = yield* execShellSafe(
226
+ "ps -axo pid=,ppid=,rss=,comm= 2>/dev/null || true"
227
+ );
228
+ for (const line of output.split("\n").filter(Boolean)) {
229
+ const parts = line.trim().split(/\s+/);
230
+ if (parts.length < 4) continue;
231
+
232
+ const [pidStr, ppidStr, rssStr, ...rest] = parts;
233
+ processes.push({
234
+ pid: parseInt(pidStr, 10),
235
+ ppid: parseInt(ppidStr, 10),
236
+ command: rest.join(" "),
237
+ args: [],
238
+ rss: parseInt(rssStr, 10) * 1024,
239
+ children: [],
240
+ });
241
+ }
242
+
243
+ yield* Effect.logDebug(`[darwin] Found ${processes.length} total processes`);
244
+
245
+ return processes;
246
+ });
247
+
248
+ const findChildProcesses = (pid: number): Effect.Effect<number[], never> =>
249
+ Effect.gen(function* () {
250
+ yield* Effect.logDebug(`[darwin] Finding all children of PID ${pid}`);
251
+
252
+ const children: number[] = [];
253
+ const visited = new Set<number>();
254
+
255
+ const recurse = (parentPid: number): Effect.Effect<void, never> =>
256
+ Effect.gen(function* () {
257
+ if (visited.has(parentPid)) return;
258
+ visited.add(parentPid);
259
+
260
+ const output = yield* execShellSafe(
261
+ `pgrep -P ${parentPid} 2>/dev/null || true`
262
+ );
263
+ if (!output) return;
264
+
265
+ for (const line of output.split("\n").filter(Boolean)) {
266
+ const childPid = parseInt(line, 10);
267
+ if (!isNaN(childPid)) {
268
+ children.push(childPid);
269
+ yield* recurse(childPid);
270
+ }
271
+ }
272
+ });
273
+
274
+ yield* recurse(pid);
275
+
276
+ if (children.length > 0) {
277
+ yield* Effect.logDebug(
278
+ `[darwin] PID ${pid} has ${children.length} descendants`
279
+ );
280
+ }
281
+
282
+ return children;
283
+ });
284
+
285
+ const darwinOperations: PlatformOperations = {
286
+ getPortInfo,
287
+ getProcessTree,
288
+ getMemoryInfo,
289
+ getAllProcesses,
290
+ findChildProcesses,
291
+ };
292
+
293
+ export const DarwinLayer = Layer.succeed(PlatformService, darwinOperations);
@@ -0,0 +1,35 @@
1
+ import { Effect, Layer } from "effect";
2
+ import { PlatformService } from "../types";
3
+ import { DarwinLayer } from "./darwin";
4
+ import { LinuxLayer } from "./linux";
5
+ import { WindowsLayer } from "./windows";
6
+
7
+ export const makePlatformLayer = (): Layer.Layer<PlatformService> => {
8
+ const platform = process.platform;
9
+
10
+ switch (platform) {
11
+ case "darwin":
12
+ return DarwinLayer;
13
+ case "linux":
14
+ return LinuxLayer;
15
+ case "win32":
16
+ return WindowsLayer;
17
+ default:
18
+ console.warn(`Unsupported platform: ${platform}, falling back to linux`);
19
+ return LinuxLayer;
20
+ }
21
+ };
22
+
23
+ export const PlatformLive = makePlatformLayer();
24
+
25
+ export const withPlatform = <A, E, R>(
26
+ effect: Effect.Effect<A, E, R | PlatformService>
27
+ ): Effect.Effect<A, E, Exclude<R, PlatformService>> =>
28
+ effect.pipe(Effect.provide(PlatformLive)) as Effect.Effect<
29
+ A,
30
+ E,
31
+ Exclude<R, PlatformService>
32
+ >;
33
+
34
+ export { DarwinLayer, LinuxLayer, WindowsLayer };
35
+ export { PlatformService };