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,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for ConfigLoader
|
|
3
|
+
*
|
|
4
|
+
* These tests verify the config loading functionality including:
|
|
5
|
+
* - Loading from different file locations
|
|
6
|
+
* - YAML parsing
|
|
7
|
+
* - Validation
|
|
8
|
+
* - Group retrieval
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, before, after, mock } from 'node:test';
|
|
12
|
+
import assert from 'node:assert';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import { ConfigLoader, ConfigError } from '../../src/config/loader.js';
|
|
17
|
+
|
|
18
|
+
describe('ConfigLoader Integration Tests', () => {
|
|
19
|
+
let testConfigDir: string;
|
|
20
|
+
let testConfigPath: string;
|
|
21
|
+
let originalHomeDir: string;
|
|
22
|
+
|
|
23
|
+
before(() => {
|
|
24
|
+
// Create a temporary directory for test configs
|
|
25
|
+
testConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cligr-test-'));
|
|
26
|
+
testConfigPath = path.join(testConfigDir, '.cligr.yml');
|
|
27
|
+
|
|
28
|
+
// Mock os.homedir to return our test directory
|
|
29
|
+
originalHomeDir = os.homedir();
|
|
30
|
+
mock.method(os, 'homedir', () => testConfigDir);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
after(() => {
|
|
34
|
+
// Clean up test directory
|
|
35
|
+
if (fs.existsSync(testConfigDir)) {
|
|
36
|
+
fs.rmSync(testConfigDir, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
// Restore original os.homedir
|
|
39
|
+
mock.method(os, 'homedir', () => originalHomeDir);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('load()', () => {
|
|
43
|
+
it('should load a valid config file from home directory', () => {
|
|
44
|
+
const configContent = `
|
|
45
|
+
tools:
|
|
46
|
+
docker:
|
|
47
|
+
cmd: docker run -it --rm
|
|
48
|
+
node:
|
|
49
|
+
cmd: node
|
|
50
|
+
|
|
51
|
+
groups:
|
|
52
|
+
test1:
|
|
53
|
+
tool: docker
|
|
54
|
+
restart: yes
|
|
55
|
+
items:
|
|
56
|
+
- alpine,sh
|
|
57
|
+
- nginx,nginx,-p,80:80
|
|
58
|
+
|
|
59
|
+
test2:
|
|
60
|
+
tool: node
|
|
61
|
+
restart: no
|
|
62
|
+
items:
|
|
63
|
+
- server.js
|
|
64
|
+
- worker.js
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
68
|
+
|
|
69
|
+
const loader = new ConfigLoader();
|
|
70
|
+
const config = loader.load();
|
|
71
|
+
|
|
72
|
+
assert.ok(config.groups);
|
|
73
|
+
assert.ok(config.tools);
|
|
74
|
+
assert.strictEqual(Object.keys(config.groups).length, 2);
|
|
75
|
+
assert.strictEqual(config.groups.test1.tool, 'docker');
|
|
76
|
+
assert.strictEqual(config.groups.test1.restart, 'yes');
|
|
77
|
+
assert.strictEqual(config.groups.test1.items.length, 2);
|
|
78
|
+
assert.strictEqual(config.groups.test2.tool, 'node');
|
|
79
|
+
assert.strictEqual(config.groups.test2.items.length, 2);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should throw ConfigError when config file does not exist', () => {
|
|
83
|
+
// Remove config file if it exists
|
|
84
|
+
if (fs.existsSync(testConfigPath)) {
|
|
85
|
+
fs.unlinkSync(testConfigPath);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const loader = new ConfigLoader();
|
|
89
|
+
|
|
90
|
+
assert.throws(
|
|
91
|
+
() => loader.load(),
|
|
92
|
+
(err: Error) => {
|
|
93
|
+
assert.ok(err instanceof ConfigError);
|
|
94
|
+
assert.ok(err.message.includes('Config file not found'));
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should throw ConfigError for invalid YAML', () => {
|
|
101
|
+
fs.writeFileSync(testConfigPath, 'invalid: yaml: content: [unclosed');
|
|
102
|
+
|
|
103
|
+
const loader = new ConfigLoader();
|
|
104
|
+
|
|
105
|
+
assert.throws(
|
|
106
|
+
() => loader.load(),
|
|
107
|
+
(err: Error) => {
|
|
108
|
+
assert.ok(err instanceof ConfigError);
|
|
109
|
+
assert.ok(err.message.includes('Invalid YAML'));
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should throw ConfigError when config is not an object', () => {
|
|
116
|
+
fs.writeFileSync(testConfigPath, 'just a string');
|
|
117
|
+
|
|
118
|
+
const loader = new ConfigLoader();
|
|
119
|
+
|
|
120
|
+
assert.throws(
|
|
121
|
+
() => loader.load(),
|
|
122
|
+
(err: Error) => {
|
|
123
|
+
assert.ok(err instanceof ConfigError);
|
|
124
|
+
assert.ok(err.message.includes('Config must be an object'));
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should throw ConfigError when groups field is missing', () => {
|
|
131
|
+
fs.writeFileSync(testConfigPath, 'tools:\n docker:\n cmd: docker');
|
|
132
|
+
|
|
133
|
+
const loader = new ConfigLoader();
|
|
134
|
+
|
|
135
|
+
assert.throws(
|
|
136
|
+
() => loader.load(),
|
|
137
|
+
(err: Error) => {
|
|
138
|
+
assert.ok(err instanceof ConfigError);
|
|
139
|
+
assert.ok(err.message.includes('groups'));
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should load config without tools section', () => {
|
|
146
|
+
const configContent = `
|
|
147
|
+
groups:
|
|
148
|
+
simple:
|
|
149
|
+
tool: echo
|
|
150
|
+
restart: no
|
|
151
|
+
items:
|
|
152
|
+
- hello
|
|
153
|
+
- world
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
157
|
+
|
|
158
|
+
const loader = new ConfigLoader();
|
|
159
|
+
const config = loader.load();
|
|
160
|
+
|
|
161
|
+
assert.ok(config.groups);
|
|
162
|
+
assert.strictEqual(Object.keys(config.groups).length, 1);
|
|
163
|
+
assert.strictEqual(config.groups.simple.tool, 'echo');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('getGroup()', () => {
|
|
168
|
+
before(() => {
|
|
169
|
+
const configContent = `
|
|
170
|
+
tools:
|
|
171
|
+
docker:
|
|
172
|
+
cmd: docker run -it --rm
|
|
173
|
+
|
|
174
|
+
groups:
|
|
175
|
+
web:
|
|
176
|
+
tool: docker
|
|
177
|
+
restart: unless-stopped
|
|
178
|
+
items:
|
|
179
|
+
- nginx,nginx,-p,80:80
|
|
180
|
+
- redis,redis
|
|
181
|
+
|
|
182
|
+
api:
|
|
183
|
+
tool: node
|
|
184
|
+
restart: yes
|
|
185
|
+
items:
|
|
186
|
+
- server.js,3000
|
|
187
|
+
- worker.js
|
|
188
|
+
`;
|
|
189
|
+
|
|
190
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should retrieve an existing group with tool', () => {
|
|
194
|
+
const loader = new ConfigLoader();
|
|
195
|
+
const result = loader.getGroup('web');
|
|
196
|
+
|
|
197
|
+
assert.strictEqual(result.config.tool, 'docker');
|
|
198
|
+
assert.strictEqual(result.config.restart, 'unless-stopped');
|
|
199
|
+
assert.strictEqual(result.tool, 'docker');
|
|
200
|
+
assert.strictEqual(result.toolTemplate, 'docker run -it --rm');
|
|
201
|
+
assert.strictEqual(result.config.items.length, 2);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should retrieve an existing group without registered tool', () => {
|
|
205
|
+
const loader = new ConfigLoader();
|
|
206
|
+
const result = loader.getGroup('api');
|
|
207
|
+
|
|
208
|
+
assert.strictEqual(result.config.tool, 'node');
|
|
209
|
+
assert.strictEqual(result.config.restart, 'yes');
|
|
210
|
+
assert.strictEqual(result.tool, null); // No registered tool
|
|
211
|
+
assert.strictEqual(result.toolTemplate, null);
|
|
212
|
+
assert.strictEqual(result.config.items.length, 2);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should throw ConfigError for unknown group', () => {
|
|
216
|
+
const loader = new ConfigLoader();
|
|
217
|
+
|
|
218
|
+
assert.throws(
|
|
219
|
+
() => loader.getGroup('unknown'),
|
|
220
|
+
(err: Error) => {
|
|
221
|
+
assert.ok(err instanceof ConfigError);
|
|
222
|
+
assert.ok(err.message.includes('Unknown group'));
|
|
223
|
+
assert.ok(err.message.includes('unknown'));
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('listGroups()', () => {
|
|
231
|
+
it('should return all group names', () => {
|
|
232
|
+
const configContent = `
|
|
233
|
+
groups:
|
|
234
|
+
group1:
|
|
235
|
+
tool: echo
|
|
236
|
+
restart: no
|
|
237
|
+
items:
|
|
238
|
+
- test1
|
|
239
|
+
|
|
240
|
+
group2:
|
|
241
|
+
tool: echo
|
|
242
|
+
restart: no
|
|
243
|
+
items:
|
|
244
|
+
- test2
|
|
245
|
+
|
|
246
|
+
group3:
|
|
247
|
+
tool: echo
|
|
248
|
+
restart: no
|
|
249
|
+
items:
|
|
250
|
+
- test3
|
|
251
|
+
`;
|
|
252
|
+
|
|
253
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
254
|
+
|
|
255
|
+
const loader = new ConfigLoader();
|
|
256
|
+
const groups = loader.listGroups();
|
|
257
|
+
|
|
258
|
+
assert.strictEqual(groups.length, 3);
|
|
259
|
+
assert.ok(groups.includes('group1'));
|
|
260
|
+
assert.ok(groups.includes('group2'));
|
|
261
|
+
assert.ok(groups.includes('group3'));
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should return empty array when no groups exist', () => {
|
|
265
|
+
const configContent = `
|
|
266
|
+
groups: {}
|
|
267
|
+
`;
|
|
268
|
+
|
|
269
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
270
|
+
|
|
271
|
+
const loader = new ConfigLoader();
|
|
272
|
+
const groups = loader.listGroups();
|
|
273
|
+
|
|
274
|
+
assert.strictEqual(groups.length, 0);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('Constructor with explicit path', () => {
|
|
279
|
+
it('should load config from explicit path', () => {
|
|
280
|
+
const customConfigPath = path.join(testConfigDir, 'custom-config.yml');
|
|
281
|
+
const configContent = `
|
|
282
|
+
groups:
|
|
283
|
+
custom:
|
|
284
|
+
tool: echo
|
|
285
|
+
restart: no
|
|
286
|
+
items:
|
|
287
|
+
- test
|
|
288
|
+
`;
|
|
289
|
+
|
|
290
|
+
fs.writeFileSync(customConfigPath, configContent);
|
|
291
|
+
|
|
292
|
+
const loader = new ConfigLoader(customConfigPath);
|
|
293
|
+
const config = loader.load();
|
|
294
|
+
|
|
295
|
+
assert.ok(config.groups);
|
|
296
|
+
assert.strictEqual(config.groups.custom.tool, 'echo');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should throw error when explicit path does not exist', () => {
|
|
300
|
+
const nonExistentPath = path.join(testConfigDir, 'does-not-exist.yml');
|
|
301
|
+
|
|
302
|
+
const loader = new ConfigLoader(nonExistentPath);
|
|
303
|
+
|
|
304
|
+
assert.throws(
|
|
305
|
+
() => loader.load(),
|
|
306
|
+
(err: Error) => {
|
|
307
|
+
assert.ok(err instanceof ConfigError);
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('Config precedence', () => {
|
|
315
|
+
it('should prefer home directory config over current directory', () => {
|
|
316
|
+
const homeConfigPath = path.join(testConfigDir, '.cligr.yml');
|
|
317
|
+
const currentConfigPath = path.join(process.cwd(), '.cligr.yml');
|
|
318
|
+
|
|
319
|
+
const homeContent = `
|
|
320
|
+
groups:
|
|
321
|
+
home-group:
|
|
322
|
+
tool: echo
|
|
323
|
+
restart: no
|
|
324
|
+
items:
|
|
325
|
+
- from-home
|
|
326
|
+
`;
|
|
327
|
+
|
|
328
|
+
const currentContent = `
|
|
329
|
+
groups:
|
|
330
|
+
current-group:
|
|
331
|
+
tool: echo
|
|
332
|
+
restart: no
|
|
333
|
+
items:
|
|
334
|
+
- from-current
|
|
335
|
+
`;
|
|
336
|
+
|
|
337
|
+
// Write home config (mocked to testConfigDir)
|
|
338
|
+
fs.writeFileSync(homeConfigPath, homeContent);
|
|
339
|
+
|
|
340
|
+
// Write current directory config
|
|
341
|
+
fs.writeFileSync(currentConfigPath, currentContent);
|
|
342
|
+
|
|
343
|
+
const loader = new ConfigLoader();
|
|
344
|
+
const groups = loader.listGroups();
|
|
345
|
+
|
|
346
|
+
// Should load from home directory
|
|
347
|
+
assert.strictEqual(groups.length, 1);
|
|
348
|
+
assert.strictEqual(groups[0], 'home-group');
|
|
349
|
+
|
|
350
|
+
// Clean up current directory config
|
|
351
|
+
fs.unlinkSync(currentConfigPath);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe('Edge cases', () => {
|
|
356
|
+
it('should handle empty items array', () => {
|
|
357
|
+
const configContent = `
|
|
358
|
+
groups:
|
|
359
|
+
empty:
|
|
360
|
+
tool: echo
|
|
361
|
+
restart: no
|
|
362
|
+
items: []
|
|
363
|
+
`;
|
|
364
|
+
|
|
365
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
366
|
+
|
|
367
|
+
const loader = new ConfigLoader();
|
|
368
|
+
const config = loader.load();
|
|
369
|
+
|
|
370
|
+
assert.strictEqual(config.groups.empty.items.length, 0);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should handle special characters in item strings', () => {
|
|
374
|
+
const configContent = `
|
|
375
|
+
groups:
|
|
376
|
+
special:
|
|
377
|
+
tool: echo
|
|
378
|
+
restart: no
|
|
379
|
+
items:
|
|
380
|
+
- "hello, world"
|
|
381
|
+
- "test,with,commas"
|
|
382
|
+
- "spaces test"
|
|
383
|
+
`;
|
|
384
|
+
|
|
385
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
386
|
+
|
|
387
|
+
const loader = new ConfigLoader();
|
|
388
|
+
const result = loader.getGroup('special');
|
|
389
|
+
|
|
390
|
+
assert.strictEqual(result.config.items.length, 3);
|
|
391
|
+
assert.strictEqual(result.config.items[0], 'hello, world');
|
|
392
|
+
assert.strictEqual(result.config.items[1], 'test,with,commas');
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should handle all restart policy values', () => {
|
|
396
|
+
const configContent = `
|
|
397
|
+
groups:
|
|
398
|
+
restart-yes:
|
|
399
|
+
tool: echo
|
|
400
|
+
restart: yes
|
|
401
|
+
items:
|
|
402
|
+
- test
|
|
403
|
+
|
|
404
|
+
restart-no:
|
|
405
|
+
tool: echo
|
|
406
|
+
restart: no
|
|
407
|
+
items:
|
|
408
|
+
- test
|
|
409
|
+
|
|
410
|
+
restart-unless-stopped:
|
|
411
|
+
tool: echo
|
|
412
|
+
restart: unless-stopped
|
|
413
|
+
items:
|
|
414
|
+
- test
|
|
415
|
+
`;
|
|
416
|
+
|
|
417
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
418
|
+
|
|
419
|
+
const loader = new ConfigLoader();
|
|
420
|
+
|
|
421
|
+
assert.strictEqual(loader.getGroup('restart-yes').config.restart, 'yes');
|
|
422
|
+
assert.strictEqual(loader.getGroup('restart-no').config.restart, 'no');
|
|
423
|
+
assert.strictEqual(loader.getGroup('restart-unless-stopped').config.restart, 'unless-stopped');
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
});
|