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.
- package/package.json +11 -7
- package/src/cli.ts +109 -1
- package/src/components/monitor-view.tsx +471 -0
- package/src/contract.ts +94 -0
- package/src/lib/orchestrator.ts +13 -2
- package/src/lib/resource-monitor/assertions.ts +234 -0
- package/src/lib/resource-monitor/command.ts +283 -0
- package/src/lib/resource-monitor/diff.ts +143 -0
- package/src/lib/resource-monitor/errors.ts +127 -0
- package/src/lib/resource-monitor/index.ts +305 -0
- package/src/lib/resource-monitor/platform/darwin.ts +293 -0
- package/src/lib/resource-monitor/platform/index.ts +35 -0
- package/src/lib/resource-monitor/platform/linux.ts +332 -0
- package/src/lib/resource-monitor/platform/windows.ts +298 -0
- package/src/lib/resource-monitor/snapshot.ts +204 -0
- package/src/lib/resource-monitor/types.ts +74 -0
- package/src/lib/session-recorder/errors.ts +102 -0
- package/src/lib/session-recorder/flows/login.ts +210 -0
- package/src/lib/session-recorder/index.ts +361 -0
- package/src/lib/session-recorder/playwright.ts +257 -0
- package/src/lib/session-recorder/report.ts +353 -0
- package/src/lib/session-recorder/server.ts +267 -0
- package/src/lib/session-recorder/types.ts +115 -0
- package/src/plugin.ts +154 -15
package/src/contract.ts
CHANGED
|
@@ -314,6 +314,84 @@ const DockerStopResultSchema = z.object({
|
|
|
314
314
|
error: z.string().optional(),
|
|
315
315
|
});
|
|
316
316
|
|
|
317
|
+
const MonitorOptionsSchema = z.object({
|
|
318
|
+
ports: z.array(z.number()).optional(),
|
|
319
|
+
json: z.boolean().default(false),
|
|
320
|
+
watch: z.boolean().default(false),
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const PortInfoSchema = z.object({
|
|
324
|
+
port: z.number(),
|
|
325
|
+
pid: z.number().nullable(),
|
|
326
|
+
command: z.string().nullable(),
|
|
327
|
+
state: z.enum(["LISTEN", "ESTABLISHED", "TIME_WAIT", "FREE"]),
|
|
328
|
+
name: z.string().optional(),
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const ProcessInfoSchema = z.object({
|
|
332
|
+
pid: z.number(),
|
|
333
|
+
ppid: z.number(),
|
|
334
|
+
command: z.string(),
|
|
335
|
+
args: z.array(z.string()),
|
|
336
|
+
rss: z.number(),
|
|
337
|
+
children: z.array(z.number()),
|
|
338
|
+
startTime: z.number().optional(),
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const MemoryInfoSchema = z.object({
|
|
342
|
+
total: z.number(),
|
|
343
|
+
used: z.number(),
|
|
344
|
+
free: z.number(),
|
|
345
|
+
processRss: z.number(),
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const SnapshotSchema = z.object({
|
|
349
|
+
timestamp: z.number(),
|
|
350
|
+
configPath: z.string().nullable(),
|
|
351
|
+
ports: z.record(z.string(), PortInfoSchema),
|
|
352
|
+
processes: z.array(ProcessInfoSchema),
|
|
353
|
+
memory: MemoryInfoSchema,
|
|
354
|
+
platform: z.string(),
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const MonitorResultSchema = z.object({
|
|
358
|
+
status: z.enum(["snapshot", "watching", "error"]),
|
|
359
|
+
snapshot: SnapshotSchema.optional(),
|
|
360
|
+
error: z.string().optional(),
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const SessionOptionsSchema = z.object({
|
|
364
|
+
headless: z.boolean().default(true),
|
|
365
|
+
timeout: z.number().default(120000),
|
|
366
|
+
output: z.string().default("./session-report.json"),
|
|
367
|
+
format: z.enum(["json", "html"]).default("json"),
|
|
368
|
+
flow: z.enum(["login", "navigation", "custom"]).default("login"),
|
|
369
|
+
routes: z.array(z.string()).optional(),
|
|
370
|
+
snapshotInterval: z.number().default(2000),
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const SessionSummarySchema = z.object({
|
|
374
|
+
totalMemoryDeltaMb: z.number(),
|
|
375
|
+
peakMemoryMb: z.number(),
|
|
376
|
+
averageMemoryMb: z.number(),
|
|
377
|
+
processesSpawned: z.number(),
|
|
378
|
+
processesKilled: z.number(),
|
|
379
|
+
orphanedProcesses: z.number(),
|
|
380
|
+
portsUsed: z.array(z.number()),
|
|
381
|
+
portsLeaked: z.number(),
|
|
382
|
+
hasLeaks: z.boolean(),
|
|
383
|
+
eventCount: z.number(),
|
|
384
|
+
duration: z.number(),
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const SessionResultSchema = z.object({
|
|
388
|
+
status: z.enum(["completed", "leaks_detected", "error", "timeout"]),
|
|
389
|
+
sessionId: z.string().optional(),
|
|
390
|
+
reportPath: z.string().optional(),
|
|
391
|
+
summary: SessionSummarySchema.optional(),
|
|
392
|
+
error: z.string().optional(),
|
|
393
|
+
});
|
|
394
|
+
|
|
317
395
|
const DepsUpdateOptionsSchema = z.object({
|
|
318
396
|
category: z.enum(["ui", "api"]).default("ui"),
|
|
319
397
|
packages: z.array(z.string()).optional(),
|
|
@@ -460,6 +538,16 @@ export const bosContract = oc.router({
|
|
|
460
538
|
.route({ method: "POST", path: "/docker/stop" })
|
|
461
539
|
.input(DockerStopOptionsSchema)
|
|
462
540
|
.output(DockerStopResultSchema),
|
|
541
|
+
|
|
542
|
+
monitor: oc
|
|
543
|
+
.route({ method: "GET", path: "/monitor" })
|
|
544
|
+
.input(MonitorOptionsSchema)
|
|
545
|
+
.output(MonitorResultSchema),
|
|
546
|
+
|
|
547
|
+
session: oc
|
|
548
|
+
.route({ method: "POST", path: "/session" })
|
|
549
|
+
.input(SessionOptionsSchema)
|
|
550
|
+
.output(SessionResultSchema),
|
|
463
551
|
});
|
|
464
552
|
|
|
465
553
|
export type BosContract = typeof bosContract;
|
|
@@ -498,3 +586,9 @@ export type DepsUpdateOptions = z.infer<typeof DepsUpdateOptionsSchema>;
|
|
|
498
586
|
export type DepsUpdateResult = z.infer<typeof DepsUpdateResultSchema>;
|
|
499
587
|
export type FilesSyncOptions = z.infer<typeof FilesSyncOptionsSchema>;
|
|
500
588
|
export type FilesSyncResult = z.infer<typeof FilesSyncResultSchema>;
|
|
589
|
+
export type MonitorOptions = z.infer<typeof MonitorOptionsSchema>;
|
|
590
|
+
export type MonitorResult = z.infer<typeof MonitorResultSchema>;
|
|
591
|
+
export type MonitorSnapshot = z.infer<typeof SnapshotSchema>;
|
|
592
|
+
export type SessionOptions = z.infer<typeof SessionOptionsSchema>;
|
|
593
|
+
export type SessionResult = z.infer<typeof SessionResultSchema>;
|
|
594
|
+
export type SessionSummary = z.infer<typeof SessionSummarySchema>;
|
package/src/lib/orchestrator.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { renderStreamingView } from "../components/streaming-view";
|
|
|
12
12
|
import type { AppConfig, BosConfig } from "../config";
|
|
13
13
|
import { getProcessConfig, makeDevProcess, type ProcessCallbacks, type ProcessHandle } from "./process";
|
|
14
14
|
|
|
15
|
+
let activeCleanup: (() => Promise<void>) | null = null;
|
|
16
|
+
|
|
15
17
|
const LOG_NOISE_PATTERNS = [
|
|
16
18
|
/\[ Federation Runtime \] Version .* from host of shared singleton module/,
|
|
17
19
|
/Executing an Effect versioned \d+\.\d+\.\d+ with a Runtime of version/,
|
|
@@ -137,8 +139,11 @@ export const runDevServers = (orchestrator: AppOrchestrator) =>
|
|
|
137
139
|
if (showLogs) {
|
|
138
140
|
await exportLogs();
|
|
139
141
|
}
|
|
142
|
+
activeCleanup = null;
|
|
140
143
|
};
|
|
141
144
|
|
|
145
|
+
activeCleanup = cleanup;
|
|
146
|
+
|
|
142
147
|
const useInteractive = orchestrator.interactive ?? isInteractiveSupported();
|
|
143
148
|
|
|
144
149
|
view = useInteractive
|
|
@@ -220,12 +225,18 @@ export const startApp = (orchestrator: AppOrchestrator) => {
|
|
|
220
225
|
}))
|
|
221
226
|
);
|
|
222
227
|
|
|
228
|
+
const handleSignal = async () => {
|
|
229
|
+
if (activeCleanup) {
|
|
230
|
+
await activeCleanup();
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
223
234
|
process.on("SIGINT", () => {
|
|
224
|
-
|
|
235
|
+
handleSignal().finally(() => process.exit(0));
|
|
225
236
|
});
|
|
226
237
|
|
|
227
238
|
process.on("SIGTERM", () => {
|
|
228
|
-
|
|
239
|
+
handleSignal().finally(() => process.exit(0));
|
|
229
240
|
});
|
|
230
241
|
|
|
231
242
|
BunRuntime.runMain(program);
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import {
|
|
3
|
+
MemoryLimitExceeded,
|
|
4
|
+
MemoryPercentExceeded,
|
|
5
|
+
OrphanedProcesses,
|
|
6
|
+
PortStillBound,
|
|
7
|
+
ProcessesStillAlive,
|
|
8
|
+
ResourceLeaks,
|
|
9
|
+
} from "./errors";
|
|
10
|
+
import { PlatformService, withPlatform } from "./platform";
|
|
11
|
+
import { isProcessAlive } from "./snapshot";
|
|
12
|
+
import type { Snapshot, SnapshotDiff } from "./types";
|
|
13
|
+
|
|
14
|
+
export const assertAllPortsFree = (
|
|
15
|
+
ports: number[]
|
|
16
|
+
): Effect.Effect<void, PortStillBound, PlatformService> =>
|
|
17
|
+
Effect.gen(function* () {
|
|
18
|
+
yield* Effect.logInfo(`Asserting ${ports.length} ports are free`);
|
|
19
|
+
|
|
20
|
+
const platform = yield* PlatformService;
|
|
21
|
+
const portInfo = yield* platform.getPortInfo(ports);
|
|
22
|
+
|
|
23
|
+
const bound: Array<{
|
|
24
|
+
port: number;
|
|
25
|
+
pid: number | null;
|
|
26
|
+
command: string | null;
|
|
27
|
+
}> = [];
|
|
28
|
+
|
|
29
|
+
for (const [portStr, info] of Object.entries(portInfo)) {
|
|
30
|
+
if (info.state !== "FREE") {
|
|
31
|
+
bound.push({
|
|
32
|
+
port: parseInt(portStr, 10),
|
|
33
|
+
pid: info.pid,
|
|
34
|
+
command: info.command,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (bound.length > 0) {
|
|
40
|
+
yield* Effect.logError(`${bound.length} ports still bound:`);
|
|
41
|
+
for (const p of bound) {
|
|
42
|
+
yield* Effect.logError(` :${p.port} ← PID ${p.pid} (${p.command})`);
|
|
43
|
+
}
|
|
44
|
+
return yield* Effect.fail(new PortStillBound({ ports: bound }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
yield* Effect.logInfo(`All ${ports.length} ports are free ✓`);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const assertAllPortsFreeWithPlatform = (
|
|
51
|
+
ports: number[]
|
|
52
|
+
): Effect.Effect<void, PortStillBound> =>
|
|
53
|
+
withPlatform(assertAllPortsFree(ports));
|
|
54
|
+
|
|
55
|
+
export const assertNoOrphanProcesses = (
|
|
56
|
+
runningSnapshot: Snapshot,
|
|
57
|
+
afterSnapshot: Snapshot
|
|
58
|
+
): Effect.Effect<void, OrphanedProcesses> =>
|
|
59
|
+
Effect.gen(function* () {
|
|
60
|
+
yield* Effect.logInfo("Checking for orphaned processes");
|
|
61
|
+
|
|
62
|
+
const runningPids = new Set(runningSnapshot.processes.map((p) => p.pid));
|
|
63
|
+
|
|
64
|
+
const orphans: Array<{ pid: number; command: string; rss: number }> = [];
|
|
65
|
+
|
|
66
|
+
for (const proc of runningSnapshot.processes) {
|
|
67
|
+
if (!runningPids.has(proc.pid)) continue;
|
|
68
|
+
|
|
69
|
+
const stillInAfter = afterSnapshot.processes.some(
|
|
70
|
+
(p) => p.pid === proc.pid
|
|
71
|
+
);
|
|
72
|
+
if (stillInAfter) continue;
|
|
73
|
+
|
|
74
|
+
const alive = yield* isProcessAlive(proc.pid);
|
|
75
|
+
if (alive) {
|
|
76
|
+
orphans.push({
|
|
77
|
+
pid: proc.pid,
|
|
78
|
+
command: proc.command,
|
|
79
|
+
rss: proc.rss,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (orphans.length > 0) {
|
|
85
|
+
yield* Effect.logError(`${orphans.length} orphaned processes found:`);
|
|
86
|
+
for (const p of orphans) {
|
|
87
|
+
yield* Effect.logError(
|
|
88
|
+
` PID ${p.pid}: ${p.command} (${(p.rss / 1024 / 1024).toFixed(1)}MB)`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return yield* Effect.fail(new OrphanedProcesses({ processes: orphans }));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
yield* Effect.logInfo("No orphaned processes ✓");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export const assertMemoryDelta = (
|
|
98
|
+
baseline: Snapshot,
|
|
99
|
+
after: Snapshot,
|
|
100
|
+
options: { maxDeltaMB?: number; maxDeltaPercent?: number }
|
|
101
|
+
): Effect.Effect<void, MemoryLimitExceeded | MemoryPercentExceeded> =>
|
|
102
|
+
Effect.gen(function* () {
|
|
103
|
+
yield* Effect.logInfo("Checking memory delta");
|
|
104
|
+
|
|
105
|
+
const deltaMB =
|
|
106
|
+
(after.memory.processRss - baseline.memory.processRss) / 1024 / 1024;
|
|
107
|
+
|
|
108
|
+
yield* Effect.logDebug(
|
|
109
|
+
`Memory delta: ${deltaMB >= 0 ? "+" : ""}${deltaMB.toFixed(1)}MB`
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (options.maxDeltaMB !== undefined && deltaMB > options.maxDeltaMB) {
|
|
113
|
+
yield* Effect.logError(
|
|
114
|
+
`Memory delta ${deltaMB.toFixed(1)}MB exceeds max ${options.maxDeltaMB}MB`
|
|
115
|
+
);
|
|
116
|
+
return yield* Effect.fail(
|
|
117
|
+
new MemoryLimitExceeded({
|
|
118
|
+
deltaMB,
|
|
119
|
+
limitMB: options.maxDeltaMB,
|
|
120
|
+
baselineRss: baseline.memory.processRss,
|
|
121
|
+
afterRss: after.memory.processRss,
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (options.maxDeltaPercent !== undefined && baseline.memory.processRss > 0) {
|
|
127
|
+
const deltaPercent =
|
|
128
|
+
((after.memory.processRss - baseline.memory.processRss) /
|
|
129
|
+
baseline.memory.processRss) *
|
|
130
|
+
100;
|
|
131
|
+
|
|
132
|
+
yield* Effect.logDebug(
|
|
133
|
+
`Memory delta: ${deltaPercent >= 0 ? "+" : ""}${deltaPercent.toFixed(1)}%`
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (deltaPercent > options.maxDeltaPercent) {
|
|
137
|
+
yield* Effect.logError(
|
|
138
|
+
`Memory delta ${deltaPercent.toFixed(1)}% exceeds max ${options.maxDeltaPercent}%`
|
|
139
|
+
);
|
|
140
|
+
return yield* Effect.fail(
|
|
141
|
+
new MemoryPercentExceeded({
|
|
142
|
+
deltaPercent,
|
|
143
|
+
limitPercent: options.maxDeltaPercent,
|
|
144
|
+
baselineRss: baseline.memory.processRss,
|
|
145
|
+
afterRss: after.memory.processRss,
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
yield* Effect.logInfo("Memory delta within limits ✓");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
export const assertProcessesDead = (
|
|
155
|
+
pids: number[]
|
|
156
|
+
): Effect.Effect<void, ProcessesStillAlive> =>
|
|
157
|
+
Effect.gen(function* () {
|
|
158
|
+
yield* Effect.logInfo(`Asserting ${pids.length} processes are dead`);
|
|
159
|
+
|
|
160
|
+
const stillAlive: number[] = [];
|
|
161
|
+
|
|
162
|
+
for (const pid of pids) {
|
|
163
|
+
const alive = yield* isProcessAlive(pid);
|
|
164
|
+
if (alive) {
|
|
165
|
+
stillAlive.push(pid);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (stillAlive.length > 0) {
|
|
170
|
+
yield* Effect.logError(
|
|
171
|
+
`${stillAlive.length} processes still alive: ${stillAlive.join(", ")}`
|
|
172
|
+
);
|
|
173
|
+
return yield* Effect.fail(new ProcessesStillAlive({ pids: stillAlive }));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
yield* Effect.logInfo(`All ${pids.length} processes are dead ✓`);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
export const assertNoLeaks = (
|
|
180
|
+
diff: SnapshotDiff
|
|
181
|
+
): Effect.Effect<void, ResourceLeaks> =>
|
|
182
|
+
Effect.gen(function* () {
|
|
183
|
+
yield* Effect.logInfo("Checking for resource leaks");
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
diff.orphanedProcesses.length > 0 ||
|
|
187
|
+
diff.stillBoundPorts.length > 0
|
|
188
|
+
) {
|
|
189
|
+
yield* Effect.logError("Resource leaks detected:");
|
|
190
|
+
|
|
191
|
+
for (const proc of diff.orphanedProcesses) {
|
|
192
|
+
yield* Effect.logError(
|
|
193
|
+
` Orphaned PID ${proc.pid}: ${proc.command} (${(proc.rss / 1024 / 1024).toFixed(1)}MB)`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const port of diff.stillBoundPorts) {
|
|
198
|
+
yield* Effect.logError(
|
|
199
|
+
` Port :${port.port} still bound to PID ${port.pid} (${port.command})`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return yield* Effect.fail(
|
|
204
|
+
new ResourceLeaks({
|
|
205
|
+
orphanedProcesses: diff.orphanedProcesses,
|
|
206
|
+
stillBoundPorts: diff.stillBoundPorts,
|
|
207
|
+
})
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
yield* Effect.logInfo("No resource leaks detected ✓");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
export const assertCleanState = (
|
|
215
|
+
baseline: Snapshot
|
|
216
|
+
): Effect.Effect<void, PortStillBound | ProcessesStillAlive, PlatformService> =>
|
|
217
|
+
Effect.gen(function* () {
|
|
218
|
+
yield* Effect.logInfo("Asserting clean state");
|
|
219
|
+
|
|
220
|
+
const ports = Object.keys(baseline.ports).map((p) => parseInt(p, 10));
|
|
221
|
+
yield* assertAllPortsFree(ports);
|
|
222
|
+
|
|
223
|
+
const pids = baseline.processes.map((p) => p.pid);
|
|
224
|
+
if (pids.length > 0) {
|
|
225
|
+
yield* assertProcessesDead(pids);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
yield* Effect.logInfo("Clean state verified ✓");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
export const assertCleanStateWithPlatform = (
|
|
232
|
+
baseline: Snapshot
|
|
233
|
+
): Effect.Effect<void, PortStillBound | ProcessesStillAlive> =>
|
|
234
|
+
withPlatform(assertCleanState(baseline));
|
|
@@ -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
|
+
);
|