@james-pre/config 1.1.0 → 1.2.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 +12 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +79 -0
- package/dist/defaults.d.ts +15 -0
- package/dist/defaults.js +129 -0
- package/dist/manager.js +4 -70
- 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
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Remove `.default()` and `.prefault()` from a schema and everything nested within it.
|
|
4
|
+
*/
|
|
5
|
+
export declare function strip(schema: z.ZodType): z.ZodType;
|
|
6
|
+
/**
|
|
7
|
+
* Compute a schema's default value from `.default()` on it and on anything nested within it.
|
|
8
|
+
* Members without a default are left out, so the result is not necessarily a complete config.
|
|
9
|
+
*/
|
|
10
|
+
export declare function of(schema: z.ZodType): unknown;
|
|
11
|
+
/**
|
|
12
|
+
* Fill in defaults for anything missing from `value` in place, including within records, maps, arrays, and tuples.
|
|
13
|
+
*/
|
|
14
|
+
declare function _with(schema: z.ZodType, value: unknown): unknown;
|
|
15
|
+
export { _with as with };
|
package/dist/defaults.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Remove `.default()` and `.prefault()` from a schema and everything nested within it.
|
|
4
|
+
*/
|
|
5
|
+
export function strip(schema) {
|
|
6
|
+
const def = schema._zod.def;
|
|
7
|
+
switch (def.type) {
|
|
8
|
+
case 'default':
|
|
9
|
+
case 'prefault':
|
|
10
|
+
return strip(def.innerType);
|
|
11
|
+
case 'object':
|
|
12
|
+
return z.core.clone(schema, {
|
|
13
|
+
...def,
|
|
14
|
+
shape: Object.fromEntries(Object.entries(def.shape).map(([key, member]) => [key, strip(member)])),
|
|
15
|
+
catchall: def.catchall && strip(def.catchall),
|
|
16
|
+
});
|
|
17
|
+
case 'array':
|
|
18
|
+
return z.core.clone(schema, { ...def, element: strip(def.element) });
|
|
19
|
+
case 'record':
|
|
20
|
+
case 'map':
|
|
21
|
+
case 'set':
|
|
22
|
+
return z.core.clone(schema, { ...def, valueType: strip(def.valueType) });
|
|
23
|
+
case 'union':
|
|
24
|
+
return z.core.clone(schema, { ...def, options: def.options.map(strip) });
|
|
25
|
+
case 'tuple':
|
|
26
|
+
return z.core.clone(schema, {
|
|
27
|
+
...def,
|
|
28
|
+
items: def.items.map(strip),
|
|
29
|
+
rest: def.rest && strip(def.rest),
|
|
30
|
+
});
|
|
31
|
+
case 'optional':
|
|
32
|
+
case 'nullable':
|
|
33
|
+
case 'readonly':
|
|
34
|
+
case 'nonoptional':
|
|
35
|
+
case 'catch':
|
|
36
|
+
return z.core.clone(schema, { ...def, innerType: strip(def.innerType) });
|
|
37
|
+
default:
|
|
38
|
+
return schema;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Compute a schema's default value from `.default()` on it and on anything nested within it.
|
|
43
|
+
* Members without a default are left out, so the result is not necessarily a complete config.
|
|
44
|
+
*/
|
|
45
|
+
export function of(schema) {
|
|
46
|
+
const def = schema._zod.def;
|
|
47
|
+
switch (def.type) {
|
|
48
|
+
case 'default':
|
|
49
|
+
case 'prefault':
|
|
50
|
+
case 'catch':
|
|
51
|
+
try {
|
|
52
|
+
return schema.parse(undefined);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return of(def.innerType);
|
|
56
|
+
}
|
|
57
|
+
case 'object': {
|
|
58
|
+
const value = {};
|
|
59
|
+
for (const [key, member] of Object.entries(def.shape)) {
|
|
60
|
+
const inner = of(member);
|
|
61
|
+
if (inner !== undefined)
|
|
62
|
+
value[key] = inner;
|
|
63
|
+
}
|
|
64
|
+
return Object.keys(value).length ? value : undefined;
|
|
65
|
+
}
|
|
66
|
+
default:
|
|
67
|
+
return def.innerType ? of(def.innerType) : undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Fill in defaults for anything missing from `value` in place, including within records, maps, arrays, and tuples.
|
|
72
|
+
*/
|
|
73
|
+
function _with(schema, value) {
|
|
74
|
+
const def = schema._zod.def;
|
|
75
|
+
switch (def.type) {
|
|
76
|
+
case 'default':
|
|
77
|
+
case 'prefault':
|
|
78
|
+
case 'catch':
|
|
79
|
+
return value === undefined ? of(schema) : _with(def.innerType, value);
|
|
80
|
+
case 'optional':
|
|
81
|
+
case 'nullable':
|
|
82
|
+
case 'readonly':
|
|
83
|
+
case 'nonoptional':
|
|
84
|
+
return value == null ? value : _with(def.innerType, value);
|
|
85
|
+
case 'object': {
|
|
86
|
+
if (typeof value != 'object' || value === null)
|
|
87
|
+
return value;
|
|
88
|
+
const object = value;
|
|
89
|
+
for (const [key, member] of Object.entries(def.shape)) {
|
|
90
|
+
const filled = _with(member, object[key]);
|
|
91
|
+
if (filled !== undefined)
|
|
92
|
+
object[key] = filled;
|
|
93
|
+
}
|
|
94
|
+
return object;
|
|
95
|
+
}
|
|
96
|
+
case 'record': {
|
|
97
|
+
if (typeof value != 'object' || value === null)
|
|
98
|
+
return value;
|
|
99
|
+
const record = value;
|
|
100
|
+
for (const key of Object.keys(record))
|
|
101
|
+
record[key] = _with(def.valueType, record[key]);
|
|
102
|
+
return record;
|
|
103
|
+
}
|
|
104
|
+
case 'map':
|
|
105
|
+
if (!(value instanceof Map))
|
|
106
|
+
return value;
|
|
107
|
+
for (const [key, entry] of value)
|
|
108
|
+
value.set(key, _with(def.valueType, entry));
|
|
109
|
+
return value;
|
|
110
|
+
case 'array':
|
|
111
|
+
if (!Array.isArray(value))
|
|
112
|
+
return value;
|
|
113
|
+
for (let i = 0; i < value.length; i++)
|
|
114
|
+
value[i] = _with(def.element, value[i]);
|
|
115
|
+
return value;
|
|
116
|
+
case 'tuple':
|
|
117
|
+
if (!Array.isArray(value))
|
|
118
|
+
return value;
|
|
119
|
+
for (let i = 0; i < value.length; i++) {
|
|
120
|
+
const item = i < def.items.length ? def.items[i] : def.rest;
|
|
121
|
+
if (item)
|
|
122
|
+
value[i] = _with(item, value[i]);
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
125
|
+
default:
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export { _with as with };
|
package/dist/manager.js
CHANGED
|
@@ -39,6 +39,7 @@ import { homedir } from 'node:os';
|
|
|
39
39
|
import { dirname, join, resolve, sep } from 'node:path';
|
|
40
40
|
import { deepAssign, getByString, memoize, setByString } from 'utilium';
|
|
41
41
|
import * as z from 'zod';
|
|
42
|
+
import * as defaults from './defaults.js';
|
|
42
43
|
const kInit = Symbol.for('LoadOptions:init');
|
|
43
44
|
const include = z.string().array().optional();
|
|
44
45
|
function canInclude(options, file) {
|
|
@@ -63,74 +64,6 @@ function typeFromPath(path) {
|
|
|
63
64
|
function withExtension(path) {
|
|
64
65
|
return path.endsWith('.json') ? path : path + '.json';
|
|
65
66
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Remove `.default()` and `.prefault()` from a schema and everything nested within it.
|
|
68
|
-
*/
|
|
69
|
-
function stripDefaults(schema) {
|
|
70
|
-
const def = schema._zod.def;
|
|
71
|
-
switch (def.type) {
|
|
72
|
-
case 'default':
|
|
73
|
-
case 'prefault':
|
|
74
|
-
return stripDefaults(def.innerType);
|
|
75
|
-
case 'object':
|
|
76
|
-
return z.core.clone(schema, {
|
|
77
|
-
...def,
|
|
78
|
-
shape: Object.fromEntries(Object.entries(def.shape).map(([key, member]) => [key, stripDefaults(member)])),
|
|
79
|
-
catchall: def.catchall && stripDefaults(def.catchall),
|
|
80
|
-
});
|
|
81
|
-
case 'array':
|
|
82
|
-
return z.core.clone(schema, { ...def, element: stripDefaults(def.element) });
|
|
83
|
-
case 'record':
|
|
84
|
-
case 'map':
|
|
85
|
-
case 'set':
|
|
86
|
-
return z.core.clone(schema, { ...def, valueType: stripDefaults(def.valueType) });
|
|
87
|
-
case 'union':
|
|
88
|
-
return z.core.clone(schema, { ...def, options: def.options.map(stripDefaults) });
|
|
89
|
-
case 'tuple':
|
|
90
|
-
return z.core.clone(schema, {
|
|
91
|
-
...def,
|
|
92
|
-
items: def.items.map(stripDefaults),
|
|
93
|
-
rest: def.rest && stripDefaults(def.rest),
|
|
94
|
-
});
|
|
95
|
-
case 'optional':
|
|
96
|
-
case 'nullable':
|
|
97
|
-
case 'readonly':
|
|
98
|
-
case 'nonoptional':
|
|
99
|
-
case 'catch':
|
|
100
|
-
return z.core.clone(schema, { ...def, innerType: stripDefaults(def.innerType) });
|
|
101
|
-
default:
|
|
102
|
-
return schema;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Compute a schema's default value from `.default()` on it and on anything nested within it.
|
|
107
|
-
* Members without a default are left out, so the result is not necessarily a complete config.
|
|
108
|
-
*/
|
|
109
|
-
function defaultsOf(schema) {
|
|
110
|
-
const def = schema._zod.def;
|
|
111
|
-
switch (def.type) {
|
|
112
|
-
case 'default':
|
|
113
|
-
case 'prefault':
|
|
114
|
-
case 'catch':
|
|
115
|
-
try {
|
|
116
|
-
return schema.parse(undefined);
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
return defaultsOf(def.innerType);
|
|
120
|
-
}
|
|
121
|
-
case 'object': {
|
|
122
|
-
const value = {};
|
|
123
|
-
for (const [key, member] of Object.entries(def.shape)) {
|
|
124
|
-
const inner = defaultsOf(member);
|
|
125
|
-
if (inner !== undefined)
|
|
126
|
-
value[key] = inner;
|
|
127
|
-
}
|
|
128
|
-
return Object.keys(value).length ? value : undefined;
|
|
129
|
-
}
|
|
130
|
-
default:
|
|
131
|
-
return def.innerType ? defaultsOf(def.innerType) : undefined;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
67
|
/**
|
|
135
68
|
* Manager for configuration files
|
|
136
69
|
*/
|
|
@@ -163,7 +96,7 @@ let Manager = (() => {
|
|
|
163
96
|
super({ captureRejections: true });
|
|
164
97
|
this.schema = schema;
|
|
165
98
|
this.options = options;
|
|
166
|
-
this.fileSchema = z.deepPartial(
|
|
99
|
+
this.fileSchema = z.deepPartial(defaults.strip(z.object(options.enableIncludes ? { ...schema.shape, include } : schema.shape)));
|
|
167
100
|
this.data = this.schema.parse(this.defaults);
|
|
168
101
|
}
|
|
169
102
|
/**
|
|
@@ -178,7 +111,7 @@ let Manager = (() => {
|
|
|
178
111
|
* A fresh config with only the schema's defaults applied.
|
|
179
112
|
*/
|
|
180
113
|
get defaults() {
|
|
181
|
-
return structuredClone(
|
|
114
|
+
return structuredClone(defaults.of(this.schema) ?? {});
|
|
182
115
|
}
|
|
183
116
|
get(key) {
|
|
184
117
|
return getByString(this.data, key);
|
|
@@ -191,6 +124,7 @@ let Manager = (() => {
|
|
|
191
124
|
*/
|
|
192
125
|
merge(config) {
|
|
193
126
|
deepAssign(this.data, structuredClone(config), { replaceArrays: true });
|
|
127
|
+
defaults.with(this.schema, this.data);
|
|
194
128
|
this.emit('change');
|
|
195
129
|
}
|
|
196
130
|
/** Replace the current config with `config`, applying defaults for anything missing. */
|
package/package.json
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@james-pre/config",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
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
|
}
|