cligr 1.0.5 → 1.0.7

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.
@@ -0,0 +1,164 @@
1
+ # Named Items Design
2
+
3
+ **Date:** 2026-02-25
4
+ **Status:** Approved
5
+
6
+ ## Overview
7
+
8
+ Update the config system to support named items in groups. Instead of an array of item strings, items will be a map where keys are explicit names and values are the item strings.
9
+
10
+ ## Motivation
11
+
12
+ - **Better identification:** Explicit names make it easier to identify services in logs and `ls` output
13
+ - **Consistent naming:** Process names, log prefixes, and PID files all use the explicit name
14
+ - **Clearer config:** Named items are more self-documenting than positional arrays
15
+
16
+ ## Changes
17
+
18
+ ### 1. Type Changes (`src/config/types.ts`)
19
+
20
+ Add new `ItemEntry` type and update `GroupConfig`:
21
+
22
+ ```typescript
23
+ export interface ItemEntry {
24
+ name: string; // the key from config (e.g., "nginxService1")
25
+ value: string; // the value string (e.g., "nginx,8080")
26
+ }
27
+
28
+ export interface GroupConfig {
29
+ tool: string;
30
+ restart?: 'yes' | 'no' | 'unless-stopped';
31
+ params?: Record<string, string>;
32
+ items: Record<string, string>; // only named format
33
+ }
34
+ ```
35
+
36
+ ### 2. Config Loader Changes (`src/config/loader.ts`)
37
+
38
+ - Add `normalizeItems()` method to convert object to `ItemEntry[]`
39
+ - Update `getGroup()` to return normalized items
40
+ - Add validation for items format and unique names
41
+
42
+ ```typescript
43
+ private normalizeItems(items: Record<string, string>): ItemEntry[] {
44
+ return Object.entries(items).map(([name, value]) => ({
45
+ name,
46
+ value
47
+ }));
48
+ }
49
+
50
+ private validateItems(items: unknown, groupName: string): void {
51
+ if (!items || typeof items !== 'object' || Array.isArray(items)) {
52
+ throw new ConfigError(
53
+ 'items must be an object with named entries, e.g.:\n' +
54
+ ' items:\n' +
55
+ ' serviceName: "value1,value2"'
56
+ );
57
+ }
58
+
59
+ const seenNames = new Set<string>();
60
+
61
+ for (const [name, value] of Object.entries(items as Record<string, unknown>)) {
62
+ if (typeof value !== 'string') {
63
+ throw new ConfigError(`Item "${name}" must have a string value`);
64
+ }
65
+
66
+ if (seenNames.has(name)) {
67
+ throw new ConfigError(
68
+ `Duplicate item name "${name}" in group "${groupName}". ` +
69
+ `Item names must be unique within a group.`
70
+ );
71
+ }
72
+ seenNames.add(name);
73
+ }
74
+ }
75
+ ```
76
+
77
+ ### 3. TemplateExpander Changes (`src/process/template.ts`)
78
+
79
+ Update `expand()` and `parseItem()` to accept `ItemEntry`:
80
+
81
+ ```typescript
82
+ static expand(
83
+ template: string,
84
+ item: ItemEntry,
85
+ index: number,
86
+ params: Record<string, string> = {}
87
+ ): ProcessItem {
88
+ const args = item.value.split(',').map(s => s.trim());
89
+ const name = item.name; // Use explicit name
90
+ // ... rest of expansion logic
91
+ }
92
+ ```
93
+
94
+ ### 4. ls Command Changes (`src/commands/ls.ts`)
95
+
96
+ Display items in `name: value` format:
97
+
98
+ ```typescript
99
+ for (const item of items) {
100
+ console.log(` ${item.name}: ${item.value}`);
101
+ }
102
+ ```
103
+
104
+ **Example output:**
105
+ ```
106
+ Group: web
107
+ Tool: docker
108
+ Restart: false
109
+
110
+ Items:
111
+ nginxService1: nginx,8080
112
+ nginxService2: nginx,3000
113
+ ```
114
+
115
+ ### 5. up Command Changes (`src/commands/up.ts`)
116
+
117
+ Pass `ItemEntry` to template expander:
118
+
119
+ ```typescript
120
+ const processItems = items.map((item, index) =>
121
+ TemplateExpander.parseItem(tool, toolTemplate, item, index, params)
122
+ );
123
+ ```
124
+
125
+ ### 6. No Changes Required
126
+
127
+ - `src/process/manager.ts` - Uses `ProcessItem.name` as-is, no changes needed
128
+ - `src/process/pid-store.ts` - Uses item name passed from manager, no changes needed
129
+
130
+ ## Config Example
131
+
132
+ **Before:**
133
+ ```yaml
134
+ groups:
135
+ web:
136
+ tool: docker
137
+ restart: false
138
+ items:
139
+ - "nginx,8080"
140
+ - "nginx,3000"
141
+ ```
142
+
143
+ **After:**
144
+ ```yaml
145
+ groups:
146
+ web:
147
+ tool: docker
148
+ restart: false
149
+ items:
150
+ nginxService1: "nginx,8080"
151
+ nginxService2: "nginx,3000"
152
+ ```
153
+
154
+ ## Files to Modify
155
+
156
+ 1. `src/config/types.ts` - Add `ItemEntry`, update `GroupConfig.items`
157
+ 2. `src/config/loader.ts` - Add normalization and validation
158
+ 3. `src/process/template.ts` - Accept `ItemEntry`
159
+ 4. `src/commands/ls.ts` - Update output format
160
+ 5. `src/commands/up.ts` - Pass `ItemEntry` to expander
161
+
162
+ ## Breaking Change
163
+
164
+ This is a **breaking change** for existing configs. Users must update their configs to use the named format.
@@ -0,0 +1,460 @@
1
+ # Named Items Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Replace array-based items with named object format for better service identification in logs, PID files, and ls output.
6
+
7
+ **Architecture:** Add `ItemEntry` type to normalize named items, update config loader to validate and normalize, pass ItemEntry through template expander to process manager.
8
+
9
+ **Tech Stack:** TypeScript, js-yaml, Node.js
10
+
11
+ ---
12
+
13
+ ## Task 1: Update Types
14
+
15
+ **Files:**
16
+ - Modify: `src/config/types.ts:5-10`
17
+
18
+ **Step 1: Add ItemEntry interface**
19
+
20
+ Add after `ToolConfig` interface (around line 4):
21
+
22
+ ```typescript
23
+ export interface ItemEntry {
24
+ name: string; // the key from config (e.g., "nginxService1")
25
+ value: string; // the value string (e.g., "nginx,8080")
26
+ }
27
+ ```
28
+
29
+ **Step 2: Update GroupConfig items type**
30
+
31
+ Change line 9 from:
32
+ ```typescript
33
+ items: string[];
34
+ ```
35
+
36
+ To:
37
+ ```typescript
38
+ items: Record<string, string>;
39
+ ```
40
+
41
+ **Step 3: Verify types compile**
42
+
43
+ Run: `npm run typecheck`
44
+ Expected: No errors
45
+
46
+ **Step 4: Commit**
47
+
48
+ ```bash
49
+ git add src/config/types.ts
50
+ git commit -m "feat(types): add ItemEntry and update GroupConfig items to named format"
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Task 2: Update Config Loader - Validation
56
+
57
+ **Files:**
58
+ - Modify: `src/config/loader.ts:60-72`
59
+
60
+ **Step 1: Update validate method to validate items**
61
+
62
+ Replace the `validate` method (lines 60-72) with:
63
+
64
+ ```typescript
65
+ private validate(config: unknown): CliGrConfig {
66
+ if (!config || typeof config !== 'object') {
67
+ throw new ConfigError('Config must be an object');
68
+ }
69
+
70
+ const cfg = config as Record<string, unknown>;
71
+
72
+ if (!cfg.groups || typeof cfg.groups !== 'object') {
73
+ throw new ConfigError('Config must have a "groups" object');
74
+ }
75
+
76
+ // Validate each group's items
77
+ for (const [groupName, group] of Object.entries(cfg.groups as Record<string, unknown>)) {
78
+ if (group && typeof group === 'object') {
79
+ const groupObj = group as Record<string, unknown>;
80
+ this.validateItems(groupObj.items, groupName);
81
+ }
82
+ }
83
+
84
+ return cfg as unknown as CliGrConfig;
85
+ }
86
+
87
+ private validateItems(items: unknown, groupName: string): void {
88
+ if (!items || typeof items !== 'object' || Array.isArray(items)) {
89
+ throw new ConfigError(
90
+ `Group "${groupName}": items must be an object with named entries, e.g.:\n` +
91
+ ' items:\n' +
92
+ ' serviceName: "value1,value2"'
93
+ );
94
+ }
95
+
96
+ const seenNames = new Set<string>();
97
+
98
+ for (const [name, value] of Object.entries(items as Record<string, unknown>)) {
99
+ if (typeof value !== 'string') {
100
+ throw new ConfigError(`Group "${groupName}": item "${name}" must have a string value`);
101
+ }
102
+
103
+ if (seenNames.has(name)) {
104
+ throw new ConfigError(
105
+ `Group "${groupName}": duplicate item name "${name}". ` +
106
+ `Item names must be unique within a group.`
107
+ );
108
+ }
109
+ seenNames.add(name);
110
+ }
111
+ }
112
+ ```
113
+
114
+ **Step 2: Add ItemEntry import**
115
+
116
+ Add `ItemEntry` to the import at line 5:
117
+ ```typescript
118
+ import type { CliGrConfig, GroupConfig, ToolConfig, ItemEntry } from './types.js';
119
+ ```
120
+
121
+ **Step 3: Verify types compile**
122
+
123
+ Run: `npm run typecheck`
124
+ Expected: No errors
125
+
126
+ **Step 4: Commit**
127
+
128
+ ```bash
129
+ git add src/config/loader.ts
130
+ git commit -m "feat(loader): add validation for named items format"
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Task 3: Update Config Loader - Normalization
136
+
137
+ **Files:**
138
+ - Modify: `src/config/loader.ts:74-100`
139
+
140
+ **Step 1: Add normalizeItems method**
141
+
142
+ Add after `validateItems` method:
143
+
144
+ ```typescript
145
+ private normalizeItems(items: Record<string, string>): ItemEntry[] {
146
+ return Object.entries(items).map(([name, value]) => ({
147
+ name,
148
+ value
149
+ }));
150
+ }
151
+ ```
152
+
153
+ **Step 2: Update getGroup return type and implementation**
154
+
155
+ Replace the `getGroup` method (lines 74-100) with:
156
+
157
+ ```typescript
158
+ getGroup(name: string): { config: GroupConfig; items: ItemEntry[]; tool: string | null; toolTemplate: string | null; params: Record<string, string> } {
159
+ const config = this.load();
160
+ const group = config.groups[name];
161
+
162
+ if (!group) {
163
+ const available = Object.keys(config.groups).join(', ');
164
+ throw new ConfigError(`Unknown group: ${name}. Available: ${available}`);
165
+ }
166
+
167
+ // Normalize items to ItemEntry[]
168
+ const items = this.normalizeItems(group.items);
169
+
170
+ // Resolve tool
171
+ let toolTemplate: string | null = null;
172
+ let tool: string | null = null;
173
+
174
+ if (config.tools && config.tools[group.tool]) {
175
+ toolTemplate = config.tools[group.tool].cmd;
176
+ tool = group.tool;
177
+ } else {
178
+ tool = null;
179
+ toolTemplate = null;
180
+ }
181
+
182
+ const params = group.params || {};
183
+
184
+ return { config: group, items, tool, toolTemplate, params };
185
+ }
186
+ ```
187
+
188
+ **Step 3: Verify types compile**
189
+
190
+ Run: `npm run typecheck`
191
+ Expected: Errors in ls.ts and up.ts (they need updates)
192
+
193
+ **Step 4: Commit**
194
+
195
+ ```bash
196
+ git add src/config/loader.ts
197
+ git commit -m "feat(loader): normalize items to ItemEntry[]"
198
+ ```
199
+
200
+ ---
201
+
202
+ ## Task 4: Update TemplateExpander
203
+
204
+ **Files:**
205
+ - Modify: `src/process/template.ts:26-44`
206
+
207
+ **Step 1: Add ItemEntry import**
208
+
209
+ Add at top of file:
210
+ ```typescript
211
+ import type { ProcessItem, ItemEntry } from '../config/types.js';
212
+ ```
213
+
214
+ **Step 2: Update expand method signature and implementation**
215
+
216
+ Replace the `expand` method (lines 26-44) with:
217
+
218
+ ```typescript
219
+ static expand(template: string, item: ItemEntry, index: number, params: Record<string, string> = {}): ProcessItem {
220
+ const args = item.value.split(',').map(s => s.trim());
221
+
222
+ // Use explicit name from ItemEntry
223
+ const name = item.name;
224
+
225
+ // Replace $1, $2, $3 etc. with args (positional params)
226
+ let fullCmd = template;
227
+ for (let i = args.length - 1; i >= 0; i--) {
228
+ const placeholder = `$${i + 1}`;
229
+ fullCmd = fullCmd.replaceAll(placeholder, args[i]);
230
+ }
231
+
232
+ // Replace named params ($name, $env, etc.) AFTER positional params
233
+ fullCmd = this.expandNamedParams(fullCmd, params);
234
+
235
+ return { name, args, fullCmd };
236
+ }
237
+ ```
238
+
239
+ **Step 3: Update parseItem method signature and implementation**
240
+
241
+ Replace the `parseItem` method (lines 54-97) with:
242
+
243
+ ```typescript
244
+ static parseItem(
245
+ tool: string | null,
246
+ toolTemplate: string | null,
247
+ item: ItemEntry,
248
+ index: number,
249
+ params: Record<string, string> = {}
250
+ ): ProcessItem {
251
+ if (toolTemplate) {
252
+ // Use registered tool template
253
+ const result = this.expand(toolTemplate, item, index, params);
254
+
255
+ // If there are more args than placeholders in the template, append them
256
+ const placeholdersInTemplate = (toolTemplate.match(/\$\d+/g) || []);
257
+ let maxPlaceholder = 0;
258
+ for (const p of placeholdersInTemplate) {
259
+ const num = parseInt(p.substring(1), 10);
260
+ if (num > maxPlaceholder) maxPlaceholder = num;
261
+ }
262
+
263
+ if (maxPlaceholder > 0 && result.args.length > maxPlaceholder) {
264
+ const remainingArgs = result.args.slice(maxPlaceholder);
265
+ result.fullCmd = `${result.fullCmd} ${remainingArgs.join(' ')}`;
266
+ }
267
+
268
+ return result;
269
+ } else {
270
+ // Direct executable - use tool as command prefix
271
+ const args = item.value.split(',').map(s => s.trim());
272
+ const name = item.name;
273
+ const fullCmd = tool ? `${tool} ${item.value}` : item.value;
274
+ return { name, args, fullCmd };
275
+ }
276
+ }
277
+ ```
278
+
279
+ **Step 4: Verify types compile**
280
+
281
+ Run: `npm run typecheck`
282
+ Expected: Errors in up.ts only
283
+
284
+ **Step 5: Commit**
285
+
286
+ ```bash
287
+ git add src/process/template.ts
288
+ git commit -m "feat(template): accept ItemEntry instead of raw string"
289
+ ```
290
+
291
+ ---
292
+
293
+ ## Task 5: Update ls Command
294
+
295
+ **Files:**
296
+ - Modify: `src/commands/ls.ts:14-17`
297
+
298
+ **Step 1: Update ls to use normalized items**
299
+
300
+ Replace the `lsCommand` function with:
301
+
302
+ ```typescript
303
+ export async function lsCommand(groupName: string): Promise<number> {
304
+ const loader = new ConfigLoader();
305
+
306
+ try {
307
+ const { config, items } = loader.getGroup(groupName);
308
+
309
+ console.log(`\nGroup: ${groupName}`);
310
+ console.log(`Tool: ${config.tool}`);
311
+ console.log(`Restart: ${config.restart}`);
312
+ console.log('\nItems:');
313
+
314
+ for (const item of items) {
315
+ console.log(` ${item.name}: ${item.value}`);
316
+ }
317
+
318
+ console.log('');
319
+
320
+ return 0;
321
+ } catch (err) {
322
+ console.error((err as Error).message);
323
+ return 1;
324
+ }
325
+ }
326
+ ```
327
+
328
+ **Step 2: Verify types compile**
329
+
330
+ Run: `npm run typecheck`
331
+ Expected: No errors
332
+
333
+ **Step 3: Commit**
334
+
335
+ ```bash
336
+ git add src/commands/ls.ts
337
+ git commit -m "feat(ls): display named items in name: value format"
338
+ ```
339
+
340
+ ---
341
+
342
+ ## Task 6: Update up Command
343
+
344
+ **Files:**
345
+ - Modify: `src/commands/up.ts:19-21`
346
+
347
+ **Step 1: Update up to use normalized items**
348
+
349
+ Replace line 16 with:
350
+ ```typescript
351
+ const { config, items, tool, toolTemplate, params } = loader.getGroup(groupName);
352
+ ```
353
+
354
+ Replace lines 19-21 with:
355
+ ```typescript
356
+ // Build process items
357
+ const processItems = items.map((item, index) =>
358
+ TemplateExpander.parseItem(tool, toolTemplate, item, index, params)
359
+ );
360
+
361
+ // Spawn all processes
362
+ manager.spawnGroup(groupName, processItems, config.restart);
363
+ ```
364
+
365
+ **Step 2: Verify types compile**
366
+
367
+ Run: `npm run typecheck`
368
+ Expected: No errors
369
+
370
+ **Step 3: Commit**
371
+
372
+ ```bash
373
+ git add src/commands/up.ts
374
+ git commit -m "feat(up): pass ItemEntry to template expander"
375
+ ```
376
+
377
+ ---
378
+
379
+ ## Task 7: Update Tests
380
+
381
+ **Files:**
382
+ - Modify: `tests/integration/process-manager.test.ts`
383
+
384
+ **Step 1: Run existing tests to identify failures**
385
+
386
+ Run: `npm test`
387
+ Expected: Some tests may fail due to config format change
388
+
389
+ **Step 2: Check test file for config usage**
390
+
391
+ Read the test file and identify any inline configs that use array format for items.
392
+
393
+ **Step 3: Update test configs to named format**
394
+
395
+ Change any test configs from:
396
+ ```yaml
397
+ items:
398
+ - "nginx,8080"
399
+ ```
400
+
401
+ To:
402
+ ```yaml
403
+ items:
404
+ nginxService: "nginx,8080"
405
+ ```
406
+
407
+ **Step 4: Run tests to verify**
408
+
409
+ Run: `npm test`
410
+ Expected: All tests pass
411
+
412
+ **Step 5: Commit**
413
+
414
+ ```bash
415
+ git add tests/integration/process-manager.test.ts
416
+ git commit -m "test: update tests to use named items format"
417
+ ```
418
+
419
+ ---
420
+
421
+ ## Task 8: Final Verification
422
+
423
+ **Step 1: Run full typecheck**
424
+
425
+ Run: `npm run typecheck`
426
+ Expected: No errors
427
+
428
+ **Step 2: Run all tests**
429
+
430
+ Run: `npm test`
431
+ Expected: All tests pass
432
+
433
+ **Step 3: Manual smoke test**
434
+
435
+ Create a test config file with named items and verify:
436
+ - `cligr groups` lists groups
437
+ - `cligr ls <group>` shows named items correctly
438
+ - `cligr up <group>` starts processes with correct names in logs
439
+
440
+ **Step 4: Final commit (if any fixes needed)**
441
+
442
+ ```bash
443
+ git add -A
444
+ git commit -m "fix: final cleanup for named items feature"
445
+ ```
446
+
447
+ ---
448
+
449
+ ## Summary
450
+
451
+ | Task | Description | Files |
452
+ |------|-------------|-------|
453
+ | 1 | Update types | `src/config/types.ts` |
454
+ | 2 | Add validation | `src/config/loader.ts` |
455
+ | 3 | Add normalization | `src/config/loader.ts` |
456
+ | 4 | Update template expander | `src/process/template.ts` |
457
+ | 5 | Update ls command | `src/commands/ls.ts` |
458
+ | 6 | Update up command | `src/commands/up.ts` |
459
+ | 7 | Update tests | `tests/integration/process-manager.test.ts` |
460
+ | 8 | Final verification | - |