@dexto/tools-process 1.5.0

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.
Files changed (42) hide show
  1. package/LICENSE +44 -0
  2. package/dist/bash-exec-tool.cjs +130 -0
  3. package/dist/bash-exec-tool.d.cts +17 -0
  4. package/dist/bash-exec-tool.d.ts +17 -0
  5. package/dist/bash-exec-tool.js +96 -0
  6. package/dist/bash-output-tool.cjs +49 -0
  7. package/dist/bash-output-tool.d.cts +16 -0
  8. package/dist/bash-output-tool.d.ts +16 -0
  9. package/dist/bash-output-tool.js +25 -0
  10. package/dist/command-validator.cjs +554 -0
  11. package/dist/command-validator.d.cts +52 -0
  12. package/dist/command-validator.d.ts +52 -0
  13. package/dist/command-validator.js +530 -0
  14. package/dist/error-codes.cjs +47 -0
  15. package/dist/error-codes.d.cts +26 -0
  16. package/dist/error-codes.d.ts +26 -0
  17. package/dist/error-codes.js +23 -0
  18. package/dist/errors.cjs +243 -0
  19. package/dist/errors.d.cts +90 -0
  20. package/dist/errors.d.ts +90 -0
  21. package/dist/errors.js +219 -0
  22. package/dist/index.cjs +49 -0
  23. package/dist/index.d.cts +11 -0
  24. package/dist/index.d.ts +11 -0
  25. package/dist/index.js +18 -0
  26. package/dist/kill-process-tool.cjs +47 -0
  27. package/dist/kill-process-tool.d.cts +16 -0
  28. package/dist/kill-process-tool.d.ts +16 -0
  29. package/dist/kill-process-tool.js +23 -0
  30. package/dist/process-service.cjs +544 -0
  31. package/dist/process-service.d.cts +96 -0
  32. package/dist/process-service.d.ts +96 -0
  33. package/dist/process-service.js +510 -0
  34. package/dist/tool-provider.cjs +96 -0
  35. package/dist/tool-provider.d.cts +72 -0
  36. package/dist/tool-provider.d.ts +72 -0
  37. package/dist/tool-provider.js +72 -0
  38. package/dist/types.cjs +16 -0
  39. package/dist/types.d.cts +108 -0
  40. package/dist/types.d.ts +108 -0
  41. package/dist/types.js +0 -0
  42. package/package.json +38 -0
@@ -0,0 +1,96 @@
1
+ import { ProcessConfig, ExecuteOptions, ProcessResult, ProcessHandle, ProcessOutput, ProcessInfo } from './types.cjs';
2
+ import { IDextoLogger } from '@dexto/core';
3
+
4
+ /**
5
+ * Process Service
6
+ *
7
+ * Secure command execution and process management for Dexto internal tools
8
+ */
9
+
10
+ /**
11
+ * ProcessService - Handles command execution and process management
12
+ *
13
+ * This service receives fully-validated configuration from the Process Tools Provider.
14
+ * All defaults have been applied by the provider's schema, so the service trusts the config
15
+ * and uses it as-is without any fallback logic.
16
+ *
17
+ * TODO: Add tests for this class
18
+ */
19
+ declare class ProcessService {
20
+ private config;
21
+ private commandValidator;
22
+ private initialized;
23
+ private initPromise;
24
+ private backgroundProcesses;
25
+ private logger;
26
+ /**
27
+ * Create a new ProcessService with validated configuration.
28
+ *
29
+ * @param config - Fully-validated configuration from provider schema.
30
+ * All required fields have values, defaults already applied.
31
+ * @param logger - Logger instance for this service
32
+ */
33
+ constructor(config: ProcessConfig, logger: IDextoLogger);
34
+ /**
35
+ * Initialize the service.
36
+ * Safe to call multiple times - subsequent calls return the same promise.
37
+ */
38
+ initialize(): Promise<void>;
39
+ /**
40
+ * Internal initialization logic.
41
+ */
42
+ private doInitialize;
43
+ /**
44
+ * Ensure the service is initialized before use.
45
+ * Tools should call this at the start of their execute methods.
46
+ * Safe to call multiple times - will await the same initialization promise.
47
+ */
48
+ ensureInitialized(): Promise<void>;
49
+ /**
50
+ * Execute a command
51
+ */
52
+ executeCommand(command: string, options?: ExecuteOptions): Promise<ProcessResult | ProcessHandle>;
53
+ private static readonly SIGKILL_TIMEOUT_MS;
54
+ /**
55
+ * Kill a process tree (process group on Unix, taskkill on Windows)
56
+ */
57
+ private killProcessTree;
58
+ /**
59
+ * Execute command in foreground with timeout and abort support
60
+ */
61
+ private executeForeground;
62
+ /**
63
+ * Execute command in background
64
+ */
65
+ private executeInBackground;
66
+ /**
67
+ * Get output from a background process
68
+ */
69
+ getProcessOutput(processId: string): Promise<ProcessOutput>;
70
+ /**
71
+ * Kill a background process
72
+ */
73
+ killProcess(processId: string): Promise<void>;
74
+ /**
75
+ * List all background processes
76
+ */
77
+ listProcesses(): Promise<ProcessInfo[]>;
78
+ /**
79
+ * Get buffer size in bytes
80
+ */
81
+ private getBufferSize;
82
+ /**
83
+ * Get service configuration
84
+ */
85
+ getConfig(): Readonly<ProcessConfig>;
86
+ /**
87
+ * Resolve and confine cwd to the configured working directory
88
+ */
89
+ private resolveSafeCwd;
90
+ /**
91
+ * Cleanup completed processes
92
+ */
93
+ cleanup(): Promise<void>;
94
+ }
95
+
96
+ export { ProcessService };
@@ -0,0 +1,96 @@
1
+ import { ProcessConfig, ExecuteOptions, ProcessResult, ProcessHandle, ProcessOutput, ProcessInfo } from './types.js';
2
+ import { IDextoLogger } from '@dexto/core';
3
+
4
+ /**
5
+ * Process Service
6
+ *
7
+ * Secure command execution and process management for Dexto internal tools
8
+ */
9
+
10
+ /**
11
+ * ProcessService - Handles command execution and process management
12
+ *
13
+ * This service receives fully-validated configuration from the Process Tools Provider.
14
+ * All defaults have been applied by the provider's schema, so the service trusts the config
15
+ * and uses it as-is without any fallback logic.
16
+ *
17
+ * TODO: Add tests for this class
18
+ */
19
+ declare class ProcessService {
20
+ private config;
21
+ private commandValidator;
22
+ private initialized;
23
+ private initPromise;
24
+ private backgroundProcesses;
25
+ private logger;
26
+ /**
27
+ * Create a new ProcessService with validated configuration.
28
+ *
29
+ * @param config - Fully-validated configuration from provider schema.
30
+ * All required fields have values, defaults already applied.
31
+ * @param logger - Logger instance for this service
32
+ */
33
+ constructor(config: ProcessConfig, logger: IDextoLogger);
34
+ /**
35
+ * Initialize the service.
36
+ * Safe to call multiple times - subsequent calls return the same promise.
37
+ */
38
+ initialize(): Promise<void>;
39
+ /**
40
+ * Internal initialization logic.
41
+ */
42
+ private doInitialize;
43
+ /**
44
+ * Ensure the service is initialized before use.
45
+ * Tools should call this at the start of their execute methods.
46
+ * Safe to call multiple times - will await the same initialization promise.
47
+ */
48
+ ensureInitialized(): Promise<void>;
49
+ /**
50
+ * Execute a command
51
+ */
52
+ executeCommand(command: string, options?: ExecuteOptions): Promise<ProcessResult | ProcessHandle>;
53
+ private static readonly SIGKILL_TIMEOUT_MS;
54
+ /**
55
+ * Kill a process tree (process group on Unix, taskkill on Windows)
56
+ */
57
+ private killProcessTree;
58
+ /**
59
+ * Execute command in foreground with timeout and abort support
60
+ */
61
+ private executeForeground;
62
+ /**
63
+ * Execute command in background
64
+ */
65
+ private executeInBackground;
66
+ /**
67
+ * Get output from a background process
68
+ */
69
+ getProcessOutput(processId: string): Promise<ProcessOutput>;
70
+ /**
71
+ * Kill a background process
72
+ */
73
+ killProcess(processId: string): Promise<void>;
74
+ /**
75
+ * List all background processes
76
+ */
77
+ listProcesses(): Promise<ProcessInfo[]>;
78
+ /**
79
+ * Get buffer size in bytes
80
+ */
81
+ private getBufferSize;
82
+ /**
83
+ * Get service configuration
84
+ */
85
+ getConfig(): Readonly<ProcessConfig>;
86
+ /**
87
+ * Resolve and confine cwd to the configured working directory
88
+ */
89
+ private resolveSafeCwd;
90
+ /**
91
+ * Cleanup completed processes
92
+ */
93
+ cleanup(): Promise<void>;
94
+ }
95
+
96
+ export { ProcessService };
@@ -0,0 +1,510 @@
1
+ import { spawn } from "node:child_process";
2
+ import * as crypto from "node:crypto";
3
+ import * as path from "node:path";
4
+ import { CommandValidator } from "./command-validator.js";
5
+ import { ProcessError } from "./errors.js";
6
+ import { DextoLogComponent } from "@dexto/core";
7
+ const DEFAULT_TIMEOUT = 12e4;
8
+ class ProcessService {
9
+ config;
10
+ commandValidator;
11
+ initialized = false;
12
+ initPromise = null;
13
+ backgroundProcesses = /* @__PURE__ */ new Map();
14
+ logger;
15
+ /**
16
+ * Create a new ProcessService with validated configuration.
17
+ *
18
+ * @param config - Fully-validated configuration from provider schema.
19
+ * All required fields have values, defaults already applied.
20
+ * @param logger - Logger instance for this service
21
+ */
22
+ constructor(config, logger) {
23
+ this.config = config;
24
+ this.logger = logger.createChild(DextoLogComponent.PROCESS);
25
+ this.commandValidator = new CommandValidator(this.config, this.logger);
26
+ }
27
+ /**
28
+ * Initialize the service.
29
+ * Safe to call multiple times - subsequent calls return the same promise.
30
+ */
31
+ initialize() {
32
+ if (this.initPromise) {
33
+ return this.initPromise;
34
+ }
35
+ this.initPromise = this.doInitialize();
36
+ return this.initPromise;
37
+ }
38
+ /**
39
+ * Internal initialization logic.
40
+ */
41
+ async doInitialize() {
42
+ if (this.initialized) {
43
+ this.logger.debug("ProcessService already initialized");
44
+ return;
45
+ }
46
+ this.backgroundProcesses.clear();
47
+ this.initialized = true;
48
+ this.logger.info("ProcessService initialized successfully");
49
+ }
50
+ /**
51
+ * Ensure the service is initialized before use.
52
+ * Tools should call this at the start of their execute methods.
53
+ * Safe to call multiple times - will await the same initialization promise.
54
+ */
55
+ async ensureInitialized() {
56
+ if (this.initialized) {
57
+ return;
58
+ }
59
+ await this.initialize();
60
+ }
61
+ /**
62
+ * Execute a command
63
+ */
64
+ async executeCommand(command, options = {}) {
65
+ await this.ensureInitialized();
66
+ const validation = this.commandValidator.validateCommand(command);
67
+ if (!validation.isValid || !validation.normalizedCommand) {
68
+ throw ProcessError.invalidCommand(command, validation.error || "Unknown error");
69
+ }
70
+ const normalizedCommand = validation.normalizedCommand;
71
+ const rawTimeout = options.timeout !== void 0 && Number.isFinite(options.timeout) ? options.timeout : DEFAULT_TIMEOUT;
72
+ const timeout = Math.max(1, Math.min(rawTimeout, this.config.maxTimeout));
73
+ const cwd = this.resolveSafeCwd(options.cwd);
74
+ const env = {};
75
+ for (const [key, value] of Object.entries({
76
+ ...process.env,
77
+ ...this.config.environment,
78
+ ...options.env
79
+ })) {
80
+ if (value !== void 0) {
81
+ env[key] = value;
82
+ }
83
+ }
84
+ if (options.runInBackground) {
85
+ return await this.executeInBackground(normalizedCommand, options);
86
+ }
87
+ return await this.executeForeground(normalizedCommand, {
88
+ cwd,
89
+ timeout,
90
+ env,
91
+ ...options.description !== void 0 && { description: options.description },
92
+ ...options.abortSignal !== void 0 && { abortSignal: options.abortSignal }
93
+ });
94
+ }
95
+ static SIGKILL_TIMEOUT_MS = 200;
96
+ /**
97
+ * Kill a process tree (process group on Unix, taskkill on Windows)
98
+ */
99
+ async killProcessTree(pid, child) {
100
+ if (process.platform === "win32") {
101
+ await new Promise((resolve) => {
102
+ const killer = spawn("taskkill", ["/pid", String(pid), "/f", "/t"], {
103
+ stdio: "ignore"
104
+ });
105
+ killer.once("exit", () => resolve());
106
+ killer.once("error", () => resolve());
107
+ });
108
+ } else {
109
+ try {
110
+ process.kill(-pid, "SIGTERM");
111
+ await new Promise((res) => setTimeout(res, ProcessService.SIGKILL_TIMEOUT_MS));
112
+ if (child.exitCode === null) {
113
+ process.kill(-pid, "SIGKILL");
114
+ }
115
+ } catch {
116
+ child.kill("SIGTERM");
117
+ await new Promise((res) => setTimeout(res, ProcessService.SIGKILL_TIMEOUT_MS));
118
+ if (child.exitCode === null) {
119
+ child.kill("SIGKILL");
120
+ }
121
+ }
122
+ }
123
+ }
124
+ /**
125
+ * Execute command in foreground with timeout and abort support
126
+ */
127
+ executeForeground(command, options) {
128
+ return new Promise((resolve, reject) => {
129
+ const startTime = Date.now();
130
+ let stdout = "";
131
+ let stderr = "";
132
+ let stdoutBytes = 0;
133
+ let stderrBytes = 0;
134
+ let outputTruncated = false;
135
+ let killed = false;
136
+ let aborted = false;
137
+ let closed = false;
138
+ const maxBuffer = this.config.maxOutputBuffer;
139
+ if (options.abortSignal?.aborted) {
140
+ this.logger.debug(`Command cancelled before execution: ${command}`);
141
+ resolve({
142
+ stdout: "",
143
+ stderr: "(Command was cancelled)",
144
+ exitCode: 130,
145
+ // Standard exit code for SIGINT
146
+ duration: 0
147
+ });
148
+ return;
149
+ }
150
+ this.logger.debug(`Executing command: ${command}`);
151
+ const child = spawn(command, {
152
+ cwd: options.cwd,
153
+ env: options.env,
154
+ shell: true,
155
+ detached: process.platform !== "win32"
156
+ // Create process group on Unix
157
+ });
158
+ const timeoutHandle = setTimeout(() => {
159
+ killed = true;
160
+ if (child.pid) {
161
+ void this.killProcessTree(child.pid, child);
162
+ } else {
163
+ child.kill("SIGTERM");
164
+ }
165
+ }, options.timeout);
166
+ const abortHandler = () => {
167
+ if (closed) return;
168
+ aborted = true;
169
+ this.logger.debug(`Command cancelled by user: ${command}`);
170
+ clearTimeout(timeoutHandle);
171
+ if (child.pid) {
172
+ void this.killProcessTree(child.pid, child);
173
+ } else {
174
+ child.kill("SIGTERM");
175
+ }
176
+ };
177
+ options.abortSignal?.addEventListener("abort", abortHandler, { once: true });
178
+ child.stdout?.on("data", (data) => {
179
+ if (outputTruncated) return;
180
+ const chunk = data.toString();
181
+ const chunkBytes = Buffer.byteLength(chunk, "utf8");
182
+ if (stdoutBytes + stderrBytes + chunkBytes <= maxBuffer) {
183
+ stdout += chunk;
184
+ stdoutBytes += chunkBytes;
185
+ } else {
186
+ const remaining = maxBuffer - stdoutBytes - stderrBytes;
187
+ if (remaining > 0) {
188
+ stdout += chunk.slice(0, remaining);
189
+ stdoutBytes += remaining;
190
+ }
191
+ stdout += "\n...[truncated]";
192
+ outputTruncated = true;
193
+ this.logger.warn(`Output buffer full for command: ${command}`);
194
+ }
195
+ });
196
+ child.stderr?.on("data", (data) => {
197
+ if (outputTruncated) return;
198
+ const chunk = data.toString();
199
+ const chunkBytes = Buffer.byteLength(chunk, "utf8");
200
+ if (stdoutBytes + stderrBytes + chunkBytes <= maxBuffer) {
201
+ stderr += chunk;
202
+ stderrBytes += chunkBytes;
203
+ } else {
204
+ const remaining = maxBuffer - stdoutBytes - stderrBytes;
205
+ if (remaining > 0) {
206
+ stderr += chunk.slice(0, remaining);
207
+ stderrBytes += remaining;
208
+ }
209
+ stderr += "\n...[truncated]";
210
+ outputTruncated = true;
211
+ this.logger.warn(`Output buffer full for command: ${command}`);
212
+ }
213
+ });
214
+ child.on("close", (code, signal) => {
215
+ closed = true;
216
+ clearTimeout(timeoutHandle);
217
+ options.abortSignal?.removeEventListener("abort", abortHandler);
218
+ const duration = Date.now() - startTime;
219
+ if (aborted) {
220
+ stdout += "\n\n(Command was cancelled)";
221
+ this.logger.debug(`Command cancelled after ${duration}ms: ${command}`);
222
+ resolve({
223
+ stdout,
224
+ stderr,
225
+ exitCode: 130,
226
+ // Standard exit code for SIGINT
227
+ duration
228
+ });
229
+ return;
230
+ }
231
+ if (killed) {
232
+ reject(ProcessError.timeout(command, options.timeout));
233
+ return;
234
+ }
235
+ let exitCode = typeof code === "number" ? code : 1;
236
+ if (code === null) {
237
+ stderr += `
238
+ Process terminated by signal ${signal ?? "UNKNOWN"}`;
239
+ }
240
+ this.logger.debug(
241
+ `Command completed with exit code ${exitCode} in ${duration}ms: ${command}`
242
+ );
243
+ resolve({
244
+ stdout,
245
+ stderr,
246
+ exitCode,
247
+ duration
248
+ });
249
+ });
250
+ child.on("error", (error) => {
251
+ clearTimeout(timeoutHandle);
252
+ options.abortSignal?.removeEventListener("abort", abortHandler);
253
+ if (error.code === "ENOENT") {
254
+ reject(ProcessError.commandNotFound(command));
255
+ } else if (error.code === "EACCES") {
256
+ reject(ProcessError.permissionDenied(command));
257
+ } else {
258
+ reject(ProcessError.executionFailed(command, error.message));
259
+ }
260
+ });
261
+ });
262
+ }
263
+ /**
264
+ * Execute command in background
265
+ */
266
+ async executeInBackground(command, options) {
267
+ const runningCount = Array.from(this.backgroundProcesses.values()).filter(
268
+ (p) => p.status === "running"
269
+ ).length;
270
+ if (runningCount >= this.config.maxConcurrentProcesses) {
271
+ throw ProcessError.tooManyProcesses(runningCount, this.config.maxConcurrentProcesses);
272
+ }
273
+ const processId = crypto.randomBytes(4).toString("hex");
274
+ const cwd = this.resolveSafeCwd(options.cwd);
275
+ const env = {};
276
+ for (const [key, value] of Object.entries({
277
+ ...process.env,
278
+ ...this.config.environment,
279
+ ...options.env
280
+ })) {
281
+ if (value !== void 0) {
282
+ env[key] = value;
283
+ }
284
+ }
285
+ this.logger.debug(`Starting background process ${processId}: ${command}`);
286
+ const child = spawn(command, {
287
+ cwd,
288
+ env,
289
+ shell: true,
290
+ detached: false
291
+ });
292
+ const outputBuffer = {
293
+ stdout: [],
294
+ stderr: [],
295
+ complete: false,
296
+ lastRead: Date.now(),
297
+ bytesUsed: 0,
298
+ truncated: false
299
+ };
300
+ const bgProcess = {
301
+ processId,
302
+ command,
303
+ child,
304
+ startedAt: /* @__PURE__ */ new Date(),
305
+ status: "running",
306
+ outputBuffer,
307
+ description: options.description
308
+ };
309
+ this.backgroundProcesses.set(processId, bgProcess);
310
+ const bgTimeout = Math.max(
311
+ 1,
312
+ Math.min(options.timeout || DEFAULT_TIMEOUT, this.config.maxTimeout)
313
+ );
314
+ let killEscalationTimer = null;
315
+ const killTimer = setTimeout(() => {
316
+ if (bgProcess.status === "running") {
317
+ this.logger.warn(
318
+ `Background process ${processId} timed out after ${bgTimeout}ms, sending SIGTERM`
319
+ );
320
+ child.kill("SIGTERM");
321
+ killEscalationTimer = setTimeout(() => {
322
+ if (bgProcess.status === "running") {
323
+ this.logger.warn(
324
+ `Background process ${processId} did not respond to SIGTERM, sending SIGKILL`
325
+ );
326
+ child.kill("SIGKILL");
327
+ }
328
+ }, 5e3);
329
+ }
330
+ }, bgTimeout);
331
+ child.stdout?.on("data", (data) => {
332
+ const chunk = data.toString();
333
+ const chunkBytes = Buffer.byteLength(chunk, "utf8");
334
+ if (outputBuffer.bytesUsed + chunkBytes <= this.config.maxOutputBuffer) {
335
+ outputBuffer.stdout.push(chunk);
336
+ outputBuffer.bytesUsed += chunkBytes;
337
+ } else {
338
+ if (!outputBuffer.truncated) {
339
+ outputBuffer.truncated = true;
340
+ this.logger.warn(`Output buffer full for process ${processId}`);
341
+ }
342
+ }
343
+ });
344
+ child.stderr?.on("data", (data) => {
345
+ const chunk = data.toString();
346
+ const chunkBytes = Buffer.byteLength(chunk, "utf8");
347
+ if (outputBuffer.bytesUsed + chunkBytes <= this.config.maxOutputBuffer) {
348
+ outputBuffer.stderr.push(chunk);
349
+ outputBuffer.bytesUsed += chunkBytes;
350
+ } else {
351
+ if (!outputBuffer.truncated) {
352
+ outputBuffer.truncated = true;
353
+ this.logger.warn(`Error buffer full for process ${processId}`);
354
+ }
355
+ }
356
+ });
357
+ child.on("close", (code) => {
358
+ clearTimeout(killTimer);
359
+ if (killEscalationTimer) clearTimeout(killEscalationTimer);
360
+ bgProcess.status = code === 0 ? "completed" : "failed";
361
+ bgProcess.exitCode = code ?? void 0;
362
+ bgProcess.completedAt = /* @__PURE__ */ new Date();
363
+ bgProcess.outputBuffer.complete = true;
364
+ this.logger.debug(`Background process ${processId} completed with exit code ${code}`);
365
+ });
366
+ child.on("error", (error) => {
367
+ clearTimeout(killTimer);
368
+ if (killEscalationTimer) clearTimeout(killEscalationTimer);
369
+ bgProcess.status = "failed";
370
+ bgProcess.completedAt = /* @__PURE__ */ new Date();
371
+ bgProcess.outputBuffer.complete = true;
372
+ const chunk = `Error: ${error.message}`;
373
+ const chunkBytes = Buffer.byteLength(chunk, "utf8");
374
+ if (bgProcess.outputBuffer.bytesUsed + chunkBytes <= this.config.maxOutputBuffer) {
375
+ bgProcess.outputBuffer.stderr.push(chunk);
376
+ bgProcess.outputBuffer.bytesUsed += chunkBytes;
377
+ } else {
378
+ if (!bgProcess.outputBuffer.truncated) {
379
+ bgProcess.outputBuffer.truncated = true;
380
+ this.logger.warn(`Error buffer full for process ${processId}`);
381
+ }
382
+ }
383
+ this.logger.error(`Background process ${processId} failed: ${error.message}`);
384
+ });
385
+ return {
386
+ processId,
387
+ command,
388
+ pid: child.pid,
389
+ startedAt: bgProcess.startedAt,
390
+ description: options.description
391
+ };
392
+ }
393
+ /**
394
+ * Get output from a background process
395
+ */
396
+ async getProcessOutput(processId) {
397
+ await this.ensureInitialized();
398
+ const bgProcess = this.backgroundProcesses.get(processId);
399
+ if (!bgProcess) {
400
+ throw ProcessError.processNotFound(processId);
401
+ }
402
+ const stdout = bgProcess.outputBuffer.stdout.join("");
403
+ const stderr = bgProcess.outputBuffer.stderr.join("");
404
+ bgProcess.outputBuffer.stdout = [];
405
+ bgProcess.outputBuffer.stderr = [];
406
+ bgProcess.outputBuffer.lastRead = Date.now();
407
+ bgProcess.outputBuffer.bytesUsed = 0;
408
+ return {
409
+ stdout,
410
+ stderr,
411
+ status: bgProcess.status,
412
+ exitCode: bgProcess.exitCode,
413
+ duration: bgProcess.completedAt ? bgProcess.completedAt.getTime() - bgProcess.startedAt.getTime() : void 0
414
+ };
415
+ }
416
+ /**
417
+ * Kill a background process
418
+ */
419
+ async killProcess(processId) {
420
+ await this.ensureInitialized();
421
+ const bgProcess = this.backgroundProcesses.get(processId);
422
+ if (!bgProcess) {
423
+ throw ProcessError.processNotFound(processId);
424
+ }
425
+ if (bgProcess.status !== "running") {
426
+ this.logger.debug(`Process ${processId} is not running (status: ${bgProcess.status})`);
427
+ return;
428
+ }
429
+ try {
430
+ bgProcess.child.kill("SIGTERM");
431
+ setTimeout(() => {
432
+ if (bgProcess.child.exitCode === null) {
433
+ bgProcess.child.kill("SIGKILL");
434
+ }
435
+ }, 5e3);
436
+ this.logger.debug(`Process ${processId} sent SIGTERM`);
437
+ } catch (error) {
438
+ throw ProcessError.killFailed(
439
+ processId,
440
+ error instanceof Error ? error.message : String(error)
441
+ );
442
+ }
443
+ }
444
+ /**
445
+ * List all background processes
446
+ */
447
+ async listProcesses() {
448
+ await this.ensureInitialized();
449
+ return Array.from(this.backgroundProcesses.values()).map((bgProcess) => ({
450
+ processId: bgProcess.processId,
451
+ command: bgProcess.command,
452
+ pid: bgProcess.child.pid,
453
+ status: bgProcess.status,
454
+ startedAt: bgProcess.startedAt,
455
+ completedAt: bgProcess.completedAt,
456
+ exitCode: bgProcess.exitCode,
457
+ description: bgProcess.description
458
+ }));
459
+ }
460
+ /**
461
+ * Get buffer size in bytes
462
+ */
463
+ getBufferSize(buffer) {
464
+ const stdoutSize = buffer.stdout.reduce((sum, line) => sum + line.length, 0);
465
+ const stderrSize = buffer.stderr.reduce((sum, line) => sum + line.length, 0);
466
+ return stdoutSize + stderrSize;
467
+ }
468
+ /**
469
+ * Get service configuration
470
+ */
471
+ getConfig() {
472
+ return { ...this.config };
473
+ }
474
+ /**
475
+ * Resolve and confine cwd to the configured working directory
476
+ */
477
+ resolveSafeCwd(cwd) {
478
+ const baseDir = this.config.workingDirectory || process.cwd();
479
+ if (!cwd) return baseDir;
480
+ const candidate = path.isAbsolute(cwd) ? path.resolve(cwd) : path.resolve(baseDir, cwd);
481
+ const rel = path.relative(baseDir, candidate);
482
+ const outside = rel.startsWith("..") || path.isAbsolute(rel);
483
+ if (outside) {
484
+ throw ProcessError.invalidWorkingDirectory(
485
+ cwd,
486
+ `Working directory must be within ${baseDir}`
487
+ );
488
+ }
489
+ return candidate;
490
+ }
491
+ /**
492
+ * Cleanup completed processes
493
+ */
494
+ async cleanup() {
495
+ const now = Date.now();
496
+ const CLEANUP_AGE = 36e5;
497
+ for (const [processId, bgProcess] of this.backgroundProcesses.entries()) {
498
+ if (bgProcess.status !== "running" && bgProcess.completedAt) {
499
+ const age = now - bgProcess.completedAt.getTime();
500
+ if (age > CLEANUP_AGE) {
501
+ this.backgroundProcesses.delete(processId);
502
+ this.logger.debug(`Cleaned up old process ${processId}`);
503
+ }
504
+ }
505
+ }
506
+ }
507
+ }
508
+ export {
509
+ ProcessService
510
+ };