cligr 1.0.6 → 1.0.8

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 (43) hide show
  1. package/.claude/worktrees/agent-ac25cfb2/README.md +65 -0
  2. package/.claude/worktrees/agent-ac25cfb2/docs/plans/2026-02-13-named-params-support.md +391 -0
  3. package/.claude/worktrees/agent-ac25cfb2/docs/plans/2026-02-25-named-items-design.md +164 -0
  4. package/.claude/worktrees/agent-ac25cfb2/docs/plans/2026-02-25-named-items-implementation.md +460 -0
  5. package/.claude/worktrees/agent-ac25cfb2/package-lock.json +554 -0
  6. package/.claude/worktrees/agent-ac25cfb2/package.json +27 -0
  7. package/.claude/worktrees/agent-ac25cfb2/scripts/build.js +20 -0
  8. package/.claude/worktrees/agent-ac25cfb2/scripts/test.js +168 -0
  9. package/.claude/worktrees/agent-ac25cfb2/src/commands/config.ts +121 -0
  10. package/.claude/worktrees/agent-ac25cfb2/src/commands/groups.ts +68 -0
  11. package/.claude/worktrees/agent-ac25cfb2/src/commands/ls.ts +25 -0
  12. package/.claude/worktrees/agent-ac25cfb2/src/commands/up.ts +49 -0
  13. package/.claude/worktrees/agent-ac25cfb2/src/config/loader.ts +148 -0
  14. package/.claude/worktrees/agent-ac25cfb2/src/config/types.ts +26 -0
  15. package/.claude/worktrees/agent-ac25cfb2/src/index.ts +97 -0
  16. package/.claude/worktrees/agent-ac25cfb2/src/process/manager.ts +270 -0
  17. package/.claude/worktrees/agent-ac25cfb2/src/process/pid-store.ts +203 -0
  18. package/.claude/worktrees/agent-ac25cfb2/src/process/template.ts +87 -0
  19. package/.claude/worktrees/agent-ac25cfb2/tests/integration/blocking-processes-fixed.test.ts +255 -0
  20. package/.claude/worktrees/agent-ac25cfb2/tests/integration/blocking-processes.test.ts +497 -0
  21. package/.claude/worktrees/agent-ac25cfb2/tests/integration/commands.test.ts +648 -0
  22. package/.claude/worktrees/agent-ac25cfb2/tests/integration/config-loader.test.ts +426 -0
  23. package/.claude/worktrees/agent-ac25cfb2/tests/integration/process-manager.test.ts +394 -0
  24. package/.claude/worktrees/agent-ac25cfb2/tests/integration/template-expander.test.ts +454 -0
  25. package/.claude/worktrees/agent-ac25cfb2/tsconfig.json +15 -0
  26. package/.claude/worktrees/agent-ac25cfb2/usage.md +9 -0
  27. package/dist/index.js +247 -39
  28. package/docs/superpowers/plans/2026-04-13-improve-web-ui-console.md +256 -0
  29. package/docs/superpowers/plans/2026-04-13-serve-command.md +1299 -0
  30. package/docs/superpowers/specs/2026-04-13-improve-web-ui-console-design.md +38 -0
  31. package/docs/superpowers/specs/2026-04-13-serve-command-design.md +93 -0
  32. package/package.json +1 -1
  33. package/src/commands/ls.ts +11 -6
  34. package/src/commands/serve.ts +417 -0
  35. package/src/config/loader.ts +71 -2
  36. package/src/config/types.ts +1 -0
  37. package/src/index.ts +10 -3
  38. package/src/process/manager.ts +36 -2
  39. package/tests/integration/commands.test.ts +24 -0
  40. package/tests/integration/config-loader.test.ts +110 -0
  41. package/tests/integration/process-manager.test.ts +103 -0
  42. package/tests/integration/serve.test.ts +245 -0
  43. /package/.claude/{settings.local.json → worktrees/agent-ac25cfb2/.claude/settings.local.json} +0 -0
@@ -0,0 +1,65 @@
1
+ # Cligr
2
+
3
+ A simple CLI tool for managing groups of concurrent processes.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm i -g cligr
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ Create a `.cligr.yml` configuration file. Cligr looks for the config in:
14
+
15
+ 1. **User home directory** (`~/.cligr.yml`) - checked first
16
+ 2. **Current directory** (`./.cligr.yml`) - fallback
17
+
18
+ You can keep a global config in your home directory and override it per project.
19
+
20
+ Quick start:
21
+
22
+ ```bash
23
+ cligr config # Opens ~/.cligr.yml in your editor
24
+ ```
25
+
26
+ This creates a config file with examples if it doesn't exist.
27
+
28
+ Example config:
29
+
30
+ ```yaml
31
+ tools:
32
+ kubefwd:
33
+ cmd: kubectl port-forward $1 $2:$3
34
+
35
+ groups:
36
+ myapp:
37
+ tool: kubefwd
38
+ restart: yes
39
+ items:
40
+ - service1,8080,80
41
+ - service2,8081,80
42
+ ```
43
+
44
+ **Syntax:**
45
+ - Items are comma-separated: `"name,arg2,arg3"`
46
+ - `$1` = name (first value)
47
+ - `$2`, `$3`... = additional arguments
48
+ - If no `tool` specified, executes directly
49
+
50
+ ## Usage
51
+
52
+ ```bash
53
+ cligr config # Open config file in editor
54
+ cligr up <group> # Start all processes in group
55
+ cligr ls <group> # List group items
56
+ cligr down <group> # Stop group (Ctrl+C also works)
57
+ cligr groups # List all groups
58
+ cligr groups -v # List groups with details
59
+ ```
60
+
61
+ ## Restart Policies
62
+
63
+ - `yes` - Always restart on exit
64
+ - `no` - Never restart
65
+ - `unless-stopped` - Restart unless killed by cligr
@@ -0,0 +1,391 @@
1
+ # Named Params Support Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Add support for named parameters in group configs that can be referenced in tool templates using `$paramName` syntax.
6
+
7
+ **Architecture:** Extend the existing `TemplateExpander` to handle named params alongside positional params ($1, $2). The params are defined at the group level and passed through during template expansion. Named params are replaced AFTER positional params to avoid conflicts.
8
+
9
+ **Tech Stack:** TypeScript, Node.js, js-yaml
10
+
11
+ ---
12
+
13
+ ## Task 1: Update Types
14
+
15
+ **Files:**
16
+ - Modify: `src/config/types.ts:5-9`
17
+
18
+ **Step 1: Add params field to GroupConfig type**
19
+
20
+ Add optional `params` field to `GroupConfig`:
21
+
22
+ ```typescript
23
+ export interface GroupConfig {
24
+ tool: string;
25
+ restart?: 'yes' | 'no' | 'unless-stopped';
26
+ params?: Record<string, string>;
27
+ items: string[];
28
+ }
29
+ ```
30
+
31
+ **Step 2: Verify TypeScript compiles**
32
+
33
+ Run: `npm run typecheck`
34
+ Expected: No errors
35
+
36
+ **Step 3: Commit**
37
+
38
+ ```bash
39
+ git add src/config/types.ts
40
+ git commit -m "feat(types): add optional params field to GroupConfig"
41
+ ```
42
+
43
+ ---
44
+
45
+ ## Task 2: Update ConfigLoader
46
+
47
+ **Files:**
48
+ - Modify: `src/config/loader.ts:74-97`
49
+
50
+ **Step 1: Update getGroup return type to include params**
51
+
52
+ Modify the `getGroup` method to extract and return params from group config:
53
+
54
+ ```typescript
55
+ getGroup(name: string): { config: GroupConfig; tool: string | null; toolTemplate: string | null; params: Record<string, string> } {
56
+ const config = this.load();
57
+ const group = config.groups[name];
58
+
59
+ if (!group) {
60
+ const available = Object.keys(config.groups).join(', ');
61
+ throw new ConfigError(`Unknown group: ${name}. Available: ${available}`);
62
+ }
63
+
64
+ // Resolve tool
65
+ let toolTemplate: string | null = null;
66
+ let tool: string | null = null;
67
+
68
+ if (config.tools && config.tools[group.tool]) {
69
+ toolTemplate = config.tools[group.tool].cmd;
70
+ tool = group.tool;
71
+ } else {
72
+ tool = null;
73
+ toolTemplate = null;
74
+ }
75
+
76
+ // Extract params (default to empty object)
77
+ const params = group.params || {};
78
+
79
+ return { config: group, tool, toolTemplate, params };
80
+ }
81
+ ```
82
+
83
+ **Step 2: Verify TypeScript compiles**
84
+
85
+ Run: `npm run typecheck`
86
+ Expected: No errors
87
+
88
+ **Step 3: Commit**
89
+
90
+ ```bash
91
+ git add src/config/loader.ts
92
+ git commit -m "feat(loader): extract and return params from group config"
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Task 3: Update TemplateExpander
98
+
99
+ **Files:**
100
+ - Modify: `src/process/template.ts:10-25`
101
+
102
+ **Step 1: Add expandNamedParams helper method**
103
+
104
+ Add a new static method to handle named parameter replacement:
105
+
106
+ ```typescript
107
+ /**
108
+ * Replaces named params in template ($name, $env, etc.)
109
+ * @param template - Command template with $paramName placeholders
110
+ * @param params - Key-value pairs for substitution
111
+ * @returns Template with named params replaced
112
+ */
113
+ private static expandNamedParams(template: string, params: Record<string, string>): string {
114
+ let result = template;
115
+ for (const [key, value] of Object.entries(params)) {
116
+ const placeholder = `$${key}`;
117
+ result = result.replaceAll(placeholder, value);
118
+ }
119
+ return result;
120
+ }
121
+ ```
122
+
123
+ **Step 2: Update expand method signature and logic**
124
+
125
+ Update the `expand` method to accept optional params and apply them after positional replacement:
126
+
127
+ ```typescript
128
+ static expand(template: string, itemStr: string, index: number, params: Record<string, string> = {}): ProcessItem {
129
+ const args = itemStr.split(',').map(s => s.trim());
130
+
131
+ // Generate name from first arg or use index
132
+ const name = args[0] || `item-${index}`;
133
+
134
+ // Replace $1, $2, $3 etc. with args (positional params)
135
+ // Must replace in reverse order to avoid replacing $1 in $10, $11, etc.
136
+ let fullCmd = template;
137
+ for (let i = args.length - 1; i >= 0; i--) {
138
+ const placeholder = `$${i + 1}`;
139
+ fullCmd = fullCmd.replaceAll(placeholder, args[i]);
140
+ }
141
+
142
+ // Replace named params ($name, $env, etc.) AFTER positional params
143
+ fullCmd = this.expandNamedParams(fullCmd, params);
144
+
145
+ return { name, args, fullCmd };
146
+ }
147
+ ```
148
+
149
+ **Step 3: Update parseItem method signature**
150
+
151
+ Update `parseItem` to accept optional params:
152
+
153
+ ```typescript
154
+ static parseItem(
155
+ tool: string | null,
156
+ toolTemplate: string | null,
157
+ itemStr: string,
158
+ index: number,
159
+ params: Record<string, string> = {}
160
+ ): ProcessItem {
161
+ if (toolTemplate) {
162
+ // Use registered tool template
163
+ const result = this.expand(toolTemplate, itemStr, index, params);
164
+
165
+ // ... rest of the method stays the same
166
+ ```
167
+
168
+ **Step 4: Verify TypeScript compiles**
169
+
170
+ Run: `npm run typecheck`
171
+ Expected: No errors
172
+
173
+ **Step 5: Commit**
174
+
175
+ ```bash
176
+ git add src/process/template.ts
177
+ git commit -m "feat(template): add named params support to TemplateExpander"
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Task 4: Write Tests for Named Params
183
+
184
+ **Files:**
185
+ - Modify: `tests/integration/template-expander.test.ts`
186
+
187
+ **Step 1: Add test block for named params**
188
+
189
+ Add new describe block at the end of the test file (before the closing of the outer describe):
190
+
191
+ ```typescript
192
+ describe('Named params', () => {
193
+ it('should replace named param in template', () => {
194
+ const template = 'node $1.js --name $name';
195
+ const itemStr = 'server';
196
+ const params = { name: 'John doe' };
197
+
198
+ const result = TemplateExpander.expand(template, itemStr, 0, params);
199
+
200
+ assert.strictEqual(result.name, 'server');
201
+ assert.strictEqual(result.fullCmd, 'node server.js --name John doe');
202
+ });
203
+
204
+ it('should replace multiple named params', () => {
205
+ const template = 'app --host $host --port $port --env $env';
206
+ const itemStr = 'myapp';
207
+ const params = { host: 'localhost', port: '3000', env: 'production' };
208
+
209
+ const result = TemplateExpander.expand(template, itemStr, 0, params);
210
+
211
+ assert.strictEqual(result.fullCmd, 'app --host localhost --port 3000 --env production');
212
+ });
213
+
214
+ it('should combine positional and named params', () => {
215
+ const template = 'node $1.js --name $name --port $port';
216
+ const itemStr = 'server';
217
+ const params = { name: 'Alice', port: '8080' };
218
+
219
+ const result = TemplateExpander.expand(template, itemStr, 0, params);
220
+
221
+ assert.strictEqual(result.fullCmd, 'node server.js --name Alice --port 8080');
222
+ });
223
+
224
+ it('should handle empty params object', () => {
225
+ const template = 'node $1.js';
226
+ const itemStr = 'server';
227
+
228
+ const result = TemplateExpander.expand(template, itemStr, 0, {});
229
+
230
+ assert.strictEqual(result.fullCmd, 'node server.js');
231
+ });
232
+
233
+ it('should leave unreplaced named params as-is', () => {
234
+ const template = 'node $1.js --name $name --env $env';
235
+ const itemStr = 'server';
236
+ const params = { name: 'Bob' }; // env not provided
237
+
238
+ const result = TemplateExpander.expand(template, itemStr, 0, params);
239
+
240
+ assert.strictEqual(result.fullCmd, 'node server.js --name Bob --env $env');
241
+ });
242
+
243
+ it('should replace all occurrences of named param', () => {
244
+ const template = 'echo $name and $name again';
245
+ const itemStr = 'test';
246
+ const params = { name: 'world' };
247
+
248
+ const result = TemplateExpander.expand(template, itemStr, 0, params);
249
+
250
+ assert.strictEqual(result.fullCmd, 'echo world and world again');
251
+ });
252
+
253
+ it('should work with parseItem for registered tools', () => {
254
+ const tool = 'node-param';
255
+ const toolTemplate = 'node $1.js --name $name';
256
+ const itemStr = 'server';
257
+ const params = { name: 'Charlie' };
258
+
259
+ const result = TemplateExpander.parseItem(tool, toolTemplate, itemStr, 0, params);
260
+
261
+ assert.strictEqual(result.name, 'server');
262
+ assert.strictEqual(result.fullCmd, 'node server.js --name Charlie');
263
+ });
264
+
265
+ it('should handle named params with spaces in values', () => {
266
+ const template = 'echo "Hello, $name!"';
267
+ const itemStr = 'test';
268
+ const params = { name: 'John Doe' };
269
+
270
+ const result = TemplateExpander.expand(template, itemStr, 0, params);
271
+
272
+ assert.strictEqual(result.fullCmd, 'echo "Hello, John Doe!"');
273
+ });
274
+ });
275
+ ```
276
+
277
+ **Step 2: Run tests to verify they fail**
278
+
279
+ Run: `npm test`
280
+ Expected: Named params tests fail (feature not implemented yet)
281
+
282
+ **Step 3: Commit**
283
+
284
+ ```bash
285
+ git add tests/integration/template-expander.test.ts
286
+ git commit -m "test: add tests for named params support"
287
+ ```
288
+
289
+ ---
290
+
291
+ ## Task 5: Update up.ts Command
292
+
293
+ **Files:**
294
+ - Modify: `src/commands/up.ts:16-21`
295
+
296
+ **Step 1: Pass params to TemplateExpander.parseItem**
297
+
298
+ Update the `upCommand` to extract and pass params:
299
+
300
+ ```typescript
301
+ export async function upCommand(groupName: string): Promise<number> {
302
+ const loader = new ConfigLoader();
303
+ const manager = new ProcessManager();
304
+ const pidStore = new PidStore();
305
+
306
+ try {
307
+ // Clean up any stale PID files for this group on startup
308
+ await pidStore.cleanupStalePids();
309
+
310
+ // Load group config
311
+ const { config, tool, toolTemplate, params } = loader.getGroup(groupName);
312
+
313
+ // Build process items
314
+ const items = config.items.map((itemStr, index) =>
315
+ TemplateExpander.parseItem(tool, toolTemplate, itemStr, index, params)
316
+ );
317
+
318
+ // ... rest stays the same
319
+ ```
320
+
321
+ **Step 2: Verify TypeScript compiles**
322
+
323
+ Run: `npm run typecheck`
324
+ Expected: No errors
325
+
326
+ **Step 3: Commit**
327
+
328
+ ```bash
329
+ git add src/commands/up.ts
330
+ git commit -m "feat(up): pass params to template expander"
331
+ ```
332
+
333
+ ---
334
+
335
+ ## Task 6: Run All Tests
336
+
337
+ **Step 1: Run full test suite**
338
+
339
+ Run: `npm test`
340
+ Expected: All tests pass including new named params tests
341
+
342
+ **Step 2: Run verbose tests for details**
343
+
344
+ Run: `npm run test:verbose`
345
+ Expected: All tests pass with detailed output
346
+
347
+ ---
348
+
349
+ ## Task 7: Integration Test
350
+
351
+ **Step 1: Build the project**
352
+
353
+ Run: `npm run build`
354
+ Expected: Build succeeds
355
+
356
+ **Step 2: Create test config file**
357
+
358
+ Create a temporary test config at `~/.cligr.yml` (or use existing one) with the named params example:
359
+
360
+ ```yaml
361
+ groups:
362
+ test-named-params:
363
+ tool: node-param
364
+ params:
365
+ name: 'John doe'
366
+ items:
367
+ - "server"
368
+
369
+ tools:
370
+ node-param:
371
+ cmd: "echo $1.js --name $name"
372
+ ```
373
+
374
+ **Step 3: Run the command**
375
+
376
+ Run: `node dist/index.js test-named-params`
377
+ Expected: Output shows `echo server.js --name John doe`
378
+
379
+ ---
380
+
381
+ ## Summary
382
+
383
+ | Task | Description | Files Modified |
384
+ |------|-------------|----------------|
385
+ | 1 | Update types | `src/config/types.ts` |
386
+ | 2 | Update loader | `src/config/loader.ts` |
387
+ | 3 | Update template expander | `src/process/template.ts` |
388
+ | 4 | Write tests | `tests/integration/template-expander.test.ts` |
389
+ | 5 | Update up command | `src/commands/up.ts` |
390
+ | 6 | Run all tests | - |
391
+ | 7 | Integration test | - |
@@ -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.