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,332 @@
1
+ import { Effect, Layer } from "effect";
2
+ import { readFile } from "node:fs/promises";
3
+ import { execShellSafe } from "../command";
4
+ import type {
5
+ MemoryInfo,
6
+ PlatformOperations,
7
+ PortInfo,
8
+ ProcessInfo,
9
+ } from "../types";
10
+ import { PlatformService } from "../types";
11
+
12
+ const readFileSafe = (path: string): Effect.Effect<string | null, never> =>
13
+ Effect.tryPromise({
14
+ try: () => readFile(path, "utf-8"),
15
+ catch: () => new Error("file not found"),
16
+ }).pipe(
17
+ Effect.catchAll(() => Effect.succeed(null))
18
+ );
19
+
20
+ const getPortInfo = (
21
+ ports: number[]
22
+ ): Effect.Effect<Record<number, PortInfo>, never> =>
23
+ Effect.gen(function* () {
24
+ yield* Effect.logInfo(`[linux] Checking ${ports.length} ports`);
25
+
26
+ const result: Record<number, PortInfo> = {};
27
+ for (const port of ports) {
28
+ result[port] = { port, pid: null, command: null, state: "FREE" };
29
+ }
30
+
31
+ if (ports.length === 0) return result;
32
+
33
+ const output = yield* execShellSafe(
34
+ "ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null || true"
35
+ );
36
+ if (!output) {
37
+ yield* Effect.logDebug("[linux] No ss/netstat output, all ports appear free");
38
+ return result;
39
+ }
40
+
41
+ const lines = output.split("\n").filter(Boolean);
42
+ yield* Effect.logDebug(`[linux] Parsing ${lines.length} ss/netstat lines`);
43
+
44
+ for (const line of lines) {
45
+ const portMatch = line.match(/:(\d+)\s/);
46
+ if (!portMatch) continue;
47
+
48
+ const port = parseInt(portMatch[1], 10);
49
+ if (!ports.includes(port)) continue;
50
+
51
+ const pidMatch = line.match(/pid=(\d+)/);
52
+ const pid = pidMatch ? parseInt(pidMatch[1], 10) : null;
53
+
54
+ let command: string | null = null;
55
+ if (pid) {
56
+ const commContent = yield* readFileSafe(`/proc/${pid}/comm`);
57
+ command = commContent?.trim() ?? null;
58
+ }
59
+
60
+ result[port] = {
61
+ port,
62
+ pid,
63
+ command,
64
+ state: "LISTEN",
65
+ };
66
+
67
+ yield* Effect.logDebug(
68
+ `[linux] Port :${port} bound to PID ${pid} (${command})`
69
+ );
70
+ }
71
+
72
+ const boundCount = Object.values(result).filter(
73
+ (p) => p.state === "LISTEN"
74
+ ).length;
75
+ yield* Effect.logInfo(
76
+ `[linux] Found ${boundCount}/${ports.length} ports in use`
77
+ );
78
+
79
+ return result;
80
+ });
81
+
82
+ const getProcessTree = (
83
+ rootPids: number[]
84
+ ): Effect.Effect<ProcessInfo[], never> =>
85
+ Effect.gen(function* () {
86
+ yield* Effect.logInfo(
87
+ `[linux] Building process tree for ${rootPids.length} root PIDs`
88
+ );
89
+
90
+ const processes: ProcessInfo[] = [];
91
+ const visited = new Set<number>();
92
+
93
+ const getProcess = (
94
+ pid: number
95
+ ): Effect.Effect<ProcessInfo | null, never> =>
96
+ Effect.gen(function* () {
97
+ if (visited.has(pid)) return null;
98
+ visited.add(pid);
99
+
100
+ const stat = yield* readFileSafe(`/proc/${pid}/stat`);
101
+ const status = yield* readFileSafe(`/proc/${pid}/status`);
102
+ const cmdline = yield* readFileSafe(`/proc/${pid}/cmdline`);
103
+
104
+ if (!stat || !status) return null;
105
+
106
+ const statParts = stat.split(" ");
107
+ const ppid = parseInt(statParts[3], 10);
108
+
109
+ let rss = 0;
110
+ for (const line of status.split("\n")) {
111
+ if (line.startsWith("VmRSS:")) {
112
+ const match = line.match(/(\d+)/);
113
+ if (match) rss = parseInt(match[1], 10) * 1024;
114
+ break;
115
+ }
116
+ }
117
+
118
+ const args = cmdline?.split("\0").filter(Boolean) ?? [];
119
+ const command = args[0] || statParts[1].replace(/[()]/g, "");
120
+
121
+ yield* Effect.logDebug(
122
+ `[linux] Process ${pid}: ${command} (RSS: ${(rss / 1024).toFixed(0)}KB)`
123
+ );
124
+
125
+ return {
126
+ pid,
127
+ ppid,
128
+ command,
129
+ args: args.slice(1),
130
+ rss,
131
+ children: [],
132
+ };
133
+ });
134
+
135
+ const getChildren = (pid: number): Effect.Effect<number[], never> =>
136
+ Effect.gen(function* () {
137
+ const childrenFile = yield* readFileSafe(
138
+ `/proc/${pid}/task/${pid}/children`
139
+ );
140
+
141
+ if (childrenFile) {
142
+ const children = childrenFile
143
+ .trim()
144
+ .split(/\s+/)
145
+ .filter(Boolean)
146
+ .map((s) => parseInt(s, 10));
147
+
148
+ if (children.length > 0) {
149
+ yield* Effect.logDebug(
150
+ `[linux] PID ${pid} has ${children.length} children: ${children.join(", ")}`
151
+ );
152
+ }
153
+
154
+ return children;
155
+ }
156
+
157
+ const output = yield* execShellSafe(
158
+ `pgrep -P ${pid} 2>/dev/null || true`
159
+ );
160
+ if (!output) return [];
161
+
162
+ const children = output
163
+ .split("\n")
164
+ .filter(Boolean)
165
+ .map((s) => parseInt(s, 10));
166
+
167
+ if (children.length > 0) {
168
+ yield* Effect.logDebug(
169
+ `[linux] PID ${pid} has ${children.length} children (via pgrep)`
170
+ );
171
+ }
172
+
173
+ return children;
174
+ });
175
+
176
+ const traverse = (pid: number): Effect.Effect<void, never> =>
177
+ Effect.gen(function* () {
178
+ const proc = yield* getProcess(pid);
179
+ if (!proc) return;
180
+
181
+ const children = yield* getChildren(pid);
182
+ proc.children = children;
183
+ processes.push(proc);
184
+
185
+ for (const child of children) {
186
+ yield* traverse(child);
187
+ }
188
+ });
189
+
190
+ for (const pid of rootPids) {
191
+ yield* traverse(pid);
192
+ }
193
+
194
+ yield* Effect.logInfo(
195
+ `[linux] Process tree contains ${processes.length} processes`
196
+ );
197
+
198
+ return processes;
199
+ });
200
+
201
+ const getMemoryInfo = (): Effect.Effect<MemoryInfo, never> =>
202
+ Effect.gen(function* () {
203
+ yield* Effect.logDebug("[linux] Getting memory info");
204
+
205
+ const meminfo = yield* readFileSafe("/proc/meminfo");
206
+
207
+ if (!meminfo) {
208
+ yield* Effect.logWarning("[linux] Could not read /proc/meminfo");
209
+ return { total: 0, used: 0, free: 0, processRss: 0 };
210
+ }
211
+
212
+ let total = 0;
213
+ let free = 0;
214
+ let available = 0;
215
+
216
+ for (const line of meminfo.split("\n")) {
217
+ const [key, value] = line.split(":");
218
+ if (!value) continue;
219
+
220
+ const kb = parseInt(value.trim().split(/\s+/)[0], 10) * 1024;
221
+
222
+ if (key === "MemTotal") total = kb;
223
+ else if (key === "MemFree") free = kb;
224
+ else if (key === "MemAvailable") available = kb;
225
+ }
226
+
227
+ const totalMB = (total / 1024 / 1024).toFixed(0);
228
+ const usedMB = ((total - (available || free)) / 1024 / 1024).toFixed(0);
229
+ yield* Effect.logDebug(
230
+ `[linux] Memory: ${usedMB}MB used / ${totalMB}MB total`
231
+ );
232
+
233
+ return {
234
+ total,
235
+ used: total - (available || free),
236
+ free: available || free,
237
+ processRss: 0,
238
+ };
239
+ });
240
+
241
+ const getAllProcesses = (): Effect.Effect<ProcessInfo[], never> =>
242
+ Effect.gen(function* () {
243
+ yield* Effect.logDebug("[linux] Getting all processes");
244
+
245
+ const processes: ProcessInfo[] = [];
246
+
247
+ const output = yield* execShellSafe(
248
+ "ps -eo pid=,ppid=,rss=,comm= 2>/dev/null || true"
249
+ );
250
+ for (const line of output.split("\n").filter(Boolean)) {
251
+ const parts = line.trim().split(/\s+/);
252
+ if (parts.length < 4) continue;
253
+
254
+ const [pidStr, ppidStr, rssStr, ...rest] = parts;
255
+ processes.push({
256
+ pid: parseInt(pidStr, 10),
257
+ ppid: parseInt(ppidStr, 10),
258
+ command: rest.join(" "),
259
+ args: [],
260
+ rss: parseInt(rssStr, 10) * 1024,
261
+ children: [],
262
+ });
263
+ }
264
+
265
+ yield* Effect.logDebug(`[linux] Found ${processes.length} total processes`);
266
+
267
+ return processes;
268
+ });
269
+
270
+ const findChildProcesses = (pid: number): Effect.Effect<number[], never> =>
271
+ Effect.gen(function* () {
272
+ yield* Effect.logDebug(`[linux] Finding all children of PID ${pid}`);
273
+
274
+ const children: number[] = [];
275
+ const visited = new Set<number>();
276
+
277
+ const recurse = (parentPid: number): Effect.Effect<void, never> =>
278
+ Effect.gen(function* () {
279
+ if (visited.has(parentPid)) return;
280
+ visited.add(parentPid);
281
+
282
+ const childrenFile = yield* readFileSafe(
283
+ `/proc/${parentPid}/task/${parentPid}/children`
284
+ );
285
+
286
+ let childPids: number[] = [];
287
+ if (childrenFile) {
288
+ childPids = childrenFile
289
+ .trim()
290
+ .split(/\s+/)
291
+ .filter(Boolean)
292
+ .map((s) => parseInt(s, 10));
293
+ } else {
294
+ const output = yield* execShellSafe(
295
+ `pgrep -P ${parentPid} 2>/dev/null || true`
296
+ );
297
+ if (output) {
298
+ childPids = output
299
+ .split("\n")
300
+ .filter(Boolean)
301
+ .map((s) => parseInt(s, 10));
302
+ }
303
+ }
304
+
305
+ for (const childPid of childPids) {
306
+ if (!isNaN(childPid)) {
307
+ children.push(childPid);
308
+ yield* recurse(childPid);
309
+ }
310
+ }
311
+ });
312
+
313
+ yield* recurse(pid);
314
+
315
+ if (children.length > 0) {
316
+ yield* Effect.logDebug(
317
+ `[linux] PID ${pid} has ${children.length} descendants`
318
+ );
319
+ }
320
+
321
+ return children;
322
+ });
323
+
324
+ const linuxOperations: PlatformOperations = {
325
+ getPortInfo,
326
+ getProcessTree,
327
+ getMemoryInfo,
328
+ getAllProcesses,
329
+ findChildProcesses,
330
+ };
331
+
332
+ export const LinuxLayer = Layer.succeed(PlatformService, linuxOperations);
@@ -0,0 +1,298 @@
1
+ import { Effect, Layer } from "effect";
2
+ import { execShellSafe, powershellSafe } from "../command";
3
+ import type {
4
+ MemoryInfo,
5
+ PlatformOperations,
6
+ PortInfo,
7
+ ProcessInfo,
8
+ } from "../types";
9
+ import { PlatformService } from "../types";
10
+
11
+ const getPortInfo = (
12
+ ports: number[]
13
+ ): Effect.Effect<Record<number, PortInfo>, never> =>
14
+ Effect.gen(function* () {
15
+ yield* Effect.logInfo(`[windows] Checking ${ports.length} ports`);
16
+
17
+ const result: Record<number, PortInfo> = {};
18
+ for (const port of ports) {
19
+ result[port] = { port, pid: null, command: null, state: "FREE" };
20
+ }
21
+
22
+ if (ports.length === 0) return result;
23
+
24
+ const output = yield* execShellSafe("netstat -ano -p TCP");
25
+ if (!output) {
26
+ yield* Effect.logDebug(
27
+ "[windows] No netstat output, all ports appear free"
28
+ );
29
+ return result;
30
+ }
31
+
32
+ const lines = output.split("\n").filter(Boolean);
33
+ yield* Effect.logDebug(`[windows] Parsing ${lines.length} netstat lines`);
34
+
35
+ for (const line of lines) {
36
+ if (!line.includes("LISTENING")) continue;
37
+
38
+ const parts = line.trim().split(/\s+/);
39
+ if (parts.length < 5) continue;
40
+
41
+ const localAddr = parts[1];
42
+ const portMatch = localAddr.match(/:(\d+)$/);
43
+ if (!portMatch) continue;
44
+
45
+ const port = parseInt(portMatch[1], 10);
46
+ if (!ports.includes(port)) continue;
47
+
48
+ const pid = parseInt(parts[4], 10);
49
+
50
+ let command: string | null = null;
51
+ if (pid) {
52
+ const cmdOutput = yield* execShellSafe(
53
+ `wmic process where ProcessId=${pid} get Name /format:list`
54
+ );
55
+ const nameMatch = cmdOutput.match(/Name=(.+)/);
56
+ command = nameMatch ? nameMatch[1].trim() : null;
57
+ }
58
+
59
+ result[port] = {
60
+ port,
61
+ pid,
62
+ command,
63
+ state: "LISTEN",
64
+ };
65
+
66
+ yield* Effect.logDebug(
67
+ `[windows] Port :${port} bound to PID ${pid} (${command})`
68
+ );
69
+ }
70
+
71
+ const boundCount = Object.values(result).filter(
72
+ (p) => p.state === "LISTEN"
73
+ ).length;
74
+ yield* Effect.logInfo(
75
+ `[windows] Found ${boundCount}/${ports.length} ports in use`
76
+ );
77
+
78
+ return result;
79
+ });
80
+
81
+ const getProcessTree = (
82
+ rootPids: number[]
83
+ ): Effect.Effect<ProcessInfo[], never> =>
84
+ Effect.gen(function* () {
85
+ yield* Effect.logInfo(
86
+ `[windows] Building process tree for ${rootPids.length} root PIDs`
87
+ );
88
+
89
+ const processes: ProcessInfo[] = [];
90
+ const visited = new Set<number>();
91
+
92
+ const getProcess = (
93
+ pid: number
94
+ ): Effect.Effect<ProcessInfo | null, never> =>
95
+ Effect.gen(function* () {
96
+ if (visited.has(pid)) return null;
97
+ visited.add(pid);
98
+
99
+ const output = yield* execShellSafe(
100
+ `wmic process where ProcessId=${pid} get ProcessId,ParentProcessId,WorkingSetSize,Name,CommandLine /format:csv`
101
+ );
102
+ const lines = output
103
+ .split("\n")
104
+ .filter((l) => l.trim() && !l.startsWith("Node"));
105
+ if (lines.length === 0) return null;
106
+
107
+ const parts = lines[0].split(",");
108
+ if (parts.length < 5) return null;
109
+
110
+ const cmdLine = parts[1] || "";
111
+ const name = parts[2] || "";
112
+ const ppid = parseInt(parts[3], 10);
113
+ const procId = parseInt(parts[4], 10);
114
+ const rss = parseInt(parts[5], 10) || 0;
115
+
116
+ yield* Effect.logDebug(
117
+ `[windows] Process ${pid}: ${name} (RSS: ${(rss / 1024).toFixed(0)}KB)`
118
+ );
119
+
120
+ return {
121
+ pid: procId,
122
+ ppid,
123
+ command: name,
124
+ args: cmdLine.split(/\s+/).slice(1),
125
+ rss,
126
+ children: [],
127
+ };
128
+ });
129
+
130
+ const getChildren = (pid: number): Effect.Effect<number[], never> =>
131
+ Effect.gen(function* () {
132
+ const output = yield* execShellSafe(
133
+ `wmic process where ParentProcessId=${pid} get ProcessId /format:list`
134
+ );
135
+ const matches = output.match(/ProcessId=(\d+)/g);
136
+ if (!matches) return [];
137
+
138
+ const children = matches.map((m) =>
139
+ parseInt(m.replace("ProcessId=", ""), 10)
140
+ );
141
+
142
+ if (children.length > 0) {
143
+ yield* Effect.logDebug(
144
+ `[windows] PID ${pid} has ${children.length} children: ${children.join(", ")}`
145
+ );
146
+ }
147
+
148
+ return children;
149
+ });
150
+
151
+ const traverse = (pid: number): Effect.Effect<void, never> =>
152
+ Effect.gen(function* () {
153
+ const proc = yield* getProcess(pid);
154
+ if (!proc) return;
155
+
156
+ const children = yield* getChildren(pid);
157
+ proc.children = children;
158
+ processes.push(proc);
159
+
160
+ for (const child of children) {
161
+ yield* traverse(child);
162
+ }
163
+ });
164
+
165
+ for (const pid of rootPids) {
166
+ yield* traverse(pid);
167
+ }
168
+
169
+ yield* Effect.logInfo(
170
+ `[windows] Process tree contains ${processes.length} processes`
171
+ );
172
+
173
+ return processes;
174
+ });
175
+
176
+ const getMemoryInfo = (): Effect.Effect<MemoryInfo, never> =>
177
+ Effect.gen(function* () {
178
+ yield* Effect.logDebug("[windows] Getting memory info");
179
+
180
+ const script = `
181
+ $os = Get-CimInstance Win32_OperatingSystem
182
+ $total = $os.TotalVisibleMemorySize * 1024
183
+ $free = $os.FreePhysicalMemory * 1024
184
+ Write-Output "$total,$free"
185
+ `;
186
+ const output = yield* powershellSafe(script);
187
+
188
+ if (!output) {
189
+ yield* Effect.logWarning("[windows] Could not get memory info");
190
+ return { total: 0, used: 0, free: 0, processRss: 0 };
191
+ }
192
+
193
+ const [totalStr, freeStr] = output.split(",");
194
+ const total = parseInt(totalStr, 10) || 0;
195
+ const free = parseInt(freeStr, 10) || 0;
196
+
197
+ const totalMB = (total / 1024 / 1024).toFixed(0);
198
+ const usedMB = ((total - free) / 1024 / 1024).toFixed(0);
199
+ yield* Effect.logDebug(
200
+ `[windows] Memory: ${usedMB}MB used / ${totalMB}MB total`
201
+ );
202
+
203
+ return {
204
+ total,
205
+ used: total - free,
206
+ free,
207
+ processRss: 0,
208
+ };
209
+ });
210
+
211
+ const getAllProcesses = (): Effect.Effect<ProcessInfo[], never> =>
212
+ Effect.gen(function* () {
213
+ yield* Effect.logDebug("[windows] Getting all processes");
214
+
215
+ const processes: ProcessInfo[] = [];
216
+
217
+ const output = yield* execShellSafe(
218
+ "wmic process get ProcessId,ParentProcessId,WorkingSetSize,Name /format:csv"
219
+ );
220
+ const lines = output
221
+ .split("\n")
222
+ .filter((l) => l.trim() && !l.startsWith("Node"));
223
+
224
+ for (const line of lines) {
225
+ const parts = line.split(",");
226
+ if (parts.length < 4) continue;
227
+
228
+ const name = parts[1] || "";
229
+ const ppid = parseInt(parts[2], 10);
230
+ const pid = parseInt(parts[3], 10);
231
+ const rss = parseInt(parts[4], 10) || 0;
232
+
233
+ if (isNaN(pid)) continue;
234
+
235
+ processes.push({
236
+ pid,
237
+ ppid: isNaN(ppid) ? 0 : ppid,
238
+ command: name,
239
+ args: [],
240
+ rss,
241
+ children: [],
242
+ });
243
+ }
244
+
245
+ yield* Effect.logDebug(
246
+ `[windows] Found ${processes.length} total processes`
247
+ );
248
+
249
+ return processes;
250
+ });
251
+
252
+ const findChildProcesses = (pid: number): Effect.Effect<number[], never> =>
253
+ Effect.gen(function* () {
254
+ yield* Effect.logDebug(`[windows] Finding all children of PID ${pid}`);
255
+
256
+ const children: number[] = [];
257
+ const visited = new Set<number>();
258
+
259
+ const recurse = (parentPid: number): Effect.Effect<void, never> =>
260
+ Effect.gen(function* () {
261
+ if (visited.has(parentPid)) return;
262
+ visited.add(parentPid);
263
+
264
+ const output = yield* execShellSafe(
265
+ `wmic process where ParentProcessId=${parentPid} get ProcessId /format:list`
266
+ );
267
+ const matches = output.match(/ProcessId=(\d+)/g);
268
+ if (!matches) return;
269
+
270
+ for (const match of matches) {
271
+ const childPid = parseInt(match.replace("ProcessId=", ""), 10);
272
+ if (!isNaN(childPid)) {
273
+ children.push(childPid);
274
+ yield* recurse(childPid);
275
+ }
276
+ }
277
+ });
278
+
279
+ yield* recurse(pid);
280
+
281
+ if (children.length > 0) {
282
+ yield* Effect.logDebug(
283
+ `[windows] PID ${pid} has ${children.length} descendants`
284
+ );
285
+ }
286
+
287
+ return children;
288
+ });
289
+
290
+ const windowsOperations: PlatformOperations = {
291
+ getPortInfo,
292
+ getProcessTree,
293
+ getMemoryInfo,
294
+ getAllProcesses,
295
+ findChildProcesses,
296
+ };
297
+
298
+ export const WindowsLayer = Layer.succeed(PlatformService, windowsOperations);