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,454 @@
1
+ /**
2
+ * Integration tests for TemplateExpander
3
+ *
4
+ * These tests verify the template expansion functionality including:
5
+ * - Command template expansion with $1, $2, etc.
6
+ * - Item string parsing
7
+ * - Name generation
8
+ * - Direct executable handling
9
+ */
10
+
11
+ import { describe, it } from 'node:test';
12
+ import assert from 'node:assert';
13
+ import { TemplateExpander } from '../../src/process/template.js';
14
+ import type { ProcessItem, ItemEntry } from '../../src/config/types.js';
15
+
16
+ describe('TemplateExpander Integration Tests', () => {
17
+ describe('expand()', () => {
18
+ it('should expand template with single argument', () => {
19
+ const template = 'echo $1';
20
+ const item: ItemEntry = { name: 'hello', value: 'hello' };
21
+
22
+ const result = TemplateExpander.expand(template, item, 0);
23
+
24
+ assert.strictEqual(result.name, 'hello');
25
+ assert.deepStrictEqual(result.args, ['hello']);
26
+ assert.strictEqual(result.fullCmd, 'echo hello');
27
+ });
28
+
29
+ it('should expand template with multiple arguments', () => {
30
+ const template = 'docker run -p $2:$3 $1';
31
+ const item: ItemEntry = { name: 'nginx', value: 'nginx,8080,80' };
32
+
33
+ const result = TemplateExpander.expand(template, item, 0);
34
+
35
+ assert.strictEqual(result.name, 'nginx');
36
+ assert.deepStrictEqual(result.args, ['nginx', '8080', '80']);
37
+ assert.strictEqual(result.fullCmd, 'docker run -p 8080:80 nginx');
38
+ });
39
+
40
+ it('should replace all occurrences of placeholder', () => {
41
+ const template = 'echo $1 and $1 again';
42
+ const item: ItemEntry = { name: 'test', value: 'test' };
43
+
44
+ const result = TemplateExpander.expand(template, item, 0);
45
+
46
+ assert.strictEqual(result.fullCmd, 'echo test and test again');
47
+ });
48
+
49
+ it('should handle placeholders with spaces around', () => {
50
+ const template = 'cmd $1 opt $2 end';
51
+ const item: ItemEntry = { name: 'args', value: 'arg1,arg2' };
52
+
53
+ const result = TemplateExpander.expand(template, item, 0);
54
+
55
+ assert.strictEqual(result.fullCmd, 'cmd arg1 opt arg2 end');
56
+ });
57
+
58
+ it('should use ItemEntry name when first argument is empty', () => {
59
+ const template = 'echo test';
60
+ const item: ItemEntry = { name: 'item-5', value: ',arg2,arg3' };
61
+
62
+ const result = TemplateExpander.expand(template, item, 5);
63
+
64
+ assert.strictEqual(result.name, 'item-5');
65
+ assert.deepStrictEqual(result.args, ['', 'arg2', 'arg3']);
66
+ });
67
+
68
+ it('should handle single argument with no commas', () => {
69
+ const template = 'node $1';
70
+ const item: ItemEntry = { name: 'server', value: 'server.js' };
71
+
72
+ const result = TemplateExpander.expand(template, item, 0);
73
+
74
+ assert.strictEqual(result.name, 'server');
75
+ assert.deepStrictEqual(result.args, ['server.js']);
76
+ assert.strictEqual(result.fullCmd, 'node server.js');
77
+ });
78
+
79
+ it('should handle more placeholders than arguments', () => {
80
+ const template = 'cmd $1 $2 $3 $4';
81
+ const item: ItemEntry = { name: 'args', value: 'a,b' };
82
+
83
+ const result = TemplateExpander.expand(template, item, 0);
84
+
85
+ assert.strictEqual(result.fullCmd, 'cmd a b $3 $4');
86
+ });
87
+
88
+ it('should trim whitespace from arguments', () => {
89
+ const template = 'cmd $1 $2 $3';
90
+ const item: ItemEntry = { name: 'args', value: ' a , b , c ' };
91
+
92
+ const result = TemplateExpander.expand(template, item, 0);
93
+
94
+ assert.deepStrictEqual(result.args, ['a', 'b', 'c']);
95
+ assert.strictEqual(result.fullCmd, 'cmd a b c');
96
+ });
97
+
98
+ it('should handle empty string argument', () => {
99
+ const template = 'cmd $1 $2 $3';
100
+ const item: ItemEntry = { name: 'args', value: 'first,,third' };
101
+
102
+ const result = TemplateExpander.expand(template, item, 0);
103
+
104
+ assert.deepStrictEqual(result.args, ['first', '', 'third']);
105
+ assert.strictEqual(result.fullCmd, 'cmd first third');
106
+ });
107
+
108
+ it('should handle quoted paths with spaces in template', () => {
109
+ const template = '"C:\\Program Files\\app\\app.exe" $1';
110
+ const item: ItemEntry = { name: 'arg', value: 'arg1' };
111
+
112
+ const result = TemplateExpander.expand(template, item, 0);
113
+
114
+ assert.strictEqual(result.fullCmd, '"C:\\Program Files\\app\\app.exe" arg1');
115
+ });
116
+ });
117
+
118
+ describe('parseItem()', () => {
119
+ describe('with registered tool template', () => {
120
+ it('should use tool template for expansion', () => {
121
+ const tool = 'docker';
122
+ const toolTemplate = 'docker run -it --rm $1';
123
+ const item: ItemEntry = { name: 'nginx', value: 'nginx,-p,80:80' };
124
+
125
+ const result = TemplateExpander.parseItem(tool, toolTemplate, item, 0);
126
+
127
+ assert.strictEqual(result.name, 'nginx');
128
+ assert.strictEqual(result.fullCmd, 'docker run -it --rm nginx -p 80:80');
129
+ });
130
+
131
+ it('should handle complex docker compose templates', () => {
132
+ const tool = 'compose';
133
+ const toolTemplate = 'docker-compose run --service-ports $1';
134
+ const item: ItemEntry = { name: 'web', value: 'web' };
135
+
136
+ const result = TemplateExpander.parseItem(tool, toolTemplate, item, 0);
137
+
138
+ assert.strictEqual(result.name, 'web');
139
+ assert.strictEqual(result.fullCmd, 'docker-compose run --service-ports web');
140
+ });
141
+
142
+ it('should handle templates with no placeholders', () => {
143
+ const tool = 'echo';
144
+ const toolTemplate = 'echo hello world';
145
+ const item: ItemEntry = { name: 'item1', value: 'item1' };
146
+
147
+ const result = TemplateExpander.parseItem(tool, toolTemplate, item, 0);
148
+
149
+ assert.strictEqual(result.name, 'item1');
150
+ assert.strictEqual(result.fullCmd, 'echo hello world');
151
+ });
152
+ });
153
+
154
+ describe('without registered tool (direct executable)', () => {
155
+ it('should prefix command with tool name', () => {
156
+ const tool = 'node';
157
+ const toolTemplate = null;
158
+ const item: ItemEntry = { name: 'server', value: 'server.js,3000' };
159
+
160
+ const result = TemplateExpander.parseItem(tool, toolTemplate, item, 0);
161
+
162
+ assert.strictEqual(result.name, 'server');
163
+ assert.strictEqual(result.fullCmd, 'node server.js,3000');
164
+ });
165
+
166
+ it('should handle null tool and toolTemplate', () => {
167
+ const tool = null;
168
+ const toolTemplate = null;
169
+ const item: ItemEntry = { name: 'echo', value: 'echo hello' };
170
+
171
+ const result = TemplateExpander.parseItem(tool, toolTemplate, item, 0);
172
+
173
+ assert.strictEqual(result.name, 'echo');
174
+ assert.strictEqual(result.fullCmd, 'echo hello');
175
+ });
176
+
177
+ it('should use ItemEntry name when value is empty', () => {
178
+ const tool = null;
179
+ const toolTemplate = null;
180
+ const item: ItemEntry = { name: 'item-3', value: '' };
181
+
182
+ const result = TemplateExpander.parseItem(tool, toolTemplate, item, 3);
183
+
184
+ assert.strictEqual(result.name, 'item-3');
185
+ });
186
+
187
+ it('should handle multiple items with same tool', () => {
188
+ const tool = 'python';
189
+ const toolTemplate = null;
190
+
191
+ const result1 = TemplateExpander.parseItem(tool, toolTemplate, { name: 'script1', value: 'script1.py' }, 0);
192
+ const result2 = TemplateExpander.parseItem(tool, toolTemplate, { name: 'script2', value: 'script2.py' }, 1);
193
+
194
+ assert.strictEqual(result1.name, 'script1');
195
+ assert.strictEqual(result2.name, 'script2');
196
+ });
197
+ });
198
+ });
199
+
200
+ describe('Real-world scenarios', () => {
201
+ it('should handle docker service templates', () => {
202
+ const toolTemplate = 'docker run -d --name $1_$2 -p $2:$3 $1';
203
+ const item: ItemEntry = { name: 'nginx', value: 'nginx,8080,80' };
204
+
205
+ const result = TemplateExpander.expand(toolTemplate, item, 0);
206
+
207
+ assert.strictEqual(result.name, 'nginx');
208
+ assert.strictEqual(result.fullCmd, 'docker run -d --name nginx_8080 -p 8080:80 nginx');
209
+ });
210
+
211
+ it('should handle node process manager templates', () => {
212
+ const toolTemplate = 'node $1 --port $2';
213
+ const item: ItemEntry = { name: 'server', value: 'server.js,3000' };
214
+
215
+ const result = TemplateExpander.expand(toolTemplate, item, 0);
216
+
217
+ assert.strictEqual(result.name, 'server');
218
+ assert.strictEqual(result.fullCmd, 'node server.js --port 3000');
219
+ });
220
+
221
+ it('should handle database connection templates', () => {
222
+ const toolTemplate = 'psql -h $1 -p $2 -U $3 -d $4';
223
+ const item: ItemEntry = { name: 'localhost', value: 'localhost,5432,admin,mydb' };
224
+
225
+ const result = TemplateExpander.expand(toolTemplate, item, 0);
226
+
227
+ assert.strictEqual(result.name, 'localhost');
228
+ assert.strictEqual(result.fullCmd, 'psql -h localhost -p 5432 -U admin -d mydb');
229
+ });
230
+
231
+ it('should handle custom script execution', () => {
232
+ const toolTemplate = './scripts/$1.sh $2 $3';
233
+ const item: ItemEntry = { name: 'deploy', value: 'deploy,production,force' };
234
+
235
+ const result = TemplateExpander.expand(toolTemplate, item, 0);
236
+
237
+ assert.strictEqual(result.name, 'deploy');
238
+ assert.strictEqual(result.fullCmd, './scripts/deploy.sh production force');
239
+ });
240
+
241
+ it('should handle Windows executable paths', () => {
242
+ const toolTemplate = '"C:\\My App\\app.exe" --config $1 --port $2';
243
+ const item: ItemEntry = { name: 'config', value: 'config.json,8080' };
244
+
245
+ const result = TemplateExpander.expand(toolTemplate, item, 0);
246
+
247
+ assert.strictEqual(result.name, 'config');
248
+ assert.strictEqual(result.fullCmd, '"C:\\My App\\app.exe" --config config.json --port 8080');
249
+ });
250
+ });
251
+
252
+ describe('Edge cases and error handling', () => {
253
+ it('should handle very long argument lists', () => {
254
+ const template = Array.from({ length: 20 }, (_, i) => `$${i + 1}`).join(' ');
255
+ const item: ItemEntry = { name: 'args', value: Array.from({ length: 20 }, (_, i) => `arg${i}`).join(',') };
256
+
257
+ const result = TemplateExpander.expand(template, item, 0);
258
+
259
+ assert.strictEqual(result.args.length, 20);
260
+ assert.ok(result.fullCmd.includes('arg0'));
261
+ assert.ok(result.fullCmd.includes('arg19'));
262
+ });
263
+
264
+ it('should handle special characters in arguments', () => {
265
+ const template = 'echo "$1" "$2"';
266
+ const item: ItemEntry = { name: 'args', value: 'hello world,test&argument' };
267
+
268
+ const result = TemplateExpander.expand(template, item, 0);
269
+
270
+ assert.deepStrictEqual(result.args, ['hello world', 'test&argument']);
271
+ });
272
+
273
+ it('should handle unicode characters', () => {
274
+ const template = 'echo $1 $2';
275
+ const item: ItemEntry = { name: 'unicode', value: 'こんにちは,世界' };
276
+
277
+ const result = TemplateExpander.expand(template, item, 0);
278
+
279
+ assert.strictEqual(result.name, 'unicode');
280
+ assert.deepStrictEqual(result.args, ['こんにちは', '世界']);
281
+ assert.strictEqual(result.fullCmd, 'echo こんにちは 世界');
282
+ });
283
+
284
+ it('should handle numeric arguments', () => {
285
+ const template = 'app --port $1 --timeout $2 --retries $3';
286
+ const item: ItemEntry = { name: 'ports', value: '8080,5000,5' };
287
+
288
+ const result = TemplateExpander.expand(template, item, 0);
289
+
290
+ assert.strictEqual(result.fullCmd, 'app --port 8080 --timeout 5000 --retries 5');
291
+ });
292
+
293
+ it('should preserve whitespace in command template', () => {
294
+ const template = 'cmd $1 $2';
295
+ const item: ItemEntry = { name: 'args', value: 'a,b' };
296
+
297
+ const result = TemplateExpander.expand(template, item, 0);
298
+
299
+ // Template whitespace is preserved
300
+ assert.strictEqual(result.fullCmd, 'cmd a b');
301
+ });
302
+ });
303
+
304
+ describe('Multiple items in sequence', () => {
305
+ it('should generate unique names using ItemEntry name', () => {
306
+ const template = 'worker $1';
307
+ const items: ItemEntry[] = [
308
+ { name: 'job1', value: 'job1' },
309
+ { name: 'job2', value: 'job2' },
310
+ { name: 'job3', value: 'job3' }
311
+ ];
312
+
313
+ const results = items.map((item, index) =>
314
+ TemplateExpander.expand(template, item, index)
315
+ );
316
+
317
+ assert.strictEqual(results[0].name, 'job1');
318
+ assert.strictEqual(results[1].name, 'job2');
319
+ assert.strictEqual(results[2].name, 'job3');
320
+ });
321
+
322
+ it('should use ItemEntry name for identical values', () => {
323
+ const template = 'echo $1';
324
+ const items: ItemEntry[] = [
325
+ { name: 'test1', value: 'test' },
326
+ { name: 'test2', value: 'test' },
327
+ { name: 'test3', value: 'test' }
328
+ ];
329
+
330
+ const results = items.map((item, index) =>
331
+ TemplateExpander.expand(template, item, index)
332
+ );
333
+
334
+ // All will have their unique ItemEntry name
335
+ assert.strictEqual(results[0].name, 'test1');
336
+ assert.strictEqual(results[1].name, 'test2');
337
+ assert.strictEqual(results[2].name, 'test3');
338
+ });
339
+ });
340
+
341
+ describe('ProcessItem type compliance', () => {
342
+ it('should return valid ProcessItem objects', () => {
343
+ const template = 'cmd $1';
344
+ const item: ItemEntry = { name: 'arg1', value: 'arg1,arg2' };
345
+
346
+ const result: ProcessItem = TemplateExpander.expand(template, item, 0);
347
+
348
+ assert.strictEqual(typeof result.name, 'string');
349
+ assert.ok(Array.isArray(result.args));
350
+ assert.strictEqual(typeof result.fullCmd, 'string');
351
+ assert.ok(result.name.length > 0);
352
+ assert.ok(result.fullCmd.length > 0);
353
+ });
354
+
355
+ it('should have consistent structure across different calls', () => {
356
+ const results: ProcessItem[] = [
357
+ TemplateExpander.expand('echo $1', { name: 'test', value: 'test' }, 0),
358
+ TemplateExpander.expand('cmd $1 $2', { name: 'args', value: 'a,b' }, 1),
359
+ TemplateExpander.parseItem('node', null, { name: 'app', value: 'app.js' }, 2),
360
+ ];
361
+
362
+ for (const result of results) {
363
+ assert.ok('name' in result);
364
+ assert.ok('args' in result);
365
+ assert.ok('fullCmd' in result);
366
+ assert.ok(Array.isArray(result.args));
367
+ }
368
+ });
369
+ });
370
+
371
+ describe('Named params', () => {
372
+ it('should replace named param in template', () => {
373
+ const template = 'node $1.js --name $name';
374
+ const item: ItemEntry = { name: 'server', value: 'server' };
375
+ const params = { name: 'John doe' };
376
+
377
+ const result = TemplateExpander.expand(template, item, 0, params);
378
+
379
+ assert.strictEqual(result.name, 'server');
380
+ assert.strictEqual(result.fullCmd, 'node server.js --name John doe');
381
+ });
382
+
383
+ it('should replace multiple named params', () => {
384
+ const template = 'app --host $host --port $port --env $env';
385
+ const item: ItemEntry = { name: 'myapp', value: 'myapp' };
386
+ const params = { host: 'localhost', port: '3000', env: 'production' };
387
+
388
+ const result = TemplateExpander.expand(template, item, 0, params);
389
+
390
+ assert.strictEqual(result.fullCmd, 'app --host localhost --port 3000 --env production');
391
+ });
392
+
393
+ it('should combine positional and named params', () => {
394
+ const template = 'node $1.js --name $name --port $port';
395
+ const item: ItemEntry = { name: 'server', value: 'server' };
396
+ const params = { name: 'Alice', port: '8080' };
397
+
398
+ const result = TemplateExpander.expand(template, item, 0, params);
399
+
400
+ assert.strictEqual(result.fullCmd, 'node server.js --name Alice --port 8080');
401
+ });
402
+
403
+ it('should handle empty params object', () => {
404
+ const template = 'node $1.js';
405
+ const item: ItemEntry = { name: 'server', value: 'server' };
406
+
407
+ const result = TemplateExpander.expand(template, item, 0, {});
408
+
409
+ assert.strictEqual(result.fullCmd, 'node server.js');
410
+ });
411
+
412
+ it('should leave unreplaced named params as-is', () => {
413
+ const template = 'node $1.js --name $name --env $env';
414
+ const item: ItemEntry = { name: 'server', value: 'server' };
415
+ const params = { name: 'Bob' }; // env not provided
416
+
417
+ const result = TemplateExpander.expand(template, item, 0, params);
418
+
419
+ assert.strictEqual(result.fullCmd, 'node server.js --name Bob --env $env');
420
+ });
421
+
422
+ it('should replace all occurrences of named param', () => {
423
+ const template = 'echo $name and $name again';
424
+ const item: ItemEntry = { name: 'test', value: 'test' };
425
+ const params = { name: 'world' };
426
+
427
+ const result = TemplateExpander.expand(template, item, 0, params);
428
+
429
+ assert.strictEqual(result.fullCmd, 'echo world and world again');
430
+ });
431
+
432
+ it('should work with parseItem for registered tools', () => {
433
+ const tool = 'node-param';
434
+ const toolTemplate = 'node $1.js --name $name';
435
+ const item: ItemEntry = { name: 'server', value: 'server' };
436
+ const params = { name: 'Charlie' };
437
+
438
+ const result = TemplateExpander.parseItem(tool, toolTemplate, item, 0, params);
439
+
440
+ assert.strictEqual(result.name, 'server');
441
+ assert.strictEqual(result.fullCmd, 'node server.js --name Charlie');
442
+ });
443
+
444
+ it('should handle named params with spaces in values', () => {
445
+ const template = 'echo "Hello, $name!"';
446
+ const item: ItemEntry = { name: 'test', value: 'test' };
447
+ const params = { name: 'John Doe' };
448
+
449
+ const result = TemplateExpander.expand(template, item, 0, params);
450
+
451
+ assert.strictEqual(result.fullCmd, 'echo "Hello, John Doe!"');
452
+ });
453
+ });
454
+ });
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "esModuleInterop": true,
6
+ "skipLibCheck": true,
7
+ "allowJs": true,
8
+ "checkJs": false,
9
+ "moduleResolution": "bundler",
10
+ "outDir": "./dist",
11
+ "rootDir": "./src"
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules", "dist"]
15
+ }
@@ -0,0 +1,9 @@
1
+ # Commands
2
+ ## cligr up <group>
3
+ Runs each item in a attached thread to the current process using the tool/executable indicated.
4
+ ## cligr down <group>
5
+ Kills all threads from this group
6
+ ## cligr ls <group>
7
+ Shows all items from this group (indicating if running or stopped)
8
+ ## cligr config
9
+ Open config file