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,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for blocking/long-running processes
|
|
3
|
+
*
|
|
4
|
+
* These tests verify the ProcessManager's ability to handle
|
|
5
|
+
* processes that run indefinitely or for extended periods.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, before, after } from 'node:test';
|
|
9
|
+
import assert from 'node:assert';
|
|
10
|
+
import { ProcessManager } from '../../src/process/manager.js';
|
|
11
|
+
import type { ProcessItem } from '../../src/config/types.js';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
|
|
16
|
+
describe('Blocking Processes Integration Tests', () => {
|
|
17
|
+
let manager: ProcessManager;
|
|
18
|
+
let testScriptsDir: string;
|
|
19
|
+
|
|
20
|
+
before(() => {
|
|
21
|
+
manager = new ProcessManager();
|
|
22
|
+
testScriptsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cligr-blocking-test-'));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
after(() => {
|
|
26
|
+
// Clean up any running processes
|
|
27
|
+
manager.killAll();
|
|
28
|
+
|
|
29
|
+
// Clean up test scripts directory
|
|
30
|
+
if (fs.existsSync(testScriptsDir)) {
|
|
31
|
+
fs.rmSync(testScriptsDir, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function createInfiniteLoopScript(scriptName: string, delayMs: number = 1000): string {
|
|
36
|
+
const scriptPath = path.join(testScriptsDir, scriptName);
|
|
37
|
+
const scriptContent = `
|
|
38
|
+
// Infinite loop script - simulates a long-running process
|
|
39
|
+
let counter = 0;
|
|
40
|
+
const interval = setInterval(() => {
|
|
41
|
+
counter++;
|
|
42
|
+
console.log(\`${scriptName}: \${counter}\`);
|
|
43
|
+
}, ${delayMs});
|
|
44
|
+
|
|
45
|
+
// Handle graceful shutdown
|
|
46
|
+
process.on('SIGTERM', () => {
|
|
47
|
+
clearInterval(interval);
|
|
48
|
+
process.exit(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
process.on('SIGINT', () => {
|
|
52
|
+
clearInterval(interval);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
});
|
|
55
|
+
`;
|
|
56
|
+
fs.writeFileSync(scriptPath, scriptContent);
|
|
57
|
+
return scriptPath;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createServerScript(scriptName: string, port: number): string {
|
|
61
|
+
const scriptPath = path.join(testScriptsDir, scriptName);
|
|
62
|
+
const scriptContent = `
|
|
63
|
+
// HTTP server script - simulates a service that stays running
|
|
64
|
+
import http from 'http';
|
|
65
|
+
|
|
66
|
+
const server = http.createServer((req, res) => {
|
|
67
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
68
|
+
res.end('OK\\n');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
server.listen(${port}, () => {
|
|
72
|
+
console.log(\`${scriptName}: listening on port \${port}\`);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
process.on('SIGTERM', () => {
|
|
76
|
+
server.close(() => process.exit(0));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
process.on('SIGINT', () => {
|
|
80
|
+
server.close(() => process.exit(0));
|
|
81
|
+
});
|
|
82
|
+
`;
|
|
83
|
+
fs.writeFileSync(scriptPath, scriptContent);
|
|
84
|
+
return scriptPath;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe('Multiple processes spawn in parallel', () => {
|
|
88
|
+
it('should spawn all processes concurrently', { timeout: 10000 }, async () => {
|
|
89
|
+
const scriptPath = createInfiniteLoopScript('concurrent.js', 500);
|
|
90
|
+
|
|
91
|
+
const items: ProcessItem[] = [
|
|
92
|
+
{ name: 'proc1', args: [scriptPath], fullCmd: `node ${scriptPath}` },
|
|
93
|
+
{ name: 'proc2', args: [scriptPath], fullCmd: `node ${scriptPath}` },
|
|
94
|
+
{ name: 'proc3', args: [scriptPath], fullCmd: `node ${scriptPath}` },
|
|
95
|
+
{ name: 'proc4', args: [scriptPath], fullCmd: `node ${scriptPath}` },
|
|
96
|
+
{ name: 'proc5', args: [scriptPath], fullCmd: `node ${scriptPath}` }
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const start = Date.now();
|
|
100
|
+
manager.spawnGroup('concurrent-group', items, 'no');
|
|
101
|
+
const elapsed = Date.now() - start;
|
|
102
|
+
|
|
103
|
+
// spawnGroup should return quickly (all processes started in parallel)
|
|
104
|
+
assert.ok(elapsed < 1000, `spawnGroup took ${elapsed}ms, expected < 1000ms`);
|
|
105
|
+
|
|
106
|
+
// Verify all processes are tracked
|
|
107
|
+
assert.strictEqual(manager.isGroupRunning('concurrent-group'), true);
|
|
108
|
+
|
|
109
|
+
// Wait for processes to start and run a bit
|
|
110
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
111
|
+
|
|
112
|
+
// Kill the group
|
|
113
|
+
manager.killGroup('concurrent-group');
|
|
114
|
+
|
|
115
|
+
// Verify processes were killed
|
|
116
|
+
assert.strictEqual(manager.isGroupRunning('concurrent-group'), false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should handle many concurrent processes', { timeout: 15000 }, async () => {
|
|
120
|
+
const scriptPath = createInfiniteLoopScript('many.js', 1000);
|
|
121
|
+
const items: ProcessItem[] = [];
|
|
122
|
+
|
|
123
|
+
// Create 20 items
|
|
124
|
+
for (let i = 0; i < 20; i++) {
|
|
125
|
+
items.push({
|
|
126
|
+
name: `proc${i}`,
|
|
127
|
+
args: [scriptPath],
|
|
128
|
+
fullCmd: `node ${scriptPath}`
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const start = Date.now();
|
|
133
|
+
manager.spawnGroup('many-group', items, 'no');
|
|
134
|
+
const elapsed = Date.now() - start;
|
|
135
|
+
|
|
136
|
+
// Should spawn 20 processes quickly
|
|
137
|
+
assert.ok(elapsed < 2000, `spawnGroup took ${elapsed}ms, expected < 2000ms`);
|
|
138
|
+
|
|
139
|
+
assert.strictEqual(manager.isGroupRunning('many-group'), true);
|
|
140
|
+
|
|
141
|
+
// Let them run
|
|
142
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
143
|
+
|
|
144
|
+
manager.killGroup('many-group');
|
|
145
|
+
assert.strictEqual(manager.isGroupRunning('many-group'), false);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('Server processes', () => {
|
|
150
|
+
it('should manage multiple server processes on different ports', { timeout: 10000 }, async () => {
|
|
151
|
+
const ports = [18100, 18101, 18102];
|
|
152
|
+
const items: ProcessItem[] = [];
|
|
153
|
+
|
|
154
|
+
for (const port of ports) {
|
|
155
|
+
const scriptPath = createServerScript(`server-${port}.js`, port);
|
|
156
|
+
items.push({
|
|
157
|
+
name: `server-${port}`,
|
|
158
|
+
args: [scriptPath],
|
|
159
|
+
fullCmd: `node ${scriptPath}`
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
manager.spawnGroup('server-group', items, 'no');
|
|
164
|
+
|
|
165
|
+
assert.strictEqual(manager.isGroupRunning('server-group'), true);
|
|
166
|
+
|
|
167
|
+
// Wait for servers to start
|
|
168
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
169
|
+
|
|
170
|
+
manager.killGroup('server-group');
|
|
171
|
+
assert.strictEqual(manager.isGroupRunning('server-group'), false);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('Long-running processes', () => {
|
|
176
|
+
it('should manage processes with long execution time', { timeout: 10000 }, async () => {
|
|
177
|
+
// Use sleep command for long-running processes
|
|
178
|
+
const sleepCmd = process.platform === 'win32' ? 'timeout' : 'sleep';
|
|
179
|
+
const sleepArg = process.platform === 'win32' ? '/t' : '';
|
|
180
|
+
|
|
181
|
+
const items: ProcessItem[] = [
|
|
182
|
+
{ name: 'long1', args: [sleepArg, '5'], fullCmd: `${sleepCmd} ${sleepArg} 5` },
|
|
183
|
+
{ name: 'long2', args: [sleepArg, '5'], fullCmd: `${sleepCmd} ${sleepArg} 5` },
|
|
184
|
+
{ name: 'long3', args: [sleepArg, '5'], fullCmd: `${sleepCmd} ${sleepArg} 5` }
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
manager.spawnGroup('long-group', items, 'no');
|
|
188
|
+
|
|
189
|
+
assert.strictEqual(manager.isGroupRunning('long-group'), true);
|
|
190
|
+
|
|
191
|
+
// Wait a bit then kill before they complete
|
|
192
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
193
|
+
|
|
194
|
+
manager.killGroup('long-group');
|
|
195
|
+
assert.strictEqual(manager.isGroupRunning('long-group'), false);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('Process lifecycle verification', () => {
|
|
200
|
+
it('should track process status correctly', { timeout: 10000 }, async () => {
|
|
201
|
+
const scriptPath = createInfiniteLoopScript('status.js', 500);
|
|
202
|
+
|
|
203
|
+
const items: ProcessItem[] = [
|
|
204
|
+
{ name: 'status1', args: [scriptPath], fullCmd: `node ${scriptPath}` },
|
|
205
|
+
{ name: 'status2', args: [scriptPath], fullCmd: `node ${scriptPath}` }
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
manager.spawnGroup('status-group', items, 'no');
|
|
209
|
+
|
|
210
|
+
// Check status
|
|
211
|
+
const status = manager.getGroupStatus('status-group');
|
|
212
|
+
assert.strictEqual(status.length, 2);
|
|
213
|
+
assert.strictEqual(status[0], 'running');
|
|
214
|
+
assert.strictEqual(status[1], 'running');
|
|
215
|
+
|
|
216
|
+
// Check running groups
|
|
217
|
+
const runningGroups = manager.getRunningGroups();
|
|
218
|
+
assert.ok(runningGroups.includes('status-group'));
|
|
219
|
+
|
|
220
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
221
|
+
|
|
222
|
+
manager.killGroup('status-group');
|
|
223
|
+
|
|
224
|
+
// After killing, group should not be running
|
|
225
|
+
assert.strictEqual(manager.isGroupRunning('status-group'), false);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('Cleanup verification', () => {
|
|
230
|
+
it('should properly clean up all processes', { timeout: 10000 }, async () => {
|
|
231
|
+
const scriptPath = createInfiniteLoopScript('cleanup.js', 500);
|
|
232
|
+
|
|
233
|
+
// Create multiple groups
|
|
234
|
+
for (let i = 0; i < 3; i++) {
|
|
235
|
+
const items: ProcessItem[] = [
|
|
236
|
+
{ name: `cleanup${i}-1`, args: [scriptPath], fullCmd: `node ${scriptPath}` },
|
|
237
|
+
{ name: `cleanup${i}-2`, args: [scriptPath], fullCmd: `node ${scriptPath}` }
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
manager.spawnGroup(`cleanup-group-${i}`, items, 'no');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// All groups should be running
|
|
244
|
+
assert.strictEqual(manager.getRunningGroups().length, 3);
|
|
245
|
+
|
|
246
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
247
|
+
|
|
248
|
+
// Kill all at once
|
|
249
|
+
manager.killAll();
|
|
250
|
+
|
|
251
|
+
// All groups should be cleaned up
|
|
252
|
+
assert.strictEqual(manager.getRunningGroups().length, 0);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|