everything-dev 0.1.3 → 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,143 @@
1
+ import { isProcessAliveSync } from "./snapshot";
2
+ import type { PortInfo, Snapshot, SnapshotDiff } from "./types";
3
+
4
+ export const diffSnapshots = (from: Snapshot, to: Snapshot): SnapshotDiff => {
5
+ const fromPids = new Set(from.processes.map((p) => p.pid));
6
+ const toPids = new Set(to.processes.map((p) => p.pid));
7
+
8
+ const stillBoundPorts: PortInfo[] = [];
9
+ const freedPorts: number[] = [];
10
+
11
+ for (const [portStr, fromPort] of Object.entries(from.ports)) {
12
+ const port = parseInt(portStr, 10);
13
+ const toPort = to.ports[port];
14
+
15
+ if (fromPort.state === "LISTEN") {
16
+ if (toPort?.state === "LISTEN") {
17
+ stillBoundPorts.push(toPort);
18
+ } else {
19
+ freedPorts.push(port);
20
+ }
21
+ }
22
+ }
23
+
24
+ const stillBoundPids = new Set(
25
+ stillBoundPorts.map((p) => p.pid).filter((pid): pid is number => pid !== null)
26
+ );
27
+
28
+ const orphanedProcesses = from.processes.filter(
29
+ (p) =>
30
+ fromPids.has(p.pid) &&
31
+ !toPids.has(p.pid) &&
32
+ isProcessAliveSync(p.pid) &&
33
+ stillBoundPids.has(p.pid)
34
+ );
35
+
36
+ const newProcesses = to.processes.filter((p) => !fromPids.has(p.pid));
37
+ const killedProcesses = from.processes.filter((p) => !toPids.has(p.pid));
38
+
39
+ const memoryDeltaBytes = to.memory.processRss - from.memory.processRss;
40
+
41
+ return {
42
+ from,
43
+ to,
44
+ orphanedProcesses,
45
+ stillBoundPorts,
46
+ freedPorts,
47
+ memoryDeltaBytes,
48
+ newProcesses,
49
+ killedProcesses,
50
+ };
51
+ };
52
+
53
+ export const hasLeaks = (diff: SnapshotDiff): boolean => {
54
+ return diff.orphanedProcesses.length > 0 || diff.stillBoundPorts.length > 0;
55
+ };
56
+
57
+ export const formatDiff = (diff: SnapshotDiff): string => {
58
+ const lines: string[] = [];
59
+ const elapsed = diff.to.timestamp - diff.from.timestamp;
60
+
61
+ lines.push(`Snapshot Diff (${elapsed}ms elapsed)`);
62
+ lines.push("─".repeat(50));
63
+
64
+ if (diff.stillBoundPorts.length > 0) {
65
+ lines.push("");
66
+ lines.push("⚠️ STILL BOUND PORTS:");
67
+ for (const port of diff.stillBoundPorts) {
68
+ lines.push(` :${port.port} ← PID ${port.pid} (${port.command})`);
69
+ }
70
+ }
71
+
72
+ if (diff.orphanedProcesses.length > 0) {
73
+ lines.push("");
74
+ lines.push("⚠️ ORPHANED PROCESSES:");
75
+ for (const proc of diff.orphanedProcesses) {
76
+ const mb = (proc.rss / 1024 / 1024).toFixed(1);
77
+ lines.push(` PID ${proc.pid}: ${proc.command} (${mb} MB)`);
78
+ }
79
+ }
80
+
81
+ if (diff.freedPorts.length > 0) {
82
+ lines.push("");
83
+ lines.push("✓ FREED PORTS:");
84
+ lines.push(` ${diff.freedPorts.join(", ")}`);
85
+ }
86
+
87
+ if (diff.killedProcesses.length > 0) {
88
+ lines.push("");
89
+ lines.push("✓ KILLED PROCESSES:");
90
+ for (const proc of diff.killedProcesses) {
91
+ lines.push(` PID ${proc.pid}: ${proc.command}`);
92
+ }
93
+ }
94
+
95
+ lines.push("");
96
+ const memDeltaMb = (diff.memoryDeltaBytes / 1024 / 1024).toFixed(1);
97
+ const sign = diff.memoryDeltaBytes >= 0 ? "+" : "";
98
+ lines.push(`Memory Delta: ${sign}${memDeltaMb} MB`);
99
+
100
+ if (!hasLeaks(diff)) {
101
+ lines.push("");
102
+ lines.push("✅ No resource leaks detected");
103
+ }
104
+
105
+ return lines.join("\n");
106
+ };
107
+
108
+ export const formatSnapshotSummary = (snapshot: Snapshot): string => {
109
+ const lines: string[] = [];
110
+
111
+ lines.push(`Snapshot at ${new Date(snapshot.timestamp).toISOString()}`);
112
+ lines.push(`Platform: ${snapshot.platform}`);
113
+ if (snapshot.configPath) {
114
+ lines.push(`Config: ${snapshot.configPath}`);
115
+ }
116
+ lines.push("");
117
+
118
+ lines.push("PORTS:");
119
+ for (const [port, info] of Object.entries(snapshot.ports)) {
120
+ if (info.state === "FREE") {
121
+ lines.push(` :${port} ○ free`);
122
+ } else {
123
+ lines.push(` :${port} ● PID ${info.pid} (${info.command})`);
124
+ }
125
+ }
126
+
127
+ if (snapshot.processes.length > 0) {
128
+ lines.push("");
129
+ lines.push("PROCESS TREE:");
130
+ for (const proc of snapshot.processes) {
131
+ const mb = (proc.rss / 1024 / 1024).toFixed(1);
132
+ const childCount = proc.children.length;
133
+ const childText = childCount > 0 ? ` [${childCount} children]` : "";
134
+ lines.push(` ${proc.pid} ${proc.command} (${mb} MB)${childText}`);
135
+ }
136
+ }
137
+
138
+ lines.push("");
139
+ const totalMb = (snapshot.memory.processRss / 1024 / 1024).toFixed(1);
140
+ lines.push(`Total Process RSS: ${totalMb} MB`);
141
+
142
+ return lines.join("\n");
143
+ };
@@ -0,0 +1,127 @@
1
+ import { Data } from "effect";
2
+ import type { PortInfo, ProcessInfo } from "./types";
3
+
4
+ export class CommandFailed extends Data.TaggedError("CommandFailed")<{
5
+ readonly command: string;
6
+ readonly args: string[];
7
+ readonly exitCode: number;
8
+ readonly stderr: string;
9
+ }> {
10
+ get message() {
11
+ return `Command '${this.command} ${this.args.join(" ")}' failed with exit code ${this.exitCode}: ${this.stderr}`;
12
+ }
13
+ }
14
+
15
+ export class CommandTimeout extends Data.TaggedError("CommandTimeout")<{
16
+ readonly command: string;
17
+ readonly timeoutMs: number;
18
+ }> {
19
+ get message() {
20
+ return `Command '${this.command}' timed out after ${this.timeoutMs}ms`;
21
+ }
22
+ }
23
+
24
+ export class ParseError extends Data.TaggedError("ParseError")<{
25
+ readonly source: string;
26
+ readonly raw: string;
27
+ readonly reason: string;
28
+ }> {
29
+ get message() {
30
+ return `Failed to parse ${this.source}: ${this.reason}`;
31
+ }
32
+ }
33
+
34
+ export class PortStillBound extends Data.TaggedError("PortStillBound")<{
35
+ readonly ports: Array<{ port: number; pid: number | null; command: string | null }>;
36
+ }> {
37
+ get message() {
38
+ const portList = this.ports.map((p) => `:${p.port} (PID ${p.pid})`).join(", ");
39
+ return `Expected ports to be free, but still bound: ${portList}`;
40
+ }
41
+ }
42
+
43
+ export class OrphanedProcesses extends Data.TaggedError("OrphanedProcesses")<{
44
+ readonly processes: Array<{ pid: number; command: string; rss: number }>;
45
+ }> {
46
+ get message() {
47
+ return `Found ${this.processes.length} orphaned processes still running`;
48
+ }
49
+ }
50
+
51
+ export class MemoryLimitExceeded extends Data.TaggedError("MemoryLimitExceeded")<{
52
+ readonly deltaMB: number;
53
+ readonly limitMB: number;
54
+ readonly baselineRss: number;
55
+ readonly afterRss: number;
56
+ }> {
57
+ get message() {
58
+ return `Memory delta ${this.deltaMB.toFixed(1)} MB exceeds limit ${this.limitMB} MB`;
59
+ }
60
+ }
61
+
62
+ export class MemoryPercentExceeded extends Data.TaggedError("MemoryPercentExceeded")<{
63
+ readonly deltaPercent: number;
64
+ readonly limitPercent: number;
65
+ readonly baselineRss: number;
66
+ readonly afterRss: number;
67
+ }> {
68
+ get message() {
69
+ return `Memory delta ${this.deltaPercent.toFixed(1)}% exceeds limit ${this.limitPercent}%`;
70
+ }
71
+ }
72
+
73
+ export class ProcessesStillAlive extends Data.TaggedError("ProcessesStillAlive")<{
74
+ readonly pids: number[];
75
+ }> {
76
+ get message() {
77
+ return `Expected processes to be dead, but ${this.pids.length} still alive: ${this.pids.join(", ")}`;
78
+ }
79
+ }
80
+
81
+ export class ResourceLeaks extends Data.TaggedError("ResourceLeaks")<{
82
+ readonly orphanedProcesses: ProcessInfo[];
83
+ readonly stillBoundPorts: PortInfo[];
84
+ }> {
85
+ get message() {
86
+ const parts: string[] = [];
87
+ if (this.orphanedProcesses.length > 0) {
88
+ parts.push(`${this.orphanedProcesses.length} orphaned processes`);
89
+ }
90
+ if (this.stillBoundPorts.length > 0) {
91
+ parts.push(`${this.stillBoundPorts.length} ports still bound`);
92
+ }
93
+ return `Resource leaks detected: ${parts.join(", ")}`;
94
+ }
95
+ }
96
+
97
+ export class ConfigNotFound extends Data.TaggedError("ConfigNotFound")<{
98
+ readonly path: string | undefined;
99
+ }> {
100
+ get message() {
101
+ return this.path
102
+ ? `Config not found at ${this.path}`
103
+ : `No bos.config.json found in project`;
104
+ }
105
+ }
106
+
107
+ export class FileReadError extends Data.TaggedError("FileReadError")<{
108
+ readonly path: string;
109
+ readonly reason: string;
110
+ }> {
111
+ get message() {
112
+ return `Failed to read ${this.path}: ${this.reason}`;
113
+ }
114
+ }
115
+
116
+ export type MonitorError =
117
+ | CommandFailed
118
+ | CommandTimeout
119
+ | ParseError
120
+ | PortStillBound
121
+ | OrphanedProcesses
122
+ | MemoryLimitExceeded
123
+ | MemoryPercentExceeded
124
+ | ProcessesStillAlive
125
+ | ResourceLeaks
126
+ | ConfigNotFound
127
+ | FileReadError;
@@ -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
+ };