runtimeuse 0.2.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 (120) hide show
  1. package/.env.example +4 -0
  2. package/README.md +222 -0
  3. package/dist/agent-handler.d.ts +26 -0
  4. package/dist/agent-handler.d.ts.map +1 -0
  5. package/dist/agent-handler.js +2 -0
  6. package/dist/agent-handler.js.map +1 -0
  7. package/dist/artifact-manager.d.ts +27 -0
  8. package/dist/artifact-manager.d.ts.map +1 -0
  9. package/dist/artifact-manager.js +125 -0
  10. package/dist/artifact-manager.js.map +1 -0
  11. package/dist/artifact-manager.test.d.ts +2 -0
  12. package/dist/artifact-manager.test.d.ts.map +1 -0
  13. package/dist/artifact-manager.test.js +251 -0
  14. package/dist/artifact-manager.test.js.map +1 -0
  15. package/dist/claude-handler.d.ts +3 -0
  16. package/dist/claude-handler.d.ts.map +1 -0
  17. package/dist/claude-handler.js +76 -0
  18. package/dist/claude-handler.js.map +1 -0
  19. package/dist/cli.d.ts +3 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +87 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/command-handler.d.ts +22 -0
  24. package/dist/command-handler.d.ts.map +1 -0
  25. package/dist/command-handler.js +75 -0
  26. package/dist/command-handler.js.map +1 -0
  27. package/dist/command-handler.test.d.ts +2 -0
  28. package/dist/command-handler.test.d.ts.map +1 -0
  29. package/dist/command-handler.test.js +267 -0
  30. package/dist/command-handler.test.js.map +1 -0
  31. package/dist/constants.d.ts +3 -0
  32. package/dist/constants.d.ts.map +1 -0
  33. package/dist/constants.js +13 -0
  34. package/dist/constants.js.map +1 -0
  35. package/dist/default-handler.d.ts +3 -0
  36. package/dist/default-handler.d.ts.map +1 -0
  37. package/dist/default-handler.js +76 -0
  38. package/dist/default-handler.js.map +1 -0
  39. package/dist/download-handler.d.ts +8 -0
  40. package/dist/download-handler.d.ts.map +1 -0
  41. package/dist/download-handler.js +36 -0
  42. package/dist/download-handler.js.map +1 -0
  43. package/dist/download-handler.test.d.ts +2 -0
  44. package/dist/download-handler.test.d.ts.map +1 -0
  45. package/dist/download-handler.test.js +123 -0
  46. package/dist/download-handler.test.js.map +1 -0
  47. package/dist/index.d.ts +20 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +21 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/logger.d.ts +8 -0
  52. package/dist/logger.d.ts.map +1 -0
  53. package/dist/logger.js +14 -0
  54. package/dist/logger.js.map +1 -0
  55. package/dist/openai-handler.d.ts +3 -0
  56. package/dist/openai-handler.d.ts.map +1 -0
  57. package/dist/openai-handler.js +86 -0
  58. package/dist/openai-handler.js.map +1 -0
  59. package/dist/server.d.ts +21 -0
  60. package/dist/server.d.ts.map +1 -0
  61. package/dist/server.js +52 -0
  62. package/dist/server.js.map +1 -0
  63. package/dist/session.d.ts +29 -0
  64. package/dist/session.d.ts.map +1 -0
  65. package/dist/session.js +244 -0
  66. package/dist/session.js.map +1 -0
  67. package/dist/session.test.d.ts +2 -0
  68. package/dist/session.test.d.ts.map +1 -0
  69. package/dist/session.test.js +339 -0
  70. package/dist/session.test.js.map +1 -0
  71. package/dist/storage.d.ts +3 -0
  72. package/dist/storage.d.ts.map +1 -0
  73. package/dist/storage.js +21 -0
  74. package/dist/storage.js.map +1 -0
  75. package/dist/types.d.ts +62 -0
  76. package/dist/types.d.ts.map +1 -0
  77. package/dist/types.js +2 -0
  78. package/dist/types.js.map +1 -0
  79. package/dist/upload-tracker.d.ts +10 -0
  80. package/dist/upload-tracker.d.ts.map +1 -0
  81. package/dist/upload-tracker.js +27 -0
  82. package/dist/upload-tracker.js.map +1 -0
  83. package/dist/upload-tracker.test.d.ts +2 -0
  84. package/dist/upload-tracker.test.d.ts.map +1 -0
  85. package/dist/upload-tracker.test.js +89 -0
  86. package/dist/upload-tracker.test.js.map +1 -0
  87. package/dist/utils.d.ts +7 -0
  88. package/dist/utils.d.ts.map +1 -0
  89. package/dist/utils.js +32 -0
  90. package/dist/utils.js.map +1 -0
  91. package/dist/utils.test.d.ts +2 -0
  92. package/dist/utils.test.d.ts.map +1 -0
  93. package/dist/utils.test.js +92 -0
  94. package/dist/utils.test.js.map +1 -0
  95. package/package.json +40 -0
  96. package/scripts/dev-publish.sh +45 -0
  97. package/src/agent-handler.ts +26 -0
  98. package/src/artifact-manager.test.ts +320 -0
  99. package/src/artifact-manager.ts +170 -0
  100. package/src/claude-handler.ts +95 -0
  101. package/src/cli.ts +107 -0
  102. package/src/command-handler.test.ts +507 -0
  103. package/src/command-handler.ts +102 -0
  104. package/src/constants.ts +12 -0
  105. package/src/download-handler.test.ts +183 -0
  106. package/src/download-handler.ts +45 -0
  107. package/src/index.ts +59 -0
  108. package/src/logger.ts +20 -0
  109. package/src/openai-handler.ts +120 -0
  110. package/src/server.ts +68 -0
  111. package/src/session.test.ts +448 -0
  112. package/src/session.ts +319 -0
  113. package/src/storage.ts +28 -0
  114. package/src/types.ts +101 -0
  115. package/src/upload-tracker.test.ts +112 -0
  116. package/src/upload-tracker.ts +30 -0
  117. package/src/utils.test.ts +120 -0
  118. package/src/utils.ts +35 -0
  119. package/tsconfig.json +20 -0
  120. package/vitest.config.ts +7 -0
@@ -0,0 +1,507 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { Writable } from "node:stream";
3
+ import type { Logger } from "./logger.js";
4
+
5
+ vi.mock("fs", async () => {
6
+ const actual = await vi.importActual<typeof import("fs")>("fs");
7
+ return {
8
+ ...actual,
9
+ default: {
10
+ ...actual,
11
+ existsSync: vi.fn(() => true),
12
+ mkdirSync: vi.fn(),
13
+ },
14
+ };
15
+ });
16
+
17
+ let execFileCallback: Function;
18
+ let execFileChild: any;
19
+
20
+ vi.mock("node:child_process", () => ({
21
+ execFile: vi.fn((_cmd: string, _opts: any, cb: Function) => {
22
+ execFileCallback = cb;
23
+ execFileChild = {
24
+ on: vi.fn(),
25
+ stdout: { on: vi.fn() },
26
+ stderr: { on: vi.fn() },
27
+ };
28
+ return execFileChild;
29
+ }),
30
+ }));
31
+
32
+ import fs from "fs";
33
+ import { execFile } from "node:child_process";
34
+ import CommandHandler from "./command-handler.js";
35
+ import type { Command } from "./types.js";
36
+
37
+ const mockLogger: Logger = {
38
+ log: vi.fn(),
39
+ error: vi.fn(),
40
+ debug: vi.fn(),
41
+ };
42
+
43
+ function createStreams() {
44
+ const stdoutChunks: string[] = [];
45
+ const stderrChunks: string[] = [];
46
+
47
+ const stdoutStream = new Writable({
48
+ write(chunk, _enc, cb) {
49
+ stdoutChunks.push(chunk.toString());
50
+ cb();
51
+ },
52
+ });
53
+
54
+ const stderrStream = new Writable({
55
+ write(chunk, _enc, cb) {
56
+ stderrChunks.push(chunk.toString());
57
+ cb();
58
+ },
59
+ });
60
+
61
+ return { stdoutStream, stderrStream, stdoutChunks, stderrChunks };
62
+ }
63
+
64
+ function createHandler(
65
+ command: Command,
66
+ stdoutStream: Writable,
67
+ stderrStream: Writable,
68
+ abortController = new AbortController(),
69
+ ) {
70
+ return new CommandHandler(
71
+ command,
72
+ mockLogger,
73
+ stdoutStream,
74
+ stderrStream,
75
+ abortController,
76
+ );
77
+ }
78
+
79
+ describe("CommandHandler", () => {
80
+ beforeEach(() => {
81
+ vi.clearAllMocks();
82
+ });
83
+
84
+ describe("directory creation", () => {
85
+ it("creates cwd directory when it does not exist", async () => {
86
+ vi.mocked(fs.existsSync).mockReturnValueOnce(false);
87
+ const { stdoutStream, stderrStream } = createStreams();
88
+ const handler = createHandler(
89
+ { command: "echo", cwd: "/tmp/test-dir" },
90
+ stdoutStream,
91
+ stderrStream,
92
+ );
93
+
94
+ const promise = handler.execute();
95
+
96
+ execFileCallback(null, "ok", "");
97
+ const closeHandler = execFileChild.on.mock.calls.find(
98
+ (c: unknown[]) => c[0] === "close",
99
+ )[1];
100
+ closeHandler(0);
101
+
102
+ await promise;
103
+
104
+ expect(fs.mkdirSync).toHaveBeenCalledWith("/tmp/test-dir", {
105
+ recursive: true,
106
+ });
107
+ });
108
+
109
+ it("does not create cwd directory when it already exists", async () => {
110
+ vi.mocked(fs.existsSync).mockReturnValueOnce(true);
111
+ const { stdoutStream, stderrStream } = createStreams();
112
+ const handler = createHandler(
113
+ { command: "echo", cwd: "/tmp/existing" },
114
+ stdoutStream,
115
+ stderrStream,
116
+ );
117
+
118
+ const promise = handler.execute();
119
+
120
+ execFileCallback(null, "", "");
121
+ const closeHandler = execFileChild.on.mock.calls.find(
122
+ (c: unknown[]) => c[0] === "close",
123
+ )[1];
124
+ closeHandler(0);
125
+
126
+ await promise;
127
+
128
+ expect(fs.mkdirSync).not.toHaveBeenCalled();
129
+ });
130
+
131
+ it("skips directory creation when cwd is not specified", async () => {
132
+ const { stdoutStream, stderrStream } = createStreams();
133
+ const handler = createHandler(
134
+ { command: "echo" },
135
+ stdoutStream,
136
+ stderrStream,
137
+ );
138
+
139
+ const promise = handler.execute();
140
+
141
+ execFileCallback(null, "", "");
142
+ const closeHandler = execFileChild.on.mock.calls.find(
143
+ (c: unknown[]) => c[0] === "close",
144
+ )[1];
145
+ closeHandler(0);
146
+
147
+ await promise;
148
+
149
+ expect(fs.existsSync).not.toHaveBeenCalled();
150
+ expect(fs.mkdirSync).not.toHaveBeenCalled();
151
+ });
152
+ });
153
+
154
+ describe("command execution", () => {
155
+ it("calls execFile with the right command, cwd, and env", () => {
156
+ const { stdoutStream, stderrStream } = createStreams();
157
+ const handler = createHandler(
158
+ { command: "ls", cwd: "/work", env: { FOO: "bar" } },
159
+ stdoutStream,
160
+ stderrStream,
161
+ );
162
+
163
+ handler.execute();
164
+
165
+ expect(execFile).toHaveBeenCalledWith(
166
+ "ls",
167
+ expect.objectContaining({
168
+ cwd: "/work",
169
+ env: expect.objectContaining({ FOO: "bar" }),
170
+ }),
171
+ expect.any(Function),
172
+ );
173
+ });
174
+
175
+ it("uses process.cwd() when cwd is not specified", () => {
176
+ const { stdoutStream, stderrStream } = createStreams();
177
+ const handler = createHandler(
178
+ { command: "pwd" },
179
+ stdoutStream,
180
+ stderrStream,
181
+ );
182
+
183
+ handler.execute();
184
+
185
+ expect(execFile).toHaveBeenCalledWith(
186
+ "pwd",
187
+ expect.objectContaining({ cwd: process.cwd() }),
188
+ expect.any(Function),
189
+ );
190
+ });
191
+
192
+ it("passes abort signal to execFile", () => {
193
+ const ac = new AbortController();
194
+ const { stdoutStream, stderrStream } = createStreams();
195
+ const handler = createHandler(
196
+ { command: "sleep" },
197
+ stdoutStream,
198
+ stderrStream,
199
+ ac,
200
+ );
201
+
202
+ handler.execute();
203
+
204
+ expect(execFile).toHaveBeenCalledWith(
205
+ "sleep",
206
+ expect.objectContaining({ signal: ac.signal }),
207
+ expect.any(Function),
208
+ );
209
+ });
210
+ });
211
+
212
+ describe("exit handling", () => {
213
+ it("resolves with exitCode 0 on successful close", async () => {
214
+ const { stdoutStream, stderrStream } = createStreams();
215
+ const handler = createHandler(
216
+ { command: "true" },
217
+ stdoutStream,
218
+ stderrStream,
219
+ );
220
+
221
+ const promise = handler.execute();
222
+
223
+ execFileCallback(null, "output", "");
224
+ const closeHandler = execFileChild.on.mock.calls.find(
225
+ (c: unknown[]) => c[0] === "close",
226
+ )[1];
227
+ closeHandler(0);
228
+
229
+ const result = await promise;
230
+ expect(result).toEqual({ exitCode: 0 });
231
+ });
232
+
233
+ it("resolves with exitCode 1 on close with code 1", async () => {
234
+ const { stdoutStream, stderrStream } = createStreams();
235
+ const handler = createHandler(
236
+ { command: "false" },
237
+ stdoutStream,
238
+ stderrStream,
239
+ );
240
+
241
+ const promise = handler.execute();
242
+
243
+ execFileCallback(null, "", "");
244
+ const closeHandler = execFileChild.on.mock.calls.find(
245
+ (c: unknown[]) => c[0] === "close",
246
+ )[1];
247
+ closeHandler(1);
248
+
249
+ const result = await promise;
250
+ expect(result).toEqual({ exitCode: 1 });
251
+ });
252
+
253
+ it("resolves with exitCode 1 and error when callback reports code 1", async () => {
254
+ const { stdoutStream, stderrStream } = createStreams();
255
+ const handler = createHandler(
256
+ { command: "failing" },
257
+ stdoutStream,
258
+ stderrStream,
259
+ );
260
+
261
+ const promise = handler.execute();
262
+
263
+ const err = Object.assign(new Error("exit 1"), { code: 1 });
264
+ execFileCallback(err, "", "some error");
265
+
266
+ const result = await promise;
267
+ expect(result.exitCode).toBe(1);
268
+ expect(result.error).toBeDefined();
269
+ });
270
+
271
+ it("rejects with non-0/1 exit codes from close event", async () => {
272
+ const { stdoutStream, stderrStream } = createStreams();
273
+ const handler = createHandler(
274
+ { command: "segfault" },
275
+ stdoutStream,
276
+ stderrStream,
277
+ );
278
+
279
+ const promise = handler.execute();
280
+
281
+ execFileCallback(null, "", "");
282
+ const closeHandler = execFileChild.on.mock.calls.find(
283
+ (c: unknown[]) => c[0] === "close",
284
+ )[1];
285
+ closeHandler(139);
286
+
287
+ await expect(promise).rejects.toEqual({ exitCode: 139 });
288
+ });
289
+
290
+ it("rejects when callback reports a non-1 error code", async () => {
291
+ const { stdoutStream, stderrStream } = createStreams();
292
+ const handler = createHandler(
293
+ { command: "crash" },
294
+ stdoutStream,
295
+ stderrStream,
296
+ );
297
+
298
+ const promise = handler.execute();
299
+
300
+ const err = Object.assign(new Error("killed"), { code: 137 });
301
+ execFileCallback(err, "", "");
302
+
303
+ await expect(promise).rejects.toMatchObject({ exitCode: 137 });
304
+ });
305
+ });
306
+
307
+ describe("stream output", () => {
308
+ it("writes stdout to the stdout stream", async () => {
309
+ const { stdoutStream, stderrStream, stdoutChunks } = createStreams();
310
+ const handler = createHandler(
311
+ { command: "echo" },
312
+ stdoutStream,
313
+ stderrStream,
314
+ );
315
+
316
+ const promise = handler.execute();
317
+
318
+ const stdoutDataHandler = execFileChild.stdout.on.mock.calls.find(
319
+ (c: unknown[]) => c[0] === "data",
320
+ )[1];
321
+ stdoutDataHandler("hello world");
322
+
323
+ execFileCallback(null, "", "");
324
+ const closeHandler = execFileChild.on.mock.calls.find(
325
+ (c: unknown[]) => c[0] === "close",
326
+ )[1];
327
+ closeHandler(0);
328
+
329
+ await promise;
330
+ expect(stdoutChunks).toContain("hello world");
331
+ });
332
+
333
+ it("writes stderr to the stderr stream", async () => {
334
+ const { stdoutStream, stderrStream, stderrChunks } = createStreams();
335
+ const handler = createHandler(
336
+ { command: "warn" },
337
+ stdoutStream,
338
+ stderrStream,
339
+ );
340
+
341
+ const promise = handler.execute();
342
+
343
+ const stderrDataHandler = execFileChild.stderr.on.mock.calls.find(
344
+ (c: unknown[]) => c[0] === "data",
345
+ )[1];
346
+ stderrDataHandler("warning message");
347
+
348
+ execFileCallback(null, "", "");
349
+ const closeHandler = execFileChild.on.mock.calls.find(
350
+ (c: unknown[]) => c[0] === "close",
351
+ )[1];
352
+ closeHandler(0);
353
+
354
+ await promise;
355
+ expect(stderrChunks).toContain("warning message");
356
+ });
357
+
358
+ it("redacts secrets from stdout", async () => {
359
+ const { stdoutStream, stderrStream, stdoutChunks } = createStreams();
360
+ const handler = createHandler(
361
+ { command: "echo", env: { API_KEY: "s3cret" } },
362
+ stdoutStream,
363
+ stderrStream,
364
+ );
365
+
366
+ const promise = handler.execute();
367
+
368
+ const stdoutDataHandler = execFileChild.stdout.on.mock.calls.find(
369
+ (c: unknown[]) => c[0] === "data",
370
+ )[1];
371
+ stdoutDataHandler("token is s3cret ok");
372
+
373
+ execFileCallback(null, "", "");
374
+ const closeHandler = execFileChild.on.mock.calls.find(
375
+ (c: unknown[]) => c[0] === "close",
376
+ )[1];
377
+ closeHandler(0);
378
+
379
+ await promise;
380
+ expect(stdoutChunks).toEqual(["token is [REDACTED] ok"]);
381
+ });
382
+
383
+ it("redacts secrets from stderr", async () => {
384
+ const { stdoutStream, stderrStream, stderrChunks } = createStreams();
385
+ const handler = createHandler(
386
+ { command: "warn", env: { PASSWORD: "hunter2" } },
387
+ stdoutStream,
388
+ stderrStream,
389
+ );
390
+
391
+ const promise = handler.execute();
392
+
393
+ const stderrDataHandler = execFileChild.stderr.on.mock.calls.find(
394
+ (c: unknown[]) => c[0] === "data",
395
+ )[1];
396
+ stderrDataHandler("error: hunter2 leaked");
397
+
398
+ execFileCallback(null, "", "");
399
+ const closeHandler = execFileChild.on.mock.calls.find(
400
+ (c: unknown[]) => c[0] === "close",
401
+ )[1];
402
+ closeHandler(0);
403
+
404
+ await promise;
405
+ expect(stderrChunks).toEqual(["error: [REDACTED] leaked"]);
406
+ });
407
+
408
+ it("redacts multiple env secrets from output", async () => {
409
+ const { stdoutStream, stderrStream, stdoutChunks } = createStreams();
410
+ const handler = createHandler(
411
+ { command: "echo", env: { KEY: "aaa", TOKEN: "bbb" } },
412
+ stdoutStream,
413
+ stderrStream,
414
+ );
415
+
416
+ const promise = handler.execute();
417
+
418
+ const stdoutDataHandler = execFileChild.stdout.on.mock.calls.find(
419
+ (c: unknown[]) => c[0] === "data",
420
+ )[1];
421
+ stdoutDataHandler("aaa and bbb");
422
+
423
+ execFileCallback(null, "", "");
424
+ const closeHandler = execFileChild.on.mock.calls.find(
425
+ (c: unknown[]) => c[0] === "close",
426
+ )[1];
427
+ closeHandler(0);
428
+
429
+ await promise;
430
+ expect(stdoutChunks).toEqual(["[REDACTED] and [REDACTED]"]);
431
+ });
432
+
433
+ it("passes redacted data to onStdout callback", async () => {
434
+ const { stdoutStream, stderrStream } = createStreams();
435
+ const onStdout = vi.fn();
436
+ const handler = new CommandHandler(
437
+ { command: "echo", env: { SECRET: "xyz" } },
438
+ mockLogger,
439
+ stdoutStream,
440
+ stderrStream,
441
+ new AbortController(),
442
+ onStdout,
443
+ );
444
+
445
+ const promise = handler.execute();
446
+
447
+ const stdoutDataHandler = execFileChild.stdout.on.mock.calls.find(
448
+ (c: unknown[]) => c[0] === "data",
449
+ )[1];
450
+ stdoutDataHandler("value=xyz");
451
+
452
+ execFileCallback(null, "", "");
453
+ const closeHandler = execFileChild.on.mock.calls.find(
454
+ (c: unknown[]) => c[0] === "close",
455
+ )[1];
456
+ closeHandler(0);
457
+
458
+ await promise;
459
+ expect(onStdout).toHaveBeenCalledWith("value=[REDACTED]");
460
+ });
461
+
462
+ it("does not redact when command has no env", async () => {
463
+ const { stdoutStream, stderrStream, stdoutChunks } = createStreams();
464
+ const handler = createHandler(
465
+ { command: "echo" },
466
+ stdoutStream,
467
+ stderrStream,
468
+ );
469
+
470
+ const promise = handler.execute();
471
+
472
+ const stdoutDataHandler = execFileChild.stdout.on.mock.calls.find(
473
+ (c: unknown[]) => c[0] === "data",
474
+ )[1];
475
+ stdoutDataHandler("nothing secret here");
476
+
477
+ execFileCallback(null, "", "");
478
+ const closeHandler = execFileChild.on.mock.calls.find(
479
+ (c: unknown[]) => c[0] === "close",
480
+ )[1];
481
+ closeHandler(0);
482
+
483
+ await promise;
484
+ expect(stdoutChunks).toEqual(["nothing secret here"]);
485
+ });
486
+
487
+ it("does not write empty stdout", async () => {
488
+ const { stdoutStream, stderrStream, stdoutChunks } = createStreams();
489
+ const handler = createHandler(
490
+ { command: "silent" },
491
+ stdoutStream,
492
+ stderrStream,
493
+ );
494
+
495
+ const promise = handler.execute();
496
+
497
+ execFileCallback(null, "", "");
498
+ const closeHandler = execFileChild.on.mock.calls.find(
499
+ (c: unknown[]) => c[0] === "close",
500
+ )[1];
501
+ closeHandler(0);
502
+
503
+ await promise;
504
+ expect(stdoutChunks).toHaveLength(0);
505
+ });
506
+ });
507
+ });
@@ -0,0 +1,102 @@
1
+ import { Writable } from "node:stream";
2
+ import { execFile, ExecFileException } from "node:child_process";
3
+ import fs from "fs";
4
+ import type { Logger } from "./logger.js";
5
+ import type { Command } from "./types.js";
6
+ import { redactSecrets } from "./utils.js";
7
+
8
+ export interface CommandResult {
9
+ exitCode: number;
10
+ error?: ExecFileException;
11
+ }
12
+ class CommandHandler {
13
+ private readonly command: Command;
14
+ private readonly logger: Logger;
15
+ private readonly stdoutStream: Writable;
16
+ private readonly stderrStream: Writable;
17
+ private readonly onStdout?: (stdout: string) => void;
18
+ private readonly onStderr?: (stderr: string) => void;
19
+ private readonly abortController: AbortController;
20
+ constructor(
21
+ command: Command,
22
+ logger: Logger,
23
+ stdoutStream: Writable,
24
+ stderrStream: Writable,
25
+ abortController: AbortController,
26
+ onStdout?: (stdout: string) => void,
27
+ onStderr?: (stderr: string) => void,
28
+ ) {
29
+ this.command = command;
30
+ this.logger = logger;
31
+ this.stdoutStream = stdoutStream;
32
+ this.stderrStream = stderrStream;
33
+ this.abortController = abortController;
34
+ this.onStdout = onStdout;
35
+ this.onStderr = onStderr;
36
+ }
37
+
38
+ private redactSecrets(data: string): string {
39
+ return redactSecrets(data, Object.values(this.command.env ?? {}));
40
+ }
41
+
42
+ async execute(): Promise<CommandResult> {
43
+ if (this.command.cwd) {
44
+ if (!fs.existsSync(this.command.cwd)) {
45
+ fs.mkdirSync(this.command.cwd, { recursive: true });
46
+ }
47
+ }
48
+ return new Promise((resolve, reject) => {
49
+ const result = execFile(
50
+ this.command.command,
51
+ {
52
+ cwd: this.command.cwd ?? process.cwd(),
53
+ env: { ...process.env, ...this.command.env },
54
+ signal: this.abortController.signal,
55
+ },
56
+ (error, stdout, stderr) => {
57
+ if (error) {
58
+ if (error.code === 1) {
59
+ return resolve({ exitCode: 1, error });
60
+ }
61
+ this.logger.error(
62
+ "Error executing command:",
63
+ JSON.stringify(error),
64
+ );
65
+ return reject({ exitCode: error.code, error });
66
+ }
67
+ },
68
+ );
69
+
70
+ result.stdout?.on("data", (data) => {
71
+ const redactedData = this.redactSecrets(data);
72
+ this.stdoutStream.write(redactedData);
73
+ this.onStdout?.(redactedData);
74
+ });
75
+ result.stderr?.on("data", (data) => {
76
+ const redactedData = this.redactSecrets(data);
77
+ this.stderrStream.write(redactedData);
78
+ this.onStderr?.(redactedData);
79
+ });
80
+ result.on("exit", (code) => {
81
+ this.logger.log("Command exited with code:", code);
82
+ });
83
+ result.on("error", (error) => {
84
+ this.logger.error("Command error:", error);
85
+ return reject({ exitCode: 2, error });
86
+ });
87
+ result.on("spawn", () => {
88
+ this.logger.log("Command spawned:", this.command.command);
89
+ });
90
+
91
+ result.on("close", (code) => {
92
+ if (code === 0 || code === 1) {
93
+ this.logger.log("Command exited with code:", code);
94
+ return resolve({ exitCode: code });
95
+ }
96
+ return reject({ exitCode: code });
97
+ });
98
+ });
99
+ }
100
+ }
101
+
102
+ export default CommandHandler;
@@ -0,0 +1,12 @@
1
+ export const DEFAULT_ARTIFACTS_DIR = "/tmp/artifacts";
2
+ export const DEFAULT_ARTIFACT_IGNORE = `
3
+ node_modules/
4
+ .venv/
5
+ .env/
6
+ env/
7
+ venv/
8
+ dist/
9
+ __pycache__/
10
+ __pytest_cache__/
11
+ .pytest_cache/
12
+ `;