@reliverse/rempts-plugin-config 2.3.1
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/README.md +174 -0
- package/dist/mod.d.ts +28 -0
- package/dist/mod.js +54 -0
- package/package.json +33 -0
- package/src/mod.ts +106 -0
package/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# rempts-plugin-config
|
|
2
|
+
|
|
3
|
+
Configuration file merger plugin for Rempts CLI framework. Loads and merges configuration from multiple sources including user home directory and project-specific config files.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add rempts-plugin-config
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createCLI } from '@reliverse/rempts-core'
|
|
15
|
+
import { configMergerPlugin } from '@reliverse/rempts-plugin-config'
|
|
16
|
+
|
|
17
|
+
const cli = await createCLI({
|
|
18
|
+
name: 'my-cli',
|
|
19
|
+
version: '1.0.0',
|
|
20
|
+
plugins: [
|
|
21
|
+
configMergerPlugin({
|
|
22
|
+
sources: [
|
|
23
|
+
'~/.config/{{name}}/config.json',
|
|
24
|
+
'.{{name}}rc.json',
|
|
25
|
+
'.{{name}}rc',
|
|
26
|
+
'package.json'
|
|
27
|
+
]
|
|
28
|
+
})
|
|
29
|
+
]
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Config is automatically merged into your CLI configuration
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Options
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
interface ConfigMergerOptions {
|
|
39
|
+
/**
|
|
40
|
+
* List of config file paths to load
|
|
41
|
+
* Supports {{name}} template which is replaced with CLI name
|
|
42
|
+
* Paths starting with ~ are expanded to home directory
|
|
43
|
+
*/
|
|
44
|
+
sources: string[]
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Merge strategy for combining configs
|
|
48
|
+
* - 'deep': Recursively merge objects (default)
|
|
49
|
+
* - 'shallow': Only merge top-level properties
|
|
50
|
+
*/
|
|
51
|
+
mergeStrategy?: 'deep' | 'shallow'
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Stop after finding the first config file
|
|
55
|
+
* Default: false (loads and merges all found configs)
|
|
56
|
+
*/
|
|
57
|
+
stopOnFirst?: boolean
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Custom config parser (e.g., for YAML, TOML)
|
|
61
|
+
* Default: JSON.parse
|
|
62
|
+
*/
|
|
63
|
+
parser?: (content: string) => any
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Transform config after loading
|
|
67
|
+
*/
|
|
68
|
+
transform?: (config: any) => any
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Config File Formats
|
|
73
|
+
|
|
74
|
+
By default, the plugin supports JSON files. Common patterns:
|
|
75
|
+
|
|
76
|
+
### RC Files
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# These are equivalent for a CLI named "my-cli"
|
|
80
|
+
.my-clirc
|
|
81
|
+
.my-clirc.json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Home Directory Config
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
~/.config/my-cli/config.json
|
|
88
|
+
~/.my-clirc
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Package.json
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"name": "my-project",
|
|
96
|
+
"version": "1.0.0",
|
|
97
|
+
"my-cli": {
|
|
98
|
+
"apiKey": "secret",
|
|
99
|
+
"theme": "dark"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Template Variables
|
|
105
|
+
|
|
106
|
+
- `{{name}}` - Replaced with your CLI's name
|
|
107
|
+
|
|
108
|
+
## Load Order
|
|
109
|
+
|
|
110
|
+
Configs are loaded in the order specified and merged together. Later configs override earlier ones.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// Example: User config overrides defaults
|
|
114
|
+
configMergerPlugin({
|
|
115
|
+
sources: [
|
|
116
|
+
'/etc/my-cli/defaults.json', // System defaults
|
|
117
|
+
'~/.config/my-cli/config.json', // User config
|
|
118
|
+
'.my-clirc' // Project config
|
|
119
|
+
]
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Stop on First
|
|
124
|
+
|
|
125
|
+
Use `stopOnFirst` to implement fallback behavior:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
configMergerPlugin({
|
|
129
|
+
sources: [
|
|
130
|
+
'.my-clirc', // Check project first
|
|
131
|
+
'~/.config/my-cli/config.json', // Then user
|
|
132
|
+
'/etc/my-cli/defaults.json' // Finally system
|
|
133
|
+
],
|
|
134
|
+
stopOnFirst: true // Use only the first found
|
|
135
|
+
})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Custom Parsers
|
|
139
|
+
|
|
140
|
+
Support other formats with custom parsers:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { parse as parseYAML } from 'yaml'
|
|
144
|
+
|
|
145
|
+
configMergerPlugin({
|
|
146
|
+
sources: ['.my-cli.yml', '.my-cli.yaml'],
|
|
147
|
+
parser: parseYAML
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Transform Configs
|
|
152
|
+
|
|
153
|
+
Apply transformations after loading:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
configMergerPlugin({
|
|
157
|
+
sources: ['.my-clirc'],
|
|
158
|
+
transform: (config) => {
|
|
159
|
+
// Expand environment variables
|
|
160
|
+
if (config.apiKey === '$API_KEY') {
|
|
161
|
+
config.apiKey = process.env.API_KEY
|
|
162
|
+
}
|
|
163
|
+
return config
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Error Handling
|
|
169
|
+
|
|
170
|
+
Missing config files are silently ignored. Parse errors are logged but don't crash the CLI.
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
MIT © blefnk
|
package/dist/mod.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config merger plugin for Rempts
|
|
3
|
+
* Loads configuration from multiple sources and merges them
|
|
4
|
+
*/
|
|
5
|
+
export interface ConfigPluginOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Config file sources to load
|
|
8
|
+
* Supports template variables: {{name}} for app name
|
|
9
|
+
* Default: ['~/.config/{{name}}/config.json', '.{{name}}rc', '.{{name}}rc.json']
|
|
10
|
+
*/
|
|
11
|
+
sources?: string[];
|
|
12
|
+
/**
|
|
13
|
+
* Merge strategy
|
|
14
|
+
* - 'deep': Recursively merge objects (default)
|
|
15
|
+
* - 'shallow': Only merge top-level properties
|
|
16
|
+
*/
|
|
17
|
+
mergeStrategy?: "shallow" | "deep";
|
|
18
|
+
/**
|
|
19
|
+
* Whether to stop on first found config
|
|
20
|
+
* Default: false (loads and merges all found configs)
|
|
21
|
+
*/
|
|
22
|
+
stopOnFirst?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Config merger plugin factory
|
|
26
|
+
*/
|
|
27
|
+
export declare const configMergerPlugin: any;
|
|
28
|
+
export default configMergerPlugin;
|
package/dist/mod.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createPlugin } from "@reliverse/rempts-core/plugin";
|
|
5
|
+
import { deepMerge } from "@reliverse/rempts-core/utils";
|
|
6
|
+
export const configMergerPlugin = createPlugin((options = {}) => {
|
|
7
|
+
const sources = options.sources || [
|
|
8
|
+
"~/.config/{{name}}/config.json",
|
|
9
|
+
".{{name}}rc",
|
|
10
|
+
".{{name}}rc.json",
|
|
11
|
+
".config/{{name}}.json"
|
|
12
|
+
];
|
|
13
|
+
return () => ({
|
|
14
|
+
async setup(context) {
|
|
15
|
+
const appName = context.config.name || "rempts";
|
|
16
|
+
const configs = [];
|
|
17
|
+
for (const source of sources) {
|
|
18
|
+
let path = source.replace(/^~/, homedir()).replace(/\{\{name\}\}/g, appName);
|
|
19
|
+
if (!(path.startsWith("/") || path.startsWith(homedir()))) {
|
|
20
|
+
path = join(context.paths.cwd, path);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
await access(path);
|
|
24
|
+
const content = await readFile(path, "utf-8");
|
|
25
|
+
let config;
|
|
26
|
+
try {
|
|
27
|
+
config = JSON.parse(content);
|
|
28
|
+
} catch (parseError) {
|
|
29
|
+
context.logger.warn(`Failed to parse config file ${path}: ${parseError}`);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
configs.push(config);
|
|
33
|
+
context.logger.debug(`Loaded config from ${path}`);
|
|
34
|
+
if (options.stopOnFirst) {
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
context.logger.debug(`Config file not found: ${path}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (configs.length > 0) {
|
|
42
|
+
let merged;
|
|
43
|
+
if (options.mergeStrategy === "shallow") {
|
|
44
|
+
merged = Object.assign({}, ...configs);
|
|
45
|
+
} else {
|
|
46
|
+
merged = deepMerge(...configs);
|
|
47
|
+
}
|
|
48
|
+
context.updateConfig(merged);
|
|
49
|
+
context.logger.info(`Merged ${configs.length} config file(s)`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
export default configMergerPlugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reliverse/rempts-plugin-config",
|
|
3
|
+
"version": "2.3.1",
|
|
4
|
+
"description": "Config merger plugin for Rempts - loads config from multiple sources",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"config",
|
|
8
|
+
"plugin",
|
|
9
|
+
"rempts"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "bun dler Team",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "./src/mod.ts",
|
|
19
|
+
"types": "./src/mod.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/mod.d.ts",
|
|
23
|
+
"default": "./dist/mod.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@reliverse/rempts-core": "2.3.1",
|
|
28
|
+
"@reliverse/rempts-utils": "2.3.1"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/mod.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config merger plugin for Rempts
|
|
3
|
+
* Loads configuration from multiple sources and merges them
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { access, readFile } from "node:fs/promises";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { createPlugin } from "@reliverse/rempts-core/plugin";
|
|
10
|
+
import { deepMerge } from "@reliverse/rempts-core/utils";
|
|
11
|
+
|
|
12
|
+
export interface ConfigPluginOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Config file sources to load
|
|
15
|
+
* Supports template variables: {{name}} for app name
|
|
16
|
+
* Default: ['~/.config/{{name}}/config.json', '.{{name}}rc', '.{{name}}rc.json']
|
|
17
|
+
*/
|
|
18
|
+
sources?: string[];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Merge strategy
|
|
22
|
+
* - 'deep': Recursively merge objects (default)
|
|
23
|
+
* - 'shallow': Only merge top-level properties
|
|
24
|
+
*/
|
|
25
|
+
mergeStrategy?: "shallow" | "deep";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether to stop on first found config
|
|
29
|
+
* Default: false (loads and merges all found configs)
|
|
30
|
+
*/
|
|
31
|
+
stopOnFirst?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Config merger plugin factory
|
|
36
|
+
*/
|
|
37
|
+
export const configMergerPlugin = createPlugin<ConfigPluginOptions, {}>((options = {}) => {
|
|
38
|
+
const sources = options.sources || [
|
|
39
|
+
"~/.config/{{name}}/config.json",
|
|
40
|
+
".{{name}}rc",
|
|
41
|
+
".{{name}}rc.json",
|
|
42
|
+
".config/{{name}}.json",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
return () => ({
|
|
46
|
+
async setup(context) {
|
|
47
|
+
const appName = context.config.name || "rempts";
|
|
48
|
+
const configs: any[] = [];
|
|
49
|
+
|
|
50
|
+
for (const source of sources) {
|
|
51
|
+
// Resolve template variables and home directory
|
|
52
|
+
let path = source.replace(/^~/, homedir()).replace(/\{\{name\}\}/g, appName);
|
|
53
|
+
|
|
54
|
+
// Resolve relative paths from context cwd
|
|
55
|
+
if (!(path.startsWith("/") || path.startsWith(homedir()))) {
|
|
56
|
+
path = join(context.paths.cwd, path);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// Check if file exists
|
|
61
|
+
await access(path);
|
|
62
|
+
|
|
63
|
+
// Read and parse config
|
|
64
|
+
const content = await readFile(path, "utf-8");
|
|
65
|
+
let config: any;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
config = JSON.parse(content);
|
|
69
|
+
} catch (parseError) {
|
|
70
|
+
context.logger.warn(`Failed to parse config file ${path}: ${parseError}`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
configs.push(config);
|
|
75
|
+
context.logger.debug(`Loaded config from ${path}`);
|
|
76
|
+
|
|
77
|
+
// Stop if requested
|
|
78
|
+
if (options.stopOnFirst) {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// File doesn't exist, skip silently
|
|
83
|
+
context.logger.debug(`Config file not found: ${path}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (configs.length > 0) {
|
|
88
|
+
// Merge all found configs
|
|
89
|
+
let merged: any;
|
|
90
|
+
|
|
91
|
+
if (options.mergeStrategy === "shallow") {
|
|
92
|
+
merged = Object.assign({}, ...configs);
|
|
93
|
+
} else {
|
|
94
|
+
// Deep merge is already available
|
|
95
|
+
merged = deepMerge(...configs);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
context.updateConfig(merged);
|
|
99
|
+
context.logger.info(`Merged ${configs.length} config file(s)`);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Default export for convenience
|
|
106
|
+
export default configMergerPlugin;
|