cligr 1.0.7 → 1.0.9

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 (35) hide show
  1. package/.claude/worktrees/agent-ac25cfb2/.claude/settings.local.json +30 -0
  2. package/.claude/worktrees/agent-ac25cfb2/README.md +65 -0
  3. package/.claude/worktrees/agent-ac25cfb2/docs/plans/2026-02-13-named-params-support.md +391 -0
  4. package/.claude/worktrees/agent-ac25cfb2/docs/plans/2026-02-25-named-items-design.md +164 -0
  5. package/.claude/worktrees/agent-ac25cfb2/docs/plans/2026-02-25-named-items-implementation.md +460 -0
  6. package/.claude/worktrees/agent-ac25cfb2/package-lock.json +554 -0
  7. package/.claude/worktrees/agent-ac25cfb2/package.json +27 -0
  8. package/.claude/worktrees/agent-ac25cfb2/scripts/build.js +20 -0
  9. package/.claude/worktrees/agent-ac25cfb2/scripts/test.js +168 -0
  10. package/.claude/worktrees/agent-ac25cfb2/src/commands/config.ts +121 -0
  11. package/.claude/worktrees/agent-ac25cfb2/src/commands/groups.ts +68 -0
  12. package/.claude/worktrees/agent-ac25cfb2/src/commands/ls.ts +25 -0
  13. package/.claude/worktrees/agent-ac25cfb2/src/commands/up.ts +49 -0
  14. package/.claude/worktrees/agent-ac25cfb2/src/config/loader.ts +148 -0
  15. package/.claude/worktrees/agent-ac25cfb2/src/config/types.ts +26 -0
  16. package/.claude/worktrees/agent-ac25cfb2/src/index.ts +97 -0
  17. package/.claude/worktrees/agent-ac25cfb2/src/process/manager.ts +270 -0
  18. package/.claude/worktrees/agent-ac25cfb2/src/process/pid-store.ts +203 -0
  19. package/.claude/worktrees/agent-ac25cfb2/src/process/template.ts +87 -0
  20. package/.claude/worktrees/agent-ac25cfb2/tests/integration/blocking-processes-fixed.test.ts +255 -0
  21. package/.claude/worktrees/agent-ac25cfb2/tests/integration/blocking-processes.test.ts +497 -0
  22. package/.claude/worktrees/agent-ac25cfb2/tests/integration/commands.test.ts +648 -0
  23. package/.claude/worktrees/agent-ac25cfb2/tests/integration/config-loader.test.ts +426 -0
  24. package/.claude/worktrees/agent-ac25cfb2/tests/integration/process-manager.test.ts +394 -0
  25. package/.claude/worktrees/agent-ac25cfb2/tests/integration/template-expander.test.ts +454 -0
  26. package/.claude/worktrees/agent-ac25cfb2/tsconfig.json +15 -0
  27. package/.claude/worktrees/agent-ac25cfb2/usage.md +9 -0
  28. package/dist/index.js +103 -46
  29. package/docs/superpowers/specs/2026-04-13-improve-web-ui-console-design.md +38 -0
  30. package/package.json +1 -1
  31. package/src/commands/groups.ts +1 -1
  32. package/src/commands/ls.ts +1 -1
  33. package/src/commands/serve.ts +65 -8
  34. package/src/config/loader.ts +6 -2
  35. package/src/config/types.ts +1 -1
@@ -0,0 +1,648 @@
1
+ /**
2
+ * Integration tests for CLI commands
3
+ *
4
+ * These tests verify the CLI command functionality including:
5
+ * - up command (starting process groups)
6
+ * - ls command (listing groups)
7
+ * - Error handling
8
+ */
9
+
10
+ import { describe, it, before, after, mock } from 'node:test';
11
+ import assert from 'node:assert';
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import os from 'node:os';
15
+ import { upCommand } from '../../src/commands/up.js';
16
+ import { lsCommand } from '../../src/commands/ls.js';
17
+ import { groupsCommand } from '../../src/commands/groups.js';
18
+
19
+ describe('CLI Commands Integration Tests', () => {
20
+ let testConfigDir: string;
21
+ let testConfigPath: string;
22
+ let originalHomeDir: string;
23
+ let originalConsoleLog: typeof console.log;
24
+ let originalConsoleError: typeof console.error;
25
+ let logOutput: string[];
26
+ let errorOutput: string[];
27
+
28
+ before(() => {
29
+ // Create a temporary directory for test configs
30
+ testConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cligr-cli-test-'));
31
+ testConfigPath = path.join(testConfigDir, '.cligr.yml');
32
+
33
+ // Mock os.homedir to return our test directory
34
+ originalHomeDir = os.homedir();
35
+ mock.method(os, 'homedir', () => testConfigDir);
36
+
37
+ // Capture console output
38
+ originalConsoleLog = console.log;
39
+ originalConsoleError = console.error;
40
+ logOutput = [];
41
+ errorOutput = [];
42
+
43
+ console.log = (...args: any[]) => {
44
+ logOutput.push(args.map(arg => String(arg)).join(' '));
45
+ };
46
+ console.error = (...args: any[]) => {
47
+ errorOutput.push(args.map(arg => String(arg)).join(' '));
48
+ };
49
+ });
50
+
51
+ after(() => {
52
+ // Restore console
53
+ console.log = originalConsoleLog;
54
+ console.error = originalConsoleError;
55
+ os.homedir = () => originalHomeDir;
56
+
57
+ // Clean up test directory
58
+ if (fs.existsSync(testConfigDir)) {
59
+ fs.rmSync(testConfigDir, { recursive: true, force: true });
60
+ }
61
+ });
62
+
63
+ function resetOutput() {
64
+ logOutput = [];
65
+ errorOutput = [];
66
+ }
67
+
68
+ function getLogOutput(): string {
69
+ return logOutput.join('\n');
70
+ }
71
+
72
+ function getErrorOutput(): string {
73
+ return errorOutput.join('\n');
74
+ }
75
+
76
+ describe('upCommand', () => {
77
+ it('should start a group with echo commands (completes quickly)', { timeout: 5000 }, async () => {
78
+ const configContent = `
79
+ groups:
80
+ echo-test:
81
+ tool: echo
82
+ restart: no
83
+ items:
84
+ hello: hello
85
+ world: world
86
+ `;
87
+
88
+ fs.writeFileSync(testConfigPath, configContent);
89
+
90
+ // upCommand waits for Ctrl+C, so we need a different approach
91
+ // We'll test by verifying the config loads correctly
92
+ // The actual process spawning is tested in ProcessManager tests
93
+
94
+ // For now, just verify the command structure is valid
95
+ // Note: upCommand is designed to wait indefinitely, so we can't test it fully
96
+ // We just verify it doesn't throw on valid config
97
+ try {
98
+ // Create a timeout promise to avoid hanging
99
+ const timeoutPromise = new Promise<number>((resolve) => {
100
+ setTimeout(() => resolve(0), 100);
101
+ });
102
+
103
+ // Start the command but it will wait for signals
104
+ // We'll just verify the initial setup doesn't throw
105
+ const { ConfigLoader } = await import('../../src/config/loader.js');
106
+ const loader = new ConfigLoader();
107
+ const { config, items } = loader.getGroup('echo-test');
108
+
109
+ assert.strictEqual(config.tool, 'echo');
110
+ assert.strictEqual(config.restart, 'no');
111
+ assert.strictEqual(items.length, 2);
112
+ } catch (err) {
113
+ assert.fail(`Should not throw: ${err}`);
114
+ }
115
+ });
116
+
117
+ it('should handle missing config file', async () => {
118
+ // Remove config file
119
+ if (fs.existsSync(testConfigPath)) {
120
+ fs.unlinkSync(testConfigPath);
121
+ }
122
+
123
+ const exitCode = await upCommand('missing');
124
+
125
+ assert.strictEqual(exitCode, 1);
126
+ assert.ok(getErrorOutput().includes('Config file not found') || getErrorOutput().includes('Unknown group'));
127
+ });
128
+
129
+ it('should handle unknown group', async () => {
130
+ const configContent = `
131
+ groups:
132
+ known-group:
133
+ tool: echo
134
+ restart: no
135
+ items:
136
+ test: test
137
+ `;
138
+
139
+ fs.writeFileSync(testConfigPath, configContent);
140
+
141
+ const exitCode = await upCommand('unknown-group');
142
+
143
+ assert.strictEqual(exitCode, 1);
144
+ assert.ok(getErrorOutput().includes('Unknown group'));
145
+ });
146
+
147
+ it('should handle restart policies correctly', { timeout: 3000 }, async () => {
148
+ const configContent = `
149
+ groups:
150
+ restart-test:
151
+ tool: echo
152
+ restart: yes
153
+ items:
154
+ test: test
155
+ `;
156
+
157
+ fs.writeFileSync(testConfigPath, configContent);
158
+
159
+ // Verify config loads with correct restart policy
160
+ const { ConfigLoader } = await import('../../src/config/loader.js');
161
+ const loader = new ConfigLoader();
162
+ const { config } = loader.getGroup('restart-test');
163
+
164
+ assert.strictEqual(config.restart, 'yes');
165
+ });
166
+ });
167
+
168
+ describe('lsCommand', () => {
169
+ it('should list items in a group', async () => {
170
+ const configContent = `
171
+ tools:
172
+ docker:
173
+ cmd: docker run -it --rm
174
+
175
+ groups:
176
+ web:
177
+ tool: docker
178
+ restart: unless-stopped
179
+ items:
180
+ nginx: nginx,nginx,-p,80:80
181
+ redis: redis,redis
182
+ postgres: postgres,postgres,-e,POSTGRES_PASSWORD=test
183
+ `;
184
+
185
+ fs.writeFileSync(testConfigPath, configContent);
186
+ resetOutput();
187
+
188
+ const exitCode = await lsCommand('web');
189
+
190
+ assert.strictEqual(exitCode, 0);
191
+ const output = getLogOutput();
192
+ assert.ok(output.includes('Group: web'));
193
+ assert.ok(output.includes('Tool: docker'));
194
+ assert.ok(output.includes('Restart: unless-stopped'));
195
+ assert.ok(output.includes('Items:'));
196
+ assert.ok(output.includes('nginx'));
197
+ assert.ok(output.includes('redis'));
198
+ assert.ok(output.includes('postgres'));
199
+ });
200
+
201
+ it('should list items with correct arguments', async () => {
202
+ const configContent = `
203
+ groups:
204
+ test:
205
+ tool: echo
206
+ restart: no
207
+ items:
208
+ item1: item1,arg1,arg2
209
+ item2: item2,arg3,arg4,arg5
210
+ `;
211
+
212
+ fs.writeFileSync(testConfigPath, configContent);
213
+ resetOutput();
214
+
215
+ const exitCode = await lsCommand('test');
216
+
217
+ assert.strictEqual(exitCode, 0);
218
+ const output = getLogOutput();
219
+ // Items are shown as full strings
220
+ assert.ok(output.includes('item1,arg1,arg2'));
221
+ assert.ok(output.includes('item2,arg3,arg4,arg5'));
222
+ });
223
+
224
+ it('should handle empty items list', async () => {
225
+ const configContent = `
226
+ groups:
227
+ empty:
228
+ tool: echo
229
+ restart: no
230
+ items: {}
231
+ `;
232
+
233
+ fs.writeFileSync(testConfigPath, configContent);
234
+ resetOutput();
235
+
236
+ const exitCode = await lsCommand('empty');
237
+
238
+ assert.strictEqual(exitCode, 0);
239
+ const output = getLogOutput();
240
+ assert.ok(output.includes('Group: empty'));
241
+ assert.ok(output.includes('Items:'));
242
+ });
243
+
244
+ it('should return error exit code for unknown group', async () => {
245
+ const configContent = `
246
+ groups:
247
+ known:
248
+ tool: echo
249
+ restart: no
250
+ items:
251
+ test: test
252
+ `;
253
+
254
+ fs.writeFileSync(testConfigPath, configContent);
255
+ resetOutput();
256
+
257
+ const exitCode = await lsCommand('unknown');
258
+
259
+ assert.strictEqual(exitCode, 1);
260
+ assert.ok(getErrorOutput().includes('Unknown group'));
261
+ });
262
+
263
+ it('should return error exit code for missing config', async () => {
264
+ if (fs.existsSync(testConfigPath)) {
265
+ fs.unlinkSync(testConfigPath);
266
+ }
267
+ resetOutput();
268
+
269
+ const exitCode = await lsCommand('any-group');
270
+
271
+ assert.strictEqual(exitCode, 1);
272
+ assert.ok(getErrorOutput().includes('Config file not found') || getErrorOutput().includes('Unknown group'));
273
+ });
274
+
275
+ it('should handle groups without registered tools', async () => {
276
+ const configContent = `
277
+ groups:
278
+ direct:
279
+ tool: node
280
+ restart: no
281
+ items:
282
+ server: server.js,3000
283
+ worker: worker.js
284
+ `;
285
+
286
+ fs.writeFileSync(testConfigPath, configContent);
287
+ resetOutput();
288
+
289
+ const exitCode = await lsCommand('direct');
290
+
291
+ assert.strictEqual(exitCode, 0);
292
+ const output = getLogOutput();
293
+ assert.ok(output.includes('Tool: node'));
294
+ });
295
+ });
296
+
297
+ describe('Command integration scenarios', () => {
298
+ it('should handle ls followed by up workflow', async () => {
299
+ const configContent = `
300
+ groups:
301
+ workflow-test:
302
+ tool: echo
303
+ restart: no
304
+ items:
305
+ service1: service1
306
+ service2: service2
307
+ service3: service3
308
+ `;
309
+
310
+ fs.writeFileSync(testConfigPath, configContent);
311
+
312
+ // First list the group
313
+ resetOutput();
314
+ const lsExitCode = await lsCommand('workflow-test');
315
+ assert.strictEqual(lsExitCode, 0);
316
+ assert.ok(getLogOutput().includes('workflow-test'));
317
+
318
+ // Then verify config loads for up
319
+ const { ConfigLoader } = await import('../../src/config/loader.js');
320
+ const loader = new ConfigLoader();
321
+ const { items } = loader.getGroup('workflow-test');
322
+
323
+ assert.strictEqual(items.length, 3);
324
+ });
325
+
326
+ it('should handle multiple groups in config', async () => {
327
+ const configContent = `
328
+ groups:
329
+ group1:
330
+ tool: echo
331
+ restart: no
332
+ items:
333
+ item1: item1
334
+
335
+ group2:
336
+ tool: echo
337
+ restart: yes
338
+ items:
339
+ item2: item2
340
+
341
+ group3:
342
+ tool: echo
343
+ restart: unless-stopped
344
+ items:
345
+ item3: item3
346
+ `;
347
+
348
+ fs.writeFileSync(testConfigPath, configContent);
349
+
350
+ // List each group
351
+ for (const groupName of ['group1', 'group2', 'group3']) {
352
+ resetOutput();
353
+ const exitCode = await lsCommand(groupName);
354
+ assert.strictEqual(exitCode, 0);
355
+ assert.ok(getLogOutput().includes(`Group: ${groupName}`));
356
+ }
357
+ });
358
+ });
359
+
360
+ describe('Error handling', () => {
361
+ it('should handle malformed YAML in config', async () => {
362
+ fs.writeFileSync(testConfigPath, 'invalid: yaml: [');
363
+
364
+ resetOutput();
365
+
366
+ const exitCode = await lsCommand('any-group');
367
+
368
+ assert.strictEqual(exitCode, 1);
369
+ assert.ok(getErrorOutput().includes('Invalid YAML') || getErrorOutput().includes('Config file not found') || getErrorOutput().includes('Unknown group'));
370
+ });
371
+
372
+ it('should handle config with missing required fields', async () => {
373
+ const configContent = `
374
+ groups:
375
+ incomplete:
376
+ tool: echo
377
+ # missing restart and items
378
+ `;
379
+
380
+ fs.writeFileSync(testConfigPath, configContent);
381
+
382
+ resetOutput();
383
+
384
+ const exitCode = await lsCommand('incomplete');
385
+
386
+ // Should either fail or handle gracefully
387
+ // The behavior depends on the validator implementation
388
+ assert.ok([0, 1].includes(exitCode));
389
+ });
390
+
391
+ it('should handle config with extra unknown fields', async () => {
392
+ const configContent = `
393
+ extra_field: value
394
+ another_extra:
395
+ nested: value
396
+
397
+ groups:
398
+ with-extras:
399
+ tool: echo
400
+ restart: no
401
+ items:
402
+ test: test
403
+ extra_item_field: should be ignored
404
+ `;
405
+
406
+ fs.writeFileSync(testConfigPath, configContent);
407
+
408
+ resetOutput();
409
+
410
+ const exitCode = await lsCommand('with-extras');
411
+
412
+ // Should succeed - extra fields are ignored
413
+ assert.strictEqual(exitCode, 0);
414
+ });
415
+ });
416
+
417
+ describe('Special characters and edge cases', () => {
418
+ it('should handle group names with hyphens and underscores', async () => {
419
+ const configContent = `
420
+ groups:
421
+ my-test-group_123:
422
+ tool: echo
423
+ restart: no
424
+ items:
425
+ test: test
426
+ `;
427
+
428
+ fs.writeFileSync(testConfigPath, configContent);
429
+
430
+ resetOutput();
431
+ const exitCode = await lsCommand('my-test-group_123');
432
+
433
+ assert.strictEqual(exitCode, 0);
434
+ assert.ok(getLogOutput().includes('my-test-group_123'));
435
+ });
436
+
437
+ it('should handle item strings with special characters', async () => {
438
+ const configContent = `
439
+ groups:
440
+ special-chars:
441
+ tool: echo
442
+ restart: no
443
+ items:
444
+ service: "service,with,commas"
445
+ another: "another test"
446
+ simple: simple
447
+ `;
448
+
449
+ fs.writeFileSync(testConfigPath, configContent);
450
+
451
+ resetOutput();
452
+ const exitCode = await lsCommand('special-chars');
453
+
454
+ assert.strictEqual(exitCode, 0);
455
+ const output = getLogOutput();
456
+ assert.ok(output.includes('service,with,commas'));
457
+ assert.ok(output.includes('another test'));
458
+ });
459
+
460
+ it('should handle all restart policy values', async () => {
461
+ const configContent = `
462
+ groups:
463
+ restart-yes:
464
+ tool: echo
465
+ restart: yes
466
+ items:
467
+ test: test
468
+
469
+ restart-no:
470
+ tool: echo
471
+ restart: no
472
+ items:
473
+ test: test
474
+
475
+ restart-unless-stopped:
476
+ tool: echo
477
+ restart: unless-stopped
478
+ items:
479
+ test: test
480
+ `;
481
+
482
+ fs.writeFileSync(testConfigPath, configContent);
483
+
484
+ for (const groupName of ['restart-yes', 'restart-no', 'restart-unless-stopped']) {
485
+ resetOutput();
486
+ const exitCode = await lsCommand(groupName);
487
+ assert.strictEqual(exitCode, 0);
488
+ assert.ok(getLogOutput().includes(groupName));
489
+ }
490
+ });
491
+ });
492
+
493
+ describe('Tools configuration', () => {
494
+ it('should handle config with tools defined', async () => {
495
+ const configContent = `
496
+ tools:
497
+ docker:
498
+ cmd: docker run -it --rm
499
+ node:
500
+ cmd: node
501
+ python:
502
+ cmd: python3
503
+
504
+ groups:
505
+ docker-group:
506
+ tool: docker
507
+ restart: no
508
+ items:
509
+ nginx: nginx,nginx
510
+
511
+ node-group:
512
+ tool: node
513
+ restart: yes
514
+ items:
515
+ server: server.js
516
+ `;
517
+
518
+ fs.writeFileSync(testConfigPath, configContent);
519
+
520
+ // Test docker group
521
+ resetOutput();
522
+ let exitCode = await lsCommand('docker-group');
523
+ assert.strictEqual(exitCode, 0);
524
+ assert.ok(getLogOutput().includes('Tool: docker'));
525
+
526
+ // Test node group
527
+ resetOutput();
528
+ exitCode = await lsCommand('node-group');
529
+ assert.strictEqual(exitCode, 0);
530
+ assert.ok(getLogOutput().includes('Tool: node'));
531
+ });
532
+
533
+ it('should handle group referencing undefined tool', async () => {
534
+ const configContent = `
535
+ groups:
536
+ missing-tool:
537
+ tool: nonexistent-tool
538
+ restart: no
539
+ items:
540
+ test: test
541
+ `;
542
+
543
+ fs.writeFileSync(testConfigPath, configContent);
544
+
545
+ resetOutput();
546
+ const exitCode = await lsCommand('missing-tool');
547
+
548
+ // Should handle gracefully - tool is treated as direct executable
549
+ assert.strictEqual(exitCode, 0);
550
+ assert.ok(getLogOutput().includes('Tool: nonexistent-tool'));
551
+ });
552
+ });
553
+
554
+ describe('groupsCommand', () => {
555
+ it('should list group names in simple mode', async () => {
556
+ const configContent = `
557
+ groups:
558
+ web:
559
+ tool: docker
560
+ restart: no
561
+ items:
562
+ nginx: nginx
563
+ redis: redis
564
+
565
+ database:
566
+ tool: docker
567
+ restart: yes
568
+ items:
569
+ postgres: postgres
570
+ `;
571
+ fs.writeFileSync(testConfigPath, configContent);
572
+ resetOutput();
573
+
574
+ const exitCode = await groupsCommand(false);
575
+
576
+ assert.strictEqual(exitCode, 0);
577
+ const output = getLogOutput();
578
+ assert.ok(output.includes('web'));
579
+ assert.ok(output.includes('database'));
580
+ });
581
+
582
+ it('should show detailed table in verbose mode', async () => {
583
+ const configContent = `
584
+ tools:
585
+ docker:
586
+ cmd: docker run
587
+
588
+ groups:
589
+ web:
590
+ tool: docker
591
+ restart: unless-stopped
592
+ items:
593
+ nginx: nginx
594
+ redis: redis
595
+ postgres: postgres
596
+
597
+ direct:
598
+ tool: node
599
+ restart: no
600
+ items:
601
+ server: server.js
602
+ `;
603
+ fs.writeFileSync(testConfigPath, configContent);
604
+ resetOutput();
605
+
606
+ const exitCode = await groupsCommand(true);
607
+
608
+ assert.strictEqual(exitCode, 0);
609
+ const output = getLogOutput();
610
+ assert.ok(output.includes('GROUP'));
611
+ assert.ok(output.includes('TOOL'));
612
+ assert.ok(output.includes('RESTART'));
613
+ assert.ok(output.includes('ITEMS'));
614
+ assert.ok(output.includes('web'));
615
+ assert.ok(output.includes('docker'));
616
+ assert.ok(output.includes('unless-stopped'));
617
+ assert.ok(output.includes('3')); // item count for web
618
+ assert.ok(output.includes('direct'));
619
+ assert.ok(output.includes('node'));
620
+ assert.ok(output.includes('1')); // item count for direct
621
+ });
622
+
623
+ it('should handle empty groups list', async () => {
624
+ const configContent = `
625
+ groups: {}
626
+ `;
627
+ fs.writeFileSync(testConfigPath, configContent);
628
+ resetOutput();
629
+
630
+ const exitCode = await groupsCommand(false);
631
+
632
+ assert.strictEqual(exitCode, 0);
633
+ assert.strictEqual(getLogOutput(), '');
634
+ });
635
+
636
+ it('should handle missing config file', async () => {
637
+ if (fs.existsSync(testConfigPath)) {
638
+ fs.unlinkSync(testConfigPath);
639
+ }
640
+ resetOutput();
641
+
642
+ const exitCode = await groupsCommand(false);
643
+
644
+ assert.strictEqual(exitCode, 1);
645
+ assert.ok(getErrorOutput().includes('Config file not found'));
646
+ });
647
+ });
648
+ });