cligr 1.0.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/.claude/settings.local.json +12 -0
- package/README.md +65 -0
- package/dist/index.js +94 -0
- package/package.json +26 -0
- package/scripts/build.js +20 -0
- package/scripts/test.js +164 -0
- package/src/commands/config.ts +121 -0
- package/src/commands/down.ts +6 -0
- package/src/commands/groups.ts +68 -0
- package/src/commands/ls.ts +26 -0
- package/src/commands/up.ts +44 -0
- package/src/config/loader.ts +103 -0
- package/src/config/types.ts +20 -0
- package/src/index.ts +96 -0
- package/src/process/manager.ts +199 -0
- package/src/process/template.ts +72 -0
- package/tests/integration/blocking-processes-fixed.test.ts +255 -0
- package/tests/integration/blocking-processes.test.ts +497 -0
- package/tests/integration/commands.test.ts +674 -0
- package/tests/integration/config-loader.test.ts +426 -0
- package/tests/integration/process-manager.test.ts +391 -0
- package/tests/integration/template-expander.test.ts +362 -0
- package/tsconfig.json +15 -0
- package/usage.md +9 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for ProcessManager
|
|
3
|
+
*
|
|
4
|
+
* These tests verify the process management functionality including:
|
|
5
|
+
* - Spawning process groups
|
|
6
|
+
* - Process output prefixing
|
|
7
|
+
* - Restart policies
|
|
8
|
+
* - Crash loop detection
|
|
9
|
+
* - Killing groups
|
|
10
|
+
*
|
|
11
|
+
* Note: These tests spawn real processes and may be platform-specific.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, before, after } from 'node:test';
|
|
15
|
+
import assert from 'node:assert';
|
|
16
|
+
import { ProcessManager, ManagedProcess } from '../../src/process/manager.js';
|
|
17
|
+
import type { ProcessItem } from '../../src/config/types.js';
|
|
18
|
+
|
|
19
|
+
describe('ProcessManager Integration Tests', () => {
|
|
20
|
+
let manager: ProcessManager;
|
|
21
|
+
|
|
22
|
+
before(() => {
|
|
23
|
+
manager = new ProcessManager();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
after(async () => {
|
|
27
|
+
// Clean up any running processes
|
|
28
|
+
await manager.killAll();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('spawnGroup()', () => {
|
|
32
|
+
it('should spawn a group with single process', async () => {
|
|
33
|
+
const items: ProcessItem[] = [
|
|
34
|
+
{ name: 'test1', args: ['hello'], fullCmd: process.platform === 'win32' ? 'echo hello' : 'echo hello' }
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
manager.spawnGroup('test-group', items, 'no');
|
|
38
|
+
|
|
39
|
+
assert.strictEqual(manager.isGroupRunning('test-group'), true);
|
|
40
|
+
const status = manager.getGroupStatus('test-group');
|
|
41
|
+
assert.strictEqual(status.length, 1);
|
|
42
|
+
|
|
43
|
+
// Clean up
|
|
44
|
+
await manager.killGroup('test-group');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should spawn a group with multiple processes', async () => {
|
|
48
|
+
const items: ProcessItem[] = [
|
|
49
|
+
{ name: 'proc1', args: ['one'], fullCmd: process.platform === 'win32' ? 'echo one' : 'echo one' },
|
|
50
|
+
{ name: 'proc2', args: ['two'], fullCmd: process.platform === 'win32' ? 'echo two' : 'echo two' },
|
|
51
|
+
{ name: 'proc3', args: ['three'], fullCmd: process.platform === 'win32' ? 'echo three' : 'echo three' }
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
manager.spawnGroup('multi-group', items, 'no');
|
|
55
|
+
|
|
56
|
+
assert.strictEqual(manager.isGroupRunning('multi-group'), true);
|
|
57
|
+
const status = manager.getGroupStatus('multi-group');
|
|
58
|
+
assert.strictEqual(status.length, 3);
|
|
59
|
+
|
|
60
|
+
// Clean up
|
|
61
|
+
await manager.killGroup('multi-group');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should throw error when spawning duplicate group', async () => {
|
|
65
|
+
const items: ProcessItem[] = [
|
|
66
|
+
{ name: 'test', args: [], fullCmd: 'echo test' }
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
manager.spawnGroup('dup-group', items, 'no');
|
|
70
|
+
|
|
71
|
+
assert.throws(
|
|
72
|
+
() => manager.spawnGroup('dup-group', items, 'no'),
|
|
73
|
+
(err: Error) => {
|
|
74
|
+
assert.ok(err.message.includes('already running'));
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Clean up
|
|
80
|
+
await manager.killGroup('dup-group');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should handle processes with different exit times', async () => {
|
|
84
|
+
// Use sleep commands with different durations
|
|
85
|
+
const sleepCmd = process.platform === 'win32' ? 'timeout' : 'sleep';
|
|
86
|
+
const sleepFlag = process.platform === 'win32' ? '/t' : '';
|
|
87
|
+
|
|
88
|
+
const items: ProcessItem[] = [
|
|
89
|
+
{ name: 'short', args: ['1'], fullCmd: `${sleepCmd} ${sleepFlag} 1` },
|
|
90
|
+
{ name: 'long', args: ['2'], fullCmd: `${sleepCmd} ${sleepFlag} 2` }
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
manager.spawnGroup('timed-group', items, 'no');
|
|
94
|
+
|
|
95
|
+
assert.strictEqual(manager.isGroupRunning('timed-group'), true);
|
|
96
|
+
|
|
97
|
+
// Wait for short process to exit
|
|
98
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
99
|
+
|
|
100
|
+
// Group should still be tracked even after some processes exit
|
|
101
|
+
assert.strictEqual(manager.isGroupRunning('timed-group'), true);
|
|
102
|
+
|
|
103
|
+
// Clean up
|
|
104
|
+
await manager.killGroup('timed-group');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('killGroup()', () => {
|
|
109
|
+
it('should kill a running group', async () => {
|
|
110
|
+
// Use a long-running process
|
|
111
|
+
const sleepCmd = process.platform === 'win32' ? 'timeout' : 'sleep';
|
|
112
|
+
const sleepFlag = process.platform === 'win32' ? '/t' : '';
|
|
113
|
+
|
|
114
|
+
const items: ProcessItem[] = [
|
|
115
|
+
{ name: 'long-running', args: ['10'], fullCmd: `${sleepCmd} ${sleepFlag} 10` }
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
manager.spawnGroup('kill-test', items, 'no');
|
|
119
|
+
assert.strictEqual(manager.isGroupRunning('kill-test'), true);
|
|
120
|
+
|
|
121
|
+
await manager.killGroup('kill-test');
|
|
122
|
+
|
|
123
|
+
assert.strictEqual(manager.isGroupRunning('kill-test'), false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should handle killing non-existent group gracefully', async () => {
|
|
127
|
+
// Should not throw
|
|
128
|
+
await manager.killGroup('non-existent');
|
|
129
|
+
assert.strictEqual(manager.isGroupRunning('non-existent'), false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should kill all processes in a group', async () => {
|
|
133
|
+
const sleepCmd = process.platform === 'win32' ? 'timeout' : 'sleep';
|
|
134
|
+
const sleepFlag = process.platform === 'win32' ? '/t' : '';
|
|
135
|
+
|
|
136
|
+
const items: ProcessItem[] = [
|
|
137
|
+
{ name: 'p1', args: ['5'], fullCmd: `${sleepCmd} ${sleepFlag} 5` },
|
|
138
|
+
{ name: 'p2', args: ['5'], fullCmd: `${sleepCmd} ${sleepFlag} 5` },
|
|
139
|
+
{ name: 'p3', args: ['5'], fullCmd: `${sleepCmd} ${sleepFlag} 5` }
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
manager.spawnGroup('multi-kill', items, 'no');
|
|
143
|
+
assert.strictEqual(manager.isGroupRunning('multi-kill'), true);
|
|
144
|
+
|
|
145
|
+
await manager.killGroup('multi-kill');
|
|
146
|
+
|
|
147
|
+
assert.strictEqual(manager.isGroupRunning('multi-kill'), false);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('killAll()', () => {
|
|
152
|
+
it('should kill all running groups', async () => {
|
|
153
|
+
const sleepCmd = process.platform === 'win32' ? 'timeout' : 'sleep';
|
|
154
|
+
const sleepFlag = process.platform === 'win32' ? '/t' : '';
|
|
155
|
+
|
|
156
|
+
const items: ProcessItem[] = [
|
|
157
|
+
{ name: 'proc', args: ['5'], fullCmd: `${sleepCmd} ${sleepFlag} 5` }
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
manager.spawnGroup('group1', items, 'no');
|
|
161
|
+
manager.spawnGroup('group2', items, 'no');
|
|
162
|
+
manager.spawnGroup('group3', items, 'no');
|
|
163
|
+
|
|
164
|
+
assert.strictEqual(manager.isGroupRunning('group1'), true);
|
|
165
|
+
assert.strictEqual(manager.isGroupRunning('group2'), true);
|
|
166
|
+
assert.strictEqual(manager.isGroupRunning('group3'), true);
|
|
167
|
+
|
|
168
|
+
await manager.killAll();
|
|
169
|
+
|
|
170
|
+
assert.strictEqual(manager.isGroupRunning('group1'), false);
|
|
171
|
+
assert.strictEqual(manager.isGroupRunning('group2'), false);
|
|
172
|
+
assert.strictEqual(manager.isGroupRunning('group3'), false);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should handle empty state gracefully', async () => {
|
|
176
|
+
// Should not throw when no groups are running
|
|
177
|
+
await manager.killAll();
|
|
178
|
+
const running = manager.getRunningGroups();
|
|
179
|
+
assert.strictEqual(running.length, 0);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('getGroupStatus()', () => {
|
|
184
|
+
it('should return status for a running group', async () => {
|
|
185
|
+
const items: ProcessItem[] = [
|
|
186
|
+
{ name: 'status-test', args: [], fullCmd: process.platform === 'win32' ? 'echo test' : 'echo test' }
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
manager.spawnGroup('status-group', items, 'no');
|
|
190
|
+
|
|
191
|
+
const status = manager.getGroupStatus('status-group');
|
|
192
|
+
assert.strictEqual(status.length, 1);
|
|
193
|
+
assert.strictEqual(status[0], 'running');
|
|
194
|
+
|
|
195
|
+
await manager.killGroup('status-group');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should return empty array for non-existent group', () => {
|
|
199
|
+
const status = manager.getGroupStatus('non-existent');
|
|
200
|
+
assert.deepStrictEqual(status, []);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('isGroupRunning()', () => {
|
|
205
|
+
it('should return true for running group', async () => {
|
|
206
|
+
const items: ProcessItem[] = [
|
|
207
|
+
{ name: 'running-test', args: ['2'], fullCmd: process.platform === 'win32' ? 'timeout /t 2' : 'sleep 2' }
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
manager.spawnGroup('running-group', items, 'no');
|
|
211
|
+
|
|
212
|
+
assert.strictEqual(manager.isGroupRunning('running-group'), true);
|
|
213
|
+
|
|
214
|
+
await manager.killGroup('running-group');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should return false for non-existent group', () => {
|
|
218
|
+
assert.strictEqual(manager.isGroupRunning('non-existent'), false);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should return false after killing group', async () => {
|
|
222
|
+
const items: ProcessItem[] = [
|
|
223
|
+
{ name: 'temp', args: ['2'], fullCmd: process.platform === 'win32' ? 'timeout /t 2' : 'sleep 2' }
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
manager.spawnGroup('temp-group', items, 'no');
|
|
227
|
+
assert.strictEqual(manager.isGroupRunning('temp-group'), true);
|
|
228
|
+
|
|
229
|
+
await manager.killGroup('temp-group');
|
|
230
|
+
assert.strictEqual(manager.isGroupRunning('temp-group'), false);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('getRunningGroups()', () => {
|
|
235
|
+
it('should return list of running groups', async () => {
|
|
236
|
+
const sleepCmd = process.platform === 'win32' ? 'timeout' : 'sleep';
|
|
237
|
+
const sleepFlag = process.platform === 'win32' ? '/t' : '';
|
|
238
|
+
|
|
239
|
+
const items: ProcessItem[] = [
|
|
240
|
+
{ name: 'proc', args: ['2'], fullCmd: `${sleepCmd} ${sleepFlag} 2` }
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
manager.spawnGroup('list-group-1', items, 'no');
|
|
244
|
+
manager.spawnGroup('list-group-2', items, 'no');
|
|
245
|
+
manager.spawnGroup('list-group-3', items, 'no');
|
|
246
|
+
|
|
247
|
+
const running = manager.getRunningGroups();
|
|
248
|
+
assert.strictEqual(running.length, 3);
|
|
249
|
+
assert.ok(running.includes('list-group-1'));
|
|
250
|
+
assert.ok(running.includes('list-group-2'));
|
|
251
|
+
assert.ok(running.includes('list-group-3'));
|
|
252
|
+
|
|
253
|
+
await manager.killAll();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should return empty array when no groups running', async () => {
|
|
257
|
+
await manager.killAll();
|
|
258
|
+
const running = manager.getRunningGroups();
|
|
259
|
+
assert.strictEqual(running.length, 0);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('Restart policies', () => {
|
|
264
|
+
it('should not restart processes with restart=no', async () => {
|
|
265
|
+
// Create a process that exits immediately
|
|
266
|
+
const items: ProcessItem[] = [
|
|
267
|
+
{ name: 'no-restart', args: [], fullCmd: process.platform === 'win32' ? 'exit 0' : 'true' }
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
manager.spawnGroup('no-restart-group', items, 'no');
|
|
271
|
+
|
|
272
|
+
// Wait for process to exit
|
|
273
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
274
|
+
|
|
275
|
+
// Group should still be tracked but process won't restart
|
|
276
|
+
assert.strictEqual(manager.isGroupRunning('no-restart-group'), true);
|
|
277
|
+
|
|
278
|
+
await manager.killGroup('no-restart-group');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should handle unless-stopped restart policy', async () => {
|
|
282
|
+
const sleepCmd = process.platform === 'win32' ? 'timeout' : 'sleep';
|
|
283
|
+
const sleepFlag = process.platform === 'win32' ? '/t' : '';
|
|
284
|
+
|
|
285
|
+
const items: ProcessItem[] = [
|
|
286
|
+
{ name: 'unless-stopped', args: ['5'], fullCmd: `${sleepCmd} ${sleepFlag} 5` }
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
manager.spawnGroup('unless-stopped-group', items, 'unless-stopped');
|
|
290
|
+
|
|
291
|
+
assert.strictEqual(manager.isGroupRunning('unless-stopped-group'), true);
|
|
292
|
+
|
|
293
|
+
// Kill with SIGTERM
|
|
294
|
+
await manager.killGroup('unless-stopped-group');
|
|
295
|
+
|
|
296
|
+
// Process should not restart after SIGTERM
|
|
297
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
298
|
+
assert.strictEqual(manager.isGroupRunning('unless-stopped-group'), false);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('parseCommand()', () => {
|
|
303
|
+
it('should parse simple command', async () => {
|
|
304
|
+
const items: ProcessItem[] = [
|
|
305
|
+
{ name: 'parse-test', args: [], fullCmd: 'echo hello' }
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
manager.spawnGroup('parse-test-group', items, 'no');
|
|
309
|
+
|
|
310
|
+
// Command should execute successfully
|
|
311
|
+
assert.strictEqual(manager.isGroupRunning('parse-test-group'), true);
|
|
312
|
+
|
|
313
|
+
await manager.killGroup('parse-test-group');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should parse command with multiple arguments', async () => {
|
|
317
|
+
const items: ProcessItem[] = [
|
|
318
|
+
{ name: 'multi-arg', args: [], fullCmd: 'node -e "console.log(\'test\')"' }
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
manager.spawnGroup('multi-arg-group', items, 'no');
|
|
322
|
+
|
|
323
|
+
assert.strictEqual(manager.isGroupRunning('multi-arg-group'), true);
|
|
324
|
+
|
|
325
|
+
await manager.killGroup('multi-arg-group');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should handle quoted paths with spaces', async () => {
|
|
329
|
+
const items: ProcessItem[] = [
|
|
330
|
+
{ name: 'quoted-path', args: [], fullCmd: '"echo" "hello world"' }
|
|
331
|
+
];
|
|
332
|
+
|
|
333
|
+
manager.spawnGroup('quoted-path-group', items, 'no');
|
|
334
|
+
|
|
335
|
+
assert.strictEqual(manager.isGroupRunning('quoted-path-group'), true);
|
|
336
|
+
|
|
337
|
+
await manager.killGroup('quoted-path-group');
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe('Output prefixing', () => {
|
|
342
|
+
it('should prefix process output with item name', async () => {
|
|
343
|
+
// This test verifies output prefixing by checking that processes start successfully
|
|
344
|
+
// Actual output verification would require capturing stdout
|
|
345
|
+
const items: ProcessItem[] = [
|
|
346
|
+
{ name: 'prefixed-1', args: [], fullCmd: 'echo output1' },
|
|
347
|
+
{ name: 'prefixed-2', args: [], fullCmd: 'echo output2' }
|
|
348
|
+
];
|
|
349
|
+
|
|
350
|
+
manager.spawnGroup('output-test', items, 'no');
|
|
351
|
+
|
|
352
|
+
assert.strictEqual(manager.isGroupRunning('output-test'), true);
|
|
353
|
+
|
|
354
|
+
await manager.killGroup('output-test');
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe('Cross-platform compatibility', () => {
|
|
359
|
+
it('should work with Windows-specific commands', async function skipOnNonWindows() {
|
|
360
|
+
if (process.platform !== 'win32') {
|
|
361
|
+
this.skip();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const items: ProcessItem[] = [
|
|
365
|
+
{ name: 'win-cmd', args: [], fullCmd: 'cmd /c echo Windows' }
|
|
366
|
+
];
|
|
367
|
+
|
|
368
|
+
manager.spawnGroup('win-test', items, 'no');
|
|
369
|
+
|
|
370
|
+
assert.strictEqual(manager.isGroupRunning('win-test'), true);
|
|
371
|
+
|
|
372
|
+
await manager.killGroup('win-test');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should work with Unix-specific commands', async function skipOnWindows() {
|
|
376
|
+
if (process.platform === 'win32') {
|
|
377
|
+
this.skip();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const items: ProcessItem[] = [
|
|
381
|
+
{ name: 'unix-cmd', args: [], fullCmd: '/bin/echo Unix' }
|
|
382
|
+
];
|
|
383
|
+
|
|
384
|
+
manager.spawnGroup('unix-test', items, 'no');
|
|
385
|
+
|
|
386
|
+
assert.strictEqual(manager.isGroupRunning('unix-test'), true);
|
|
387
|
+
|
|
388
|
+
await manager.killGroup('unix-test');
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
});
|