@james-pre/config 1.0.2 → 1.2.0
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 +12 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +79 -0
- package/dist/manager.d.ts +18 -11
- package/dist/manager.js +29 -15
- package/package.json +20 -36
package/README.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
1
|
# @james-pre/config
|
|
2
2
|
|
|
3
3
|
A small library for managing configuration files.
|
|
4
|
+
|
|
5
|
+
## CLI
|
|
6
|
+
|
|
7
|
+
`@james-pre/config/cli` adds a `config` command to a [Commander](https://github.com/tj/commander.js) program, with `dump`, `get`, `set`, `list`, and `schema`:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { configCommand } from '@james-pre/config/cli';
|
|
11
|
+
|
|
12
|
+
configCommand(program, configManager);
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Commander is an optional peer dependency, needed only for the CLI.
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import * as z from 'zod';
|
|
3
|
+
import type { FileType, Manager } from './manager.js';
|
|
4
|
+
export interface ConfigCommandOptions {
|
|
5
|
+
/** @default 'config' */
|
|
6
|
+
name?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Keys whose values `--redact` hides, wherever they are.
|
|
9
|
+
* @default ['password', 'secret']
|
|
10
|
+
*/
|
|
11
|
+
sensitive?: readonly string[];
|
|
12
|
+
/** The kind of file `set` writes to without `--type`, otherwise the most recently loaded one. */
|
|
13
|
+
defaultType?: FileType;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Add a command for reading and changing a manager's configuration to `parent`.
|
|
17
|
+
* @returns the new command, for adding more subcommands
|
|
18
|
+
*/
|
|
19
|
+
export declare function configCommand<C extends Command>(parent: C, manager: Manager<z.ZodObject>, options?: ConfigCommandOptions): Command;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { getByString, setByString } from 'utilium';
|
|
2
|
+
import * as z from 'zod';
|
|
3
|
+
const fileTypes = ['system', 'user', 'local'];
|
|
4
|
+
function redact(value, keys) {
|
|
5
|
+
if (Array.isArray(value))
|
|
6
|
+
return value.map(item => redact(item, keys));
|
|
7
|
+
if (typeof value != 'object' || value === null)
|
|
8
|
+
return value;
|
|
9
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, keys.includes(key) ? '[redacted]' : redact(child, keys)]));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Add a command for reading and changing a manager's configuration to `parent`.
|
|
13
|
+
* @returns the new command, for adding more subcommands
|
|
14
|
+
*/
|
|
15
|
+
export function configCommand(parent, manager, options = {}) {
|
|
16
|
+
const { sensitive = ['password', 'secret'] } = options;
|
|
17
|
+
const command = parent
|
|
18
|
+
.command(options.name ?? 'config')
|
|
19
|
+
.description('Manage the configuration')
|
|
20
|
+
.option('-j, --json', 'read and write values as JSON', false)
|
|
21
|
+
.option('-r, --redact', 'hide sensitive values', false);
|
|
22
|
+
function output(value) {
|
|
23
|
+
const { json, redact: hide } = command.opts();
|
|
24
|
+
if (hide)
|
|
25
|
+
value = redact(value, sensitive);
|
|
26
|
+
console.log(json ? JSON.stringify(value, null, 4) : value);
|
|
27
|
+
}
|
|
28
|
+
command
|
|
29
|
+
.command('dump')
|
|
30
|
+
.description('Output the entire current configuration')
|
|
31
|
+
.action(() => output(manager.data));
|
|
32
|
+
command
|
|
33
|
+
.command('get')
|
|
34
|
+
.description('Get a config value')
|
|
35
|
+
.argument('<key>', 'the key to get')
|
|
36
|
+
.action(key => output(getByString(manager.data, key)));
|
|
37
|
+
command
|
|
38
|
+
.command('set')
|
|
39
|
+
.description('Set a config value, which must be JSON for anything but a string')
|
|
40
|
+
.argument('<key>', 'the key to set')
|
|
41
|
+
.argument('<value>', 'the value')
|
|
42
|
+
.addOption(command.createOption('-t, --type <type>', 'the kind of file to write to').choices(fileTypes))
|
|
43
|
+
.addOption(command.createOption('-g, --global', 'write to the system file, like --type system').conflicts('type'))
|
|
44
|
+
.action(function (key, value, opts) {
|
|
45
|
+
let parsed = value;
|
|
46
|
+
if (command.opts().json) {
|
|
47
|
+
try {
|
|
48
|
+
parsed = JSON.parse(value);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
this.error('error: value is not valid JSON');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const update = {};
|
|
55
|
+
setByString(update, key, parsed);
|
|
56
|
+
try {
|
|
57
|
+
manager.update(update, opts.global ? 'system' : (opts.type ?? options.defaultType));
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (!(error instanceof z.ZodError))
|
|
61
|
+
throw error;
|
|
62
|
+
this.error('error: invalid value\n' + z.prettifyError(error));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
command
|
|
66
|
+
.command('list')
|
|
67
|
+
.alias('ls')
|
|
68
|
+
.alias('files')
|
|
69
|
+
.description('List loaded config files')
|
|
70
|
+
.action(() => {
|
|
71
|
+
for (const path of manager.filePaths)
|
|
72
|
+
console.log(path);
|
|
73
|
+
});
|
|
74
|
+
command
|
|
75
|
+
.command('schema')
|
|
76
|
+
.description('Get the JSON schema for config files')
|
|
77
|
+
.action(() => output(z.toJSONSchema(manager.fileSchema, { io: 'input' })));
|
|
78
|
+
return command;
|
|
79
|
+
}
|
package/dist/manager.d.ts
CHANGED
|
@@ -58,7 +58,7 @@ export interface ManagerOptions {
|
|
|
58
58
|
/**
|
|
59
59
|
* Manager for configuration files
|
|
60
60
|
*/
|
|
61
|
-
export declare class Manager<
|
|
61
|
+
export declare class Manager<Schema extends z.ZodObject, LoadOpts extends LoadOptions = LoadOptions, out In extends z.input<Schema> = z.input<Schema>, out FileData = z.output<ReturnType<typeof z.deepPartial<Schema>>>> extends EventEmitter<{
|
|
62
62
|
load: [path: string, config: FileData, options: Partial<LoadOpts>];
|
|
63
63
|
post_load: [path: string, config: FileData, options: Partial<LoadOpts>];
|
|
64
64
|
load_error: [path: string, stage: 'read' | 'create' | 'parse', error: Error];
|
|
@@ -67,19 +67,26 @@ export declare class Manager<LoadOpts extends LoadOptions = LoadOptions, Shape e
|
|
|
67
67
|
write: [path: string, data: FileData];
|
|
68
68
|
reload: [];
|
|
69
69
|
}> {
|
|
70
|
+
readonly schema: Schema;
|
|
70
71
|
protected options: ManagerOptions;
|
|
71
|
-
readonly
|
|
72
|
-
readonly
|
|
73
|
-
protected readonly files: Map<string, File<FileData>>;
|
|
72
|
+
readonly fileSchema: ReturnType<typeof z.deepPartial<Schema>>;
|
|
73
|
+
protected readonly _files: Map<string, File<FileData, LoadOpts>>;
|
|
74
74
|
get filePaths(): MapIterator<string>;
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
get files(): IteratorObject<Readonly<File<FileData, LoadOpts>>>;
|
|
76
|
+
configAt(path: string): Readonly<FileData> | undefined;
|
|
77
|
+
readonly data: z.output<Schema>;
|
|
78
|
+
constructor(schema: Schema, options?: ManagerOptions);
|
|
79
|
+
/**
|
|
80
|
+
* Narrow the type of this manager's load options
|
|
81
|
+
*/
|
|
82
|
+
$loadOptions<O extends object>(): Manager<Schema, LoadOpts & O, In, FileData>;
|
|
83
|
+
$assertLoadOptions<O extends object>(): asserts this is Manager<Schema, LoadOpts & O, In, FileData>;
|
|
77
84
|
/**
|
|
78
85
|
* A fresh config with only the schema's defaults applied.
|
|
79
86
|
*/
|
|
80
|
-
get defaults(): z.output<
|
|
81
|
-
get<const K extends string | number = FlattenKeys<z.output<
|
|
82
|
-
set<const K extends string | number = FlattenKeys<z.output<
|
|
87
|
+
get defaults(): z.output<Schema> & In;
|
|
88
|
+
get<const K extends string | number = FlattenKeys<z.output<Schema>>>(key: K): GetByString<z.output<Schema>, K>;
|
|
89
|
+
set<const K extends string | number = FlattenKeys<z.output<Schema>>, V = GetByString<z.output<Schema>, K>>(key: K, value: V): void;
|
|
83
90
|
/**
|
|
84
91
|
* Deeply merge `config` into the current config. Arrays are replaced rather than combined.
|
|
85
92
|
*/
|
|
@@ -88,8 +95,8 @@ export declare class Manager<LoadOpts extends LoadOptions = LoadOptions, Shape e
|
|
|
88
95
|
replace(config: In): void;
|
|
89
96
|
loadFile(path: string, options: Partial<LoadOpts>): void;
|
|
90
97
|
/** Get the entry for `path`, adding one for a file that has not been loaded */
|
|
91
|
-
protected fileAt(path: string): File<FileData>;
|
|
92
|
-
protected writeFile(file: File<FileData>): void;
|
|
98
|
+
protected fileAt(path: string): File<FileData, LoadOpts>;
|
|
99
|
+
protected writeFile(file: File<FileData, LoadOpts>): void;
|
|
93
100
|
/**
|
|
94
101
|
* Replace the contents of the config file at `path` and write it,
|
|
95
102
|
* then rebuild the current config from the defaults and all loaded files.
|
package/dist/manager.js
CHANGED
|
@@ -145,21 +145,35 @@ let Manager = (() => {
|
|
|
145
145
|
__esDecorate(this, null, _get_defaultPaths_decorators, { kind: "getter", name: "defaultPaths", static: false, private: false, access: { has: obj => "defaultPaths" in obj, get: obj => obj.defaultPaths }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
146
146
|
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
147
147
|
}
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
schema = __runInitializers(this, _instanceExtraInitializers);
|
|
149
|
+
options;
|
|
150
150
|
fileSchema;
|
|
151
|
-
|
|
151
|
+
_files = new Map();
|
|
152
152
|
get filePaths() {
|
|
153
|
-
return this.
|
|
153
|
+
return this._files.keys();
|
|
154
|
+
}
|
|
155
|
+
get files() {
|
|
156
|
+
return this._files.values().map(f => structuredClone(f));
|
|
157
|
+
}
|
|
158
|
+
configAt(path) {
|
|
159
|
+
return structuredClone(this._files.get(path)?.data);
|
|
154
160
|
}
|
|
155
161
|
data;
|
|
156
|
-
constructor(
|
|
162
|
+
constructor(schema, options = {}) {
|
|
157
163
|
super({ captureRejections: true });
|
|
164
|
+
this.schema = schema;
|
|
158
165
|
this.options = options;
|
|
159
|
-
this.
|
|
160
|
-
this.fileSchema = z.deepPartial(stripDefaults(z.object(options.enableIncludes ? { ...shape, include } : shape)));
|
|
166
|
+
this.fileSchema = z.deepPartial(stripDefaults(z.object(options.enableIncludes ? { ...schema.shape, include } : schema.shape)));
|
|
161
167
|
this.data = this.schema.parse(this.defaults);
|
|
162
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Narrow the type of this manager's load options
|
|
171
|
+
*/
|
|
172
|
+
$loadOptions() {
|
|
173
|
+
this.$assertLoadOptions();
|
|
174
|
+
return this;
|
|
175
|
+
}
|
|
176
|
+
$assertLoadOptions() { }
|
|
163
177
|
/**
|
|
164
178
|
* A fresh config with only the schema's defaults applied.
|
|
165
179
|
*/
|
|
@@ -187,7 +201,7 @@ let Manager = (() => {
|
|
|
187
201
|
this.emit('change');
|
|
188
202
|
}
|
|
189
203
|
loadFile(path, options) {
|
|
190
|
-
if (this.
|
|
204
|
+
if (this._files.has(path))
|
|
191
205
|
return;
|
|
192
206
|
let json;
|
|
193
207
|
try {
|
|
@@ -223,7 +237,7 @@ let Manager = (() => {
|
|
|
223
237
|
this.emit('load_error', path, 'parse', e);
|
|
224
238
|
file = json;
|
|
225
239
|
}
|
|
226
|
-
this.
|
|
240
|
+
this._files.set(path, {
|
|
227
241
|
auto: false,
|
|
228
242
|
wasIncluded: false,
|
|
229
243
|
type: typeFromPath(path),
|
|
@@ -247,7 +261,7 @@ let Manager = (() => {
|
|
|
247
261
|
}
|
|
248
262
|
/** Get the entry for `path`, adding one for a file that has not been loaded */
|
|
249
263
|
fileAt(path) {
|
|
250
|
-
const existing = this.
|
|
264
|
+
const existing = this._files.get(path);
|
|
251
265
|
if (existing)
|
|
252
266
|
return existing;
|
|
253
267
|
const file = {
|
|
@@ -258,7 +272,7 @@ let Manager = (() => {
|
|
|
258
272
|
wasIncluded: false,
|
|
259
273
|
options: {},
|
|
260
274
|
};
|
|
261
|
-
this.
|
|
275
|
+
this._files.set(path, file);
|
|
262
276
|
return file;
|
|
263
277
|
}
|
|
264
278
|
writeFile(file) {
|
|
@@ -275,7 +289,7 @@ let Manager = (() => {
|
|
|
275
289
|
file.data = this.fileSchema.parse(config);
|
|
276
290
|
this.writeFile(file);
|
|
277
291
|
this.replace(this.defaults);
|
|
278
|
-
for (const file of this.
|
|
292
|
+
for (const file of this._files.values())
|
|
279
293
|
this.merge(file.data);
|
|
280
294
|
}
|
|
281
295
|
/**
|
|
@@ -313,7 +327,7 @@ let Manager = (() => {
|
|
|
313
327
|
* the most recently loaded file of that type, or the path `loadDefaults` would use for it.
|
|
314
328
|
*/
|
|
315
329
|
findPath(type) {
|
|
316
|
-
const loaded = this.
|
|
330
|
+
const loaded = this._files
|
|
317
331
|
.values()
|
|
318
332
|
.filter(file => (!type || file.type == type) && !file.wasIncluded)
|
|
319
333
|
.map(file => file.path)
|
|
@@ -332,11 +346,11 @@ let Manager = (() => {
|
|
|
332
346
|
this.updateFile(this.findPath(type), config);
|
|
333
347
|
}
|
|
334
348
|
reloadFiles(options = {}) {
|
|
335
|
-
const files = this.
|
|
349
|
+
const files = this._files
|
|
336
350
|
.values()
|
|
337
351
|
.filter(file => !file.wasIncluded)
|
|
338
352
|
.toArray();
|
|
339
|
-
this.
|
|
353
|
+
this._files.clear();
|
|
340
354
|
this.replace(this.defaults);
|
|
341
355
|
for (const file of files) {
|
|
342
356
|
this.loadFile(file.path, { ...options, [kInit]: { type: file.type, auto: file.auto } });
|
package/package.json
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@james-pre/config",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Manage configuration files",
|
|
5
|
+
"author": "James Prevett <jp@jamespre.dev> (https://jamespre.dev)",
|
|
6
|
+
"license": "LGPL-3.0-or-later",
|
|
5
7
|
"funding": {
|
|
6
8
|
"type": "individual",
|
|
7
9
|
"url": "https://github.com/sponsors/james-pre"
|
|
8
10
|
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/james-pre/common.git",
|
|
14
|
+
"directory": "packages/config"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/james-pre/common/tree/main/packages/config#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/james-pre/common/issues"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
9
21
|
"main": "dist/index.js",
|
|
10
22
|
"types": "dist/index.d.ts",
|
|
11
|
-
"type": "module",
|
|
12
23
|
"exports": {
|
|
13
24
|
".": "./dist/index.js",
|
|
14
25
|
"./*": "./dist/*.js"
|
|
@@ -18,51 +29,24 @@
|
|
|
18
29
|
"README.md"
|
|
19
30
|
],
|
|
20
31
|
"scripts": {
|
|
21
|
-
"
|
|
22
|
-
"format": "prettier --write .",
|
|
23
|
-
"build": "tsc -b",
|
|
24
|
-
"lint": "tsc -b --noEmit && eslint src",
|
|
25
|
-
"prepublishOnly": "npx tsc"
|
|
32
|
+
"build": "tsc"
|
|
26
33
|
},
|
|
27
|
-
"repository": {
|
|
28
|
-
"type": "git",
|
|
29
|
-
"url": "git+https://github.com/james-pre/config"
|
|
30
|
-
},
|
|
31
|
-
"author": "James Prevett <jp@jamespre.dev> (https://jamespre.dev)",
|
|
32
|
-
"license": "LGPL-3.0-or-later",
|
|
33
|
-
"bugs": {
|
|
34
|
-
"url": "https://github.com/james-pre/config/issues"
|
|
35
|
-
},
|
|
36
|
-
"homepage": "https://github.com/james-pre/config#readme",
|
|
37
34
|
"engines": {
|
|
38
35
|
"node": ">=22.0.0"
|
|
39
36
|
},
|
|
40
37
|
"peerDependencies": {
|
|
38
|
+
"commander": "^15.0.0",
|
|
41
39
|
"utilium": "^3.7.0",
|
|
42
40
|
"zod": "^4.5.0"
|
|
43
41
|
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"commander": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
},
|
|
44
47
|
"publishConfig": {
|
|
45
48
|
"provenance": true,
|
|
46
49
|
"access": "public"
|
|
47
50
|
},
|
|
48
|
-
"devDependencies": {
|
|
49
|
-
"@eslint/js": "^10.0.1",
|
|
50
|
-
"@types/node": "^24.7.0",
|
|
51
|
-
"eslint": "^10.1.0",
|
|
52
|
-
"globals": "^17.4.0",
|
|
53
|
-
"prettier": "^3.2.5",
|
|
54
|
-
"typedoc": "^0.28.18",
|
|
55
|
-
"typescript": "^6.0.2",
|
|
56
|
-
"typescript-eslint": "^8.58.0"
|
|
57
|
-
},
|
|
58
|
-
"prettier": {
|
|
59
|
-
"singleQuote": true,
|
|
60
|
-
"useTabs": true,
|
|
61
|
-
"trailingComma": "es5",
|
|
62
|
-
"tabWidth": 4,
|
|
63
|
-
"printWidth": 120,
|
|
64
|
-
"arrowParens": "avoid",
|
|
65
|
-
"experimentalOperatorPosition": "start"
|
|
66
|
-
},
|
|
67
51
|
"sideEffects": false
|
|
68
52
|
}
|