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,283 @@
1
+ import { Effect, Schedule } from "effect";
2
+ import { execa } from "execa";
3
+ import { CommandFailed, CommandTimeout } from "./errors";
4
+
5
+ export interface ExecOptions {
6
+ cwd?: string;
7
+ timeout?: number;
8
+ retries?: number;
9
+ silent?: boolean;
10
+ }
11
+
12
+ const DEFAULT_TIMEOUT = 10000;
13
+ const DEFAULT_RETRIES = 1;
14
+
15
+ export const execCommand = (
16
+ cmd: string,
17
+ args: string[],
18
+ options?: ExecOptions
19
+ ): Effect.Effect<string, CommandFailed | CommandTimeout> =>
20
+ Effect.gen(function* () {
21
+ const timeout = options?.timeout ?? DEFAULT_TIMEOUT;
22
+ const silent = options?.silent ?? false;
23
+
24
+ if (!silent) {
25
+ yield* Effect.logDebug(`Executing: ${cmd} ${args.join(" ")}`);
26
+ }
27
+
28
+ const result = yield* Effect.tryPromise({
29
+ try: async () => {
30
+ const proc = await execa(cmd, args, {
31
+ cwd: options?.cwd,
32
+ timeout,
33
+ reject: false,
34
+ shell: true,
35
+ });
36
+ return {
37
+ stdout: proc.stdout?.trim() ?? "",
38
+ stderr: proc.stderr?.trim() ?? "",
39
+ exitCode: proc.exitCode,
40
+ timedOut: proc.timedOut,
41
+ };
42
+ },
43
+ catch: (error) => {
44
+ const err = error as { timedOut?: boolean; message?: string };
45
+ if (err.timedOut) {
46
+ return new CommandTimeout({ command: cmd, timeoutMs: timeout });
47
+ }
48
+ return new CommandFailed({
49
+ command: cmd,
50
+ args,
51
+ exitCode: -1,
52
+ stderr: String(error),
53
+ });
54
+ },
55
+ });
56
+
57
+ if (result.timedOut) {
58
+ return yield* Effect.fail(
59
+ new CommandTimeout({ command: cmd, timeoutMs: timeout })
60
+ );
61
+ }
62
+
63
+ if (result.exitCode !== 0) {
64
+ if (!silent) {
65
+ yield* Effect.logWarning(
66
+ `Command failed: ${cmd} (exit ${result.exitCode})`
67
+ );
68
+ }
69
+ return yield* Effect.fail(
70
+ new CommandFailed({
71
+ command: cmd,
72
+ args,
73
+ exitCode: result.exitCode ?? 1,
74
+ stderr: result.stderr,
75
+ })
76
+ );
77
+ }
78
+
79
+ if (!silent) {
80
+ yield* Effect.logDebug(`Command succeeded: ${cmd}`);
81
+ }
82
+
83
+ return result.stdout;
84
+ }).pipe(
85
+ Effect.retry(
86
+ Schedule.recurs(options?.retries ?? DEFAULT_RETRIES).pipe(
87
+ Schedule.addDelay(() => "100 millis")
88
+ )
89
+ ),
90
+ Effect.catchAll((error) => Effect.fail(error))
91
+ );
92
+
93
+ export const execCommandSafe = (
94
+ cmd: string,
95
+ args: string[],
96
+ options?: ExecOptions
97
+ ): Effect.Effect<string, never> =>
98
+ execCommand(cmd, args, options).pipe(
99
+ Effect.catchAll((error) =>
100
+ Effect.gen(function* () {
101
+ yield* Effect.logWarning(`Command failed (graceful): ${error.message}`);
102
+ return "";
103
+ })
104
+ )
105
+ );
106
+
107
+ export const execShell = (
108
+ script: string,
109
+ options?: ExecOptions
110
+ ): Effect.Effect<string, CommandFailed | CommandTimeout> =>
111
+ Effect.gen(function* () {
112
+ const timeout = options?.timeout ?? DEFAULT_TIMEOUT;
113
+ const silent = options?.silent ?? false;
114
+
115
+ if (!silent) {
116
+ yield* Effect.logDebug(`Executing shell: ${script.slice(0, 50)}...`);
117
+ }
118
+
119
+ const result = yield* Effect.tryPromise({
120
+ try: async () => {
121
+ const proc = await execa(script, {
122
+ cwd: options?.cwd,
123
+ timeout,
124
+ reject: false,
125
+ shell: true,
126
+ });
127
+ return {
128
+ stdout: proc.stdout?.trim() ?? "",
129
+ stderr: proc.stderr?.trim() ?? "",
130
+ exitCode: proc.exitCode,
131
+ timedOut: proc.timedOut,
132
+ };
133
+ },
134
+ catch: (error) => {
135
+ const err = error as { timedOut?: boolean; message?: string };
136
+ if (err.timedOut) {
137
+ return new CommandTimeout({ command: script, timeoutMs: timeout });
138
+ }
139
+ return new CommandFailed({
140
+ command: script,
141
+ args: [],
142
+ exitCode: -1,
143
+ stderr: String(error),
144
+ });
145
+ },
146
+ });
147
+
148
+ if (result.timedOut) {
149
+ return yield* Effect.fail(
150
+ new CommandTimeout({ command: script, timeoutMs: timeout })
151
+ );
152
+ }
153
+
154
+ if (result.exitCode !== 0) {
155
+ if (!silent) {
156
+ yield* Effect.logWarning(
157
+ `Shell command failed (exit ${result.exitCode})`
158
+ );
159
+ }
160
+ return yield* Effect.fail(
161
+ new CommandFailed({
162
+ command: script,
163
+ args: [],
164
+ exitCode: result.exitCode ?? 1,
165
+ stderr: result.stderr,
166
+ })
167
+ );
168
+ }
169
+
170
+ return result.stdout;
171
+ }).pipe(
172
+ Effect.retry(
173
+ Schedule.recurs(options?.retries ?? DEFAULT_RETRIES).pipe(
174
+ Schedule.addDelay(() => "100 millis")
175
+ )
176
+ ),
177
+ Effect.catchAll((error) => Effect.fail(error))
178
+ );
179
+
180
+ export const execShellSafe = (
181
+ script: string,
182
+ options?: ExecOptions
183
+ ): Effect.Effect<string, never> =>
184
+ execShell(script, options).pipe(
185
+ Effect.catchAll((error) =>
186
+ Effect.gen(function* () {
187
+ yield* Effect.logWarning(
188
+ `Shell command failed (graceful): ${error.message}`
189
+ );
190
+ return "";
191
+ })
192
+ )
193
+ );
194
+
195
+ export const powershell = (
196
+ script: string,
197
+ options?: ExecOptions
198
+ ): Effect.Effect<string, CommandFailed | CommandTimeout> =>
199
+ Effect.gen(function* () {
200
+ const timeout = options?.timeout ?? DEFAULT_TIMEOUT;
201
+ const silent = options?.silent ?? false;
202
+
203
+ if (!silent) {
204
+ yield* Effect.logDebug(`Executing PowerShell: ${script.slice(0, 50)}...`);
205
+ }
206
+
207
+ const result = yield* Effect.tryPromise({
208
+ try: async () => {
209
+ const proc = await execa("powershell", ["-NoProfile", "-Command", script], {
210
+ cwd: options?.cwd,
211
+ timeout,
212
+ reject: false,
213
+ });
214
+ return {
215
+ stdout: proc.stdout?.trim() ?? "",
216
+ stderr: proc.stderr?.trim() ?? "",
217
+ exitCode: proc.exitCode,
218
+ timedOut: proc.timedOut,
219
+ };
220
+ },
221
+ catch: (error) => {
222
+ const err = error as { timedOut?: boolean; message?: string };
223
+ if (err.timedOut) {
224
+ return new CommandTimeout({
225
+ command: "powershell",
226
+ timeoutMs: timeout,
227
+ });
228
+ }
229
+ return new CommandFailed({
230
+ command: "powershell",
231
+ args: [script],
232
+ exitCode: -1,
233
+ stderr: String(error),
234
+ });
235
+ },
236
+ });
237
+
238
+ if (result.timedOut) {
239
+ return yield* Effect.fail(
240
+ new CommandTimeout({ command: "powershell", timeoutMs: timeout })
241
+ );
242
+ }
243
+
244
+ if (result.exitCode !== 0) {
245
+ if (!silent) {
246
+ yield* Effect.logWarning(
247
+ `PowerShell command failed (exit ${result.exitCode})`
248
+ );
249
+ }
250
+ return yield* Effect.fail(
251
+ new CommandFailed({
252
+ command: "powershell",
253
+ args: [script],
254
+ exitCode: result.exitCode ?? 1,
255
+ stderr: result.stderr,
256
+ })
257
+ );
258
+ }
259
+
260
+ return result.stdout;
261
+ }).pipe(
262
+ Effect.retry(
263
+ Schedule.recurs(options?.retries ?? DEFAULT_RETRIES).pipe(
264
+ Schedule.addDelay(() => "100 millis")
265
+ )
266
+ ),
267
+ Effect.catchAll((error) => Effect.fail(error))
268
+ );
269
+
270
+ export const powershellSafe = (
271
+ script: string,
272
+ options?: ExecOptions
273
+ ): Effect.Effect<string, never> =>
274
+ powershell(script, options).pipe(
275
+ Effect.catchAll((error) =>
276
+ Effect.gen(function* () {
277
+ yield* Effect.logWarning(
278
+ `PowerShell command failed (graceful): ${error.message}`
279
+ );
280
+ return "";
281
+ })
282
+ )
283
+ );
@@ -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;