@curl-runner/cli 1.9.0 → 1.11.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.
- package/package.json +1 -1
- package/src/cli.ts +234 -41
- package/src/executor/request-executor.ts +31 -1
- package/src/snapshot/index.ts +3 -0
- package/src/snapshot/snapshot-differ.test.ts +358 -0
- package/src/snapshot/snapshot-differ.ts +296 -0
- package/src/snapshot/snapshot-formatter.ts +170 -0
- package/src/snapshot/snapshot-manager.test.ts +204 -0
- package/src/snapshot/snapshot-manager.ts +342 -0
- package/src/types/config.ts +98 -0
- package/src/utils/logger.ts +49 -0
- package/src/watcher/file-watcher.test.ts +186 -0
- package/src/watcher/file-watcher.ts +140 -0
package/src/types/config.ts
CHANGED
|
@@ -147,6 +147,11 @@ export interface RequestConfig {
|
|
|
147
147
|
body?: JsonValue;
|
|
148
148
|
responseTime?: string; // Response time validation like "< 1000", "> 500, < 2000"
|
|
149
149
|
};
|
|
150
|
+
/**
|
|
151
|
+
* Snapshot configuration for this request.
|
|
152
|
+
* Use `true` to enable with defaults, or provide detailed config.
|
|
153
|
+
*/
|
|
154
|
+
snapshot?: SnapshotConfig | boolean;
|
|
150
155
|
sourceOutputConfig?: {
|
|
151
156
|
verbose?: boolean;
|
|
152
157
|
showHeaders?: boolean;
|
|
@@ -210,6 +215,16 @@ export interface GlobalConfig {
|
|
|
210
215
|
* Applied to all requests unless overridden at the request level.
|
|
211
216
|
*/
|
|
212
217
|
ssl?: SSLConfig;
|
|
218
|
+
/**
|
|
219
|
+
* Watch mode configuration.
|
|
220
|
+
* Automatically re-runs requests when YAML files change.
|
|
221
|
+
*/
|
|
222
|
+
watch?: WatchConfig;
|
|
223
|
+
/**
|
|
224
|
+
* Snapshot testing configuration.
|
|
225
|
+
* Saves response snapshots and compares future runs against them.
|
|
226
|
+
*/
|
|
227
|
+
snapshot?: GlobalSnapshotConfig;
|
|
213
228
|
variables?: Record<string, string>;
|
|
214
229
|
output?: {
|
|
215
230
|
verbose?: boolean;
|
|
@@ -247,6 +262,8 @@ export interface ExecutionResult {
|
|
|
247
262
|
firstByte?: number;
|
|
248
263
|
download?: number;
|
|
249
264
|
};
|
|
265
|
+
/** Snapshot comparison result (if snapshot testing enabled). */
|
|
266
|
+
snapshotResult?: SnapshotCompareResult;
|
|
250
267
|
}
|
|
251
268
|
|
|
252
269
|
export interface ExecutionSummary {
|
|
@@ -262,3 +279,84 @@ export interface ExecutionSummary {
|
|
|
262
279
|
* Values are stored as strings and can be referenced using ${store.variableName} syntax.
|
|
263
280
|
*/
|
|
264
281
|
export type ResponseStoreContext = Record<string, string>;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Configuration for watch mode.
|
|
285
|
+
* Watch mode automatically re-runs requests when YAML files change.
|
|
286
|
+
*/
|
|
287
|
+
export interface WatchConfig {
|
|
288
|
+
/** Enable watch mode. Default: false */
|
|
289
|
+
enabled?: boolean;
|
|
290
|
+
/** Debounce delay in milliseconds. Default: 300 */
|
|
291
|
+
debounce?: number;
|
|
292
|
+
/** Clear screen between runs. Default: true */
|
|
293
|
+
clear?: boolean;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Configuration for snapshot testing.
|
|
298
|
+
* Snapshots save response data and compare future runs against them.
|
|
299
|
+
*/
|
|
300
|
+
export interface SnapshotConfig {
|
|
301
|
+
/** Enable snapshot testing for this request. */
|
|
302
|
+
enabled?: boolean;
|
|
303
|
+
/** Custom snapshot name (defaults to request name). */
|
|
304
|
+
name?: string;
|
|
305
|
+
/** What to include in snapshot. Default: ['body'] */
|
|
306
|
+
include?: ('body' | 'status' | 'headers')[];
|
|
307
|
+
/** Paths to exclude from comparison (e.g., 'body.timestamp'). */
|
|
308
|
+
exclude?: string[];
|
|
309
|
+
/** Match rules for dynamic values (path -> '*' or 'regex:pattern'). */
|
|
310
|
+
match?: Record<string, string>;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Global snapshot configuration.
|
|
315
|
+
*/
|
|
316
|
+
export interface GlobalSnapshotConfig extends SnapshotConfig {
|
|
317
|
+
/** Directory for snapshot files. Default: '__snapshots__' */
|
|
318
|
+
dir?: string;
|
|
319
|
+
/** Update mode: 'none' | 'all' | 'failing'. Default: 'none' */
|
|
320
|
+
updateMode?: 'none' | 'all' | 'failing';
|
|
321
|
+
/** CI mode: fail if snapshot is missing. Default: false */
|
|
322
|
+
ci?: boolean;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Stored snapshot data for a single request.
|
|
327
|
+
*/
|
|
328
|
+
export interface Snapshot {
|
|
329
|
+
status?: number;
|
|
330
|
+
headers?: Record<string, string>;
|
|
331
|
+
body?: JsonValue;
|
|
332
|
+
hash: string;
|
|
333
|
+
updatedAt: string;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Snapshot file format.
|
|
338
|
+
*/
|
|
339
|
+
export interface SnapshotFile {
|
|
340
|
+
version: number;
|
|
341
|
+
snapshots: Record<string, Snapshot>;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Result of comparing a response against a snapshot.
|
|
346
|
+
*/
|
|
347
|
+
export interface SnapshotDiff {
|
|
348
|
+
path: string;
|
|
349
|
+
expected: unknown;
|
|
350
|
+
received: unknown;
|
|
351
|
+
type: 'added' | 'removed' | 'changed' | 'type_mismatch';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Result of snapshot comparison.
|
|
356
|
+
*/
|
|
357
|
+
export interface SnapshotCompareResult {
|
|
358
|
+
match: boolean;
|
|
359
|
+
isNew: boolean;
|
|
360
|
+
updated: boolean;
|
|
361
|
+
differences: SnapshotDiff[];
|
|
362
|
+
}
|
package/src/utils/logger.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SnapshotFormatter } from '../snapshot/snapshot-formatter';
|
|
1
2
|
import type {
|
|
2
3
|
ExecutionResult,
|
|
3
4
|
ExecutionSummary,
|
|
@@ -409,6 +410,12 @@ export class Logger {
|
|
|
409
410
|
this.logValidationErrors(result.error);
|
|
410
411
|
}
|
|
411
412
|
|
|
413
|
+
// Show snapshot result
|
|
414
|
+
if (result.snapshotResult) {
|
|
415
|
+
console.log();
|
|
416
|
+
this.logSnapshotResult(result.request.name || 'Request', result.snapshotResult);
|
|
417
|
+
}
|
|
418
|
+
|
|
412
419
|
console.log();
|
|
413
420
|
return;
|
|
414
421
|
}
|
|
@@ -532,9 +539,27 @@ export class Logger {
|
|
|
532
539
|
this.logValidationErrors(result.error);
|
|
533
540
|
}
|
|
534
541
|
|
|
542
|
+
// Show snapshot result
|
|
543
|
+
if (result.snapshotResult) {
|
|
544
|
+
console.log();
|
|
545
|
+
this.logSnapshotResult(result.request.name || 'Request', result.snapshotResult);
|
|
546
|
+
}
|
|
547
|
+
|
|
535
548
|
console.log();
|
|
536
549
|
}
|
|
537
550
|
|
|
551
|
+
/**
|
|
552
|
+
* Logs snapshot comparison result.
|
|
553
|
+
*/
|
|
554
|
+
private logSnapshotResult(requestName: string, result: ExecutionResult['snapshotResult']): void {
|
|
555
|
+
if (!result) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const formatter = new SnapshotFormatter();
|
|
560
|
+
console.log(formatter.formatResult(requestName, result));
|
|
561
|
+
}
|
|
562
|
+
|
|
538
563
|
logSummary(summary: ExecutionSummary, isGlobal: boolean = false): void {
|
|
539
564
|
// For raw format, don't show summary
|
|
540
565
|
if (this.config.format === 'raw') {
|
|
@@ -645,4 +670,28 @@ export class Logger {
|
|
|
645
670
|
this.color(` (${requestCount} request${requestCount === 1 ? '' : 's'})`, 'dim'),
|
|
646
671
|
);
|
|
647
672
|
}
|
|
673
|
+
|
|
674
|
+
logWatch(files: string[]): void {
|
|
675
|
+
console.log();
|
|
676
|
+
console.log(
|
|
677
|
+
`${this.color('Watching for changes...', 'cyan')} ${this.color('(press Ctrl+C to stop)', 'dim')}`,
|
|
678
|
+
);
|
|
679
|
+
const fileList = files.length <= 3 ? files.join(', ') : `${files.length} files`;
|
|
680
|
+
console.log(this.color(` Files: ${fileList}`, 'dim'));
|
|
681
|
+
console.log();
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
logWatchReady(): void {
|
|
685
|
+
console.log();
|
|
686
|
+
console.log(this.color('Watching for changes...', 'cyan'));
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
logFileChanged(filename: string): void {
|
|
690
|
+
const timestamp = new Date().toLocaleTimeString('en-US', { hour12: false });
|
|
691
|
+
console.log(this.color('-'.repeat(50), 'dim'));
|
|
692
|
+
console.log(
|
|
693
|
+
`${this.color(`[${timestamp}]`, 'dim')} File changed: ${this.color(filename, 'yellow')}`,
|
|
694
|
+
);
|
|
695
|
+
console.log();
|
|
696
|
+
}
|
|
648
697
|
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
import { FileWatcher, type WatcherOptions } from './file-watcher';
|
|
3
|
+
|
|
4
|
+
// Mock logger
|
|
5
|
+
const createMockLogger = () => ({
|
|
6
|
+
logWatch: mock(() => {}),
|
|
7
|
+
logWatchReady: mock(() => {}),
|
|
8
|
+
logFileChanged: mock(() => {}),
|
|
9
|
+
logError: mock(() => {}),
|
|
10
|
+
logWarning: mock(() => {}),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Helper to access private method for testing
|
|
14
|
+
const triggerFileChange = (watcher: FileWatcher, filename: string) => {
|
|
15
|
+
// biome-ignore lint/complexity/useLiteralKeys: accessing private method for testing
|
|
16
|
+
watcher['handleFileChange'](filename);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe('FileWatcher', () => {
|
|
20
|
+
let watcher: FileWatcher;
|
|
21
|
+
let mockLogger: ReturnType<typeof createMockLogger>;
|
|
22
|
+
let onRunMock: ReturnType<typeof mock>;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
mockLogger = createMockLogger();
|
|
26
|
+
onRunMock = mock(() => Promise.resolve());
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
if (watcher) {
|
|
31
|
+
watcher.stop();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('should debounce rapid file changes', async () => {
|
|
36
|
+
const options: WatcherOptions = {
|
|
37
|
+
files: [],
|
|
38
|
+
config: { debounce: 50 },
|
|
39
|
+
onRun: onRunMock,
|
|
40
|
+
logger: mockLogger as unknown as WatcherOptions['logger'],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
watcher = new FileWatcher(options);
|
|
44
|
+
|
|
45
|
+
// Simulate rapid file changes
|
|
46
|
+
triggerFileChange(watcher, 'test.yaml');
|
|
47
|
+
triggerFileChange(watcher, 'test.yaml');
|
|
48
|
+
triggerFileChange(watcher, 'test.yaml');
|
|
49
|
+
|
|
50
|
+
// Wait for debounce to settle
|
|
51
|
+
await Bun.sleep(100);
|
|
52
|
+
|
|
53
|
+
// Should only have been called once due to debouncing
|
|
54
|
+
// Initial run doesn't happen because we didn't call start()
|
|
55
|
+
expect(onRunMock).toHaveBeenCalledTimes(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('should queue runs during execution', async () => {
|
|
59
|
+
let runCount = 0;
|
|
60
|
+
const slowOnRun = mock(async () => {
|
|
61
|
+
runCount++;
|
|
62
|
+
await Bun.sleep(100);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const options: WatcherOptions = {
|
|
66
|
+
files: [],
|
|
67
|
+
config: { debounce: 10, clear: false },
|
|
68
|
+
onRun: slowOnRun,
|
|
69
|
+
logger: mockLogger as unknown as WatcherOptions['logger'],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
watcher = new FileWatcher(options);
|
|
73
|
+
|
|
74
|
+
// Start first run
|
|
75
|
+
triggerFileChange(watcher, 'test.yaml');
|
|
76
|
+
await Bun.sleep(20); // Let debounce fire
|
|
77
|
+
|
|
78
|
+
// Change during run (should be queued)
|
|
79
|
+
triggerFileChange(watcher, 'test.yaml');
|
|
80
|
+
|
|
81
|
+
// Wait for both runs to complete
|
|
82
|
+
await Bun.sleep(300);
|
|
83
|
+
|
|
84
|
+
expect(runCount).toBe(2);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('should clear debounce timer on stop', () => {
|
|
88
|
+
const options: WatcherOptions = {
|
|
89
|
+
files: [],
|
|
90
|
+
config: { debounce: 1000 },
|
|
91
|
+
onRun: onRunMock,
|
|
92
|
+
logger: mockLogger as unknown as WatcherOptions['logger'],
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
watcher = new FileWatcher(options);
|
|
96
|
+
|
|
97
|
+
// Trigger a change to start debounce timer
|
|
98
|
+
triggerFileChange(watcher, 'test.yaml');
|
|
99
|
+
|
|
100
|
+
// Stop should clear the timer
|
|
101
|
+
watcher.stop();
|
|
102
|
+
|
|
103
|
+
// biome-ignore lint/complexity/useLiteralKeys: accessing private field for testing
|
|
104
|
+
expect(watcher['debounceTimer']).toBeNull();
|
|
105
|
+
// biome-ignore lint/complexity/useLiteralKeys: accessing private field for testing
|
|
106
|
+
expect(watcher['watchers']).toHaveLength(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('should use default debounce of 300ms', async () => {
|
|
110
|
+
const options: WatcherOptions = {
|
|
111
|
+
files: [],
|
|
112
|
+
config: {}, // No debounce specified
|
|
113
|
+
onRun: onRunMock,
|
|
114
|
+
logger: mockLogger as unknown as WatcherOptions['logger'],
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
watcher = new FileWatcher(options);
|
|
118
|
+
|
|
119
|
+
const startTime = performance.now();
|
|
120
|
+
triggerFileChange(watcher, 'test.yaml');
|
|
121
|
+
|
|
122
|
+
// Wait less than default debounce
|
|
123
|
+
await Bun.sleep(100);
|
|
124
|
+
expect(onRunMock).not.toHaveBeenCalled();
|
|
125
|
+
|
|
126
|
+
// Wait for full debounce
|
|
127
|
+
await Bun.sleep(250);
|
|
128
|
+
expect(onRunMock).toHaveBeenCalledTimes(1);
|
|
129
|
+
|
|
130
|
+
const elapsed = performance.now() - startTime;
|
|
131
|
+
expect(elapsed).toBeGreaterThanOrEqual(300);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('should handle onRun errors gracefully', async () => {
|
|
135
|
+
const errorOnRun = mock(async () => {
|
|
136
|
+
throw new Error('Test error');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const options: WatcherOptions = {
|
|
140
|
+
files: [],
|
|
141
|
+
config: { debounce: 10 },
|
|
142
|
+
onRun: errorOnRun,
|
|
143
|
+
logger: mockLogger as unknown as WatcherOptions['logger'],
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
watcher = new FileWatcher(options);
|
|
147
|
+
|
|
148
|
+
// Should not throw
|
|
149
|
+
triggerFileChange(watcher, 'test.yaml');
|
|
150
|
+
await Bun.sleep(50);
|
|
151
|
+
|
|
152
|
+
expect(mockLogger.logError).toHaveBeenCalledWith('Test error');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('should log file changed with filename', async () => {
|
|
156
|
+
const options: WatcherOptions = {
|
|
157
|
+
files: [],
|
|
158
|
+
config: { debounce: 10, clear: false },
|
|
159
|
+
onRun: onRunMock,
|
|
160
|
+
logger: mockLogger as unknown as WatcherOptions['logger'],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
watcher = new FileWatcher(options);
|
|
164
|
+
|
|
165
|
+
triggerFileChange(watcher, 'my-api.yaml');
|
|
166
|
+
await Bun.sleep(50);
|
|
167
|
+
|
|
168
|
+
expect(mockLogger.logFileChanged).toHaveBeenCalledWith('my-api.yaml');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('should call logWatchReady after run completes', async () => {
|
|
172
|
+
const options: WatcherOptions = {
|
|
173
|
+
files: [],
|
|
174
|
+
config: { debounce: 10, clear: false },
|
|
175
|
+
onRun: onRunMock,
|
|
176
|
+
logger: mockLogger as unknown as WatcherOptions['logger'],
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
watcher = new FileWatcher(options);
|
|
180
|
+
|
|
181
|
+
triggerFileChange(watcher, 'test.yaml');
|
|
182
|
+
await Bun.sleep(50);
|
|
183
|
+
|
|
184
|
+
expect(mockLogger.logWatchReady).toHaveBeenCalled();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { type FSWatcher, watch } from 'node:fs';
|
|
2
|
+
import type { WatchConfig } from '../types/config';
|
|
3
|
+
import type { Logger } from '../utils/logger';
|
|
4
|
+
|
|
5
|
+
export interface WatcherOptions {
|
|
6
|
+
files: string[];
|
|
7
|
+
config: WatchConfig;
|
|
8
|
+
onRun: () => Promise<void>;
|
|
9
|
+
logger: Logger;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class FileWatcher {
|
|
13
|
+
private watchers: FSWatcher[] = [];
|
|
14
|
+
private debounceTimer: Timer | null = null;
|
|
15
|
+
private isRunning = false;
|
|
16
|
+
private pendingRun = false;
|
|
17
|
+
private options: WatcherOptions;
|
|
18
|
+
|
|
19
|
+
constructor(options: WatcherOptions) {
|
|
20
|
+
this.options = options;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async start(): Promise<void> {
|
|
24
|
+
const { files, logger } = this.options;
|
|
25
|
+
|
|
26
|
+
// Initial run
|
|
27
|
+
await this.runRequests();
|
|
28
|
+
|
|
29
|
+
// Setup watchers for each file
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
try {
|
|
32
|
+
const watcher = watch(file, (event) => {
|
|
33
|
+
if (event === 'change') {
|
|
34
|
+
this.handleFileChange(file);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
watcher.on('error', (error) => {
|
|
39
|
+
logger.logWarning(`Watch error on ${file}: ${error.message}`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
this.watchers.push(watcher);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
logger.logWarning(
|
|
45
|
+
`Failed to watch ${file}: ${error instanceof Error ? error.message : String(error)}`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Log watching status
|
|
51
|
+
logger.logWatch(files);
|
|
52
|
+
|
|
53
|
+
// Handle graceful shutdown
|
|
54
|
+
this.setupSignalHandlers();
|
|
55
|
+
|
|
56
|
+
// Keep process alive
|
|
57
|
+
await this.keepAlive();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private handleFileChange(filename: string): void {
|
|
61
|
+
const { config, logger } = this.options;
|
|
62
|
+
const debounce = config.debounce ?? 300;
|
|
63
|
+
|
|
64
|
+
// Clear existing debounce timer
|
|
65
|
+
if (this.debounceTimer) {
|
|
66
|
+
clearTimeout(this.debounceTimer);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Debounce the run
|
|
70
|
+
this.debounceTimer = setTimeout(async () => {
|
|
71
|
+
// If already running, queue for after completion
|
|
72
|
+
if (this.isRunning) {
|
|
73
|
+
this.pendingRun = true;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (config.clear !== false) {
|
|
78
|
+
console.clear();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
logger.logFileChanged(filename);
|
|
82
|
+
await this.runRequests();
|
|
83
|
+
}, debounce);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async runRequests(): Promise<void> {
|
|
87
|
+
const { onRun, logger } = this.options;
|
|
88
|
+
|
|
89
|
+
this.isRunning = true;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await onRun();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
logger.logError(error instanceof Error ? error.message : String(error));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.isRunning = false;
|
|
98
|
+
|
|
99
|
+
// Check for pending runs
|
|
100
|
+
if (this.pendingRun) {
|
|
101
|
+
this.pendingRun = false;
|
|
102
|
+
const { config } = this.options;
|
|
103
|
+
if (config.clear !== false) {
|
|
104
|
+
console.clear();
|
|
105
|
+
}
|
|
106
|
+
logger.logFileChanged('(queued change)');
|
|
107
|
+
await this.runRequests();
|
|
108
|
+
} else {
|
|
109
|
+
logger.logWatchReady();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private setupSignalHandlers(): void {
|
|
114
|
+
const cleanup = () => {
|
|
115
|
+
this.stop();
|
|
116
|
+
process.exit(0);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
process.on('SIGINT', cleanup);
|
|
120
|
+
process.on('SIGTERM', cleanup);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async keepAlive(): Promise<never> {
|
|
124
|
+
while (true) {
|
|
125
|
+
await Bun.sleep(1000);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
stop(): void {
|
|
130
|
+
for (const watcher of this.watchers) {
|
|
131
|
+
watcher.close();
|
|
132
|
+
}
|
|
133
|
+
this.watchers = [];
|
|
134
|
+
|
|
135
|
+
if (this.debounceTimer) {
|
|
136
|
+
clearTimeout(this.debounceTimer);
|
|
137
|
+
this.debounceTimer = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|