@vltpkg/cli-sdk 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.
- package/dist/commands/deprecate.js +1 -1
- package/dist/commands/login.js +12 -2
- package/dist/commands/publish.js +11 -1
- package/dist/commands/setup.d.ts +5 -0
- package/dist/commands/setup.js +30 -20
- package/dist/commands/unpublish.js +2 -2
- package/dist/config/definition.d.ts +11 -0
- package/dist/config/definition.js +30 -8
- package/dist/config/index.d.ts +23 -2
- package/dist/config/index.js +119 -41
- package/dist/config/merge-layers.d.ts +48 -0
- package/dist/config/merge-layers.js +138 -0
- package/package.json +25 -25
|
@@ -127,7 +127,7 @@ export const command = async (conf) => {
|
|
|
127
127
|
cause: asError(err),
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
|
-
if (response.statusCode
|
|
130
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
131
131
|
throw error('failed to update deprecation status', {
|
|
132
132
|
url: packageUrl,
|
|
133
133
|
response,
|
package/dist/commands/login.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RegistryClient } from '@vltpkg/registry-client';
|
|
2
|
+
import { configWriteTarget } from "../config/index.js";
|
|
2
3
|
import { commandUsage } from "../config/usage.js";
|
|
3
4
|
import { missingRegistryError } from "../require-registry.js";
|
|
4
5
|
export const usage = () => commandUsage({
|
|
@@ -10,7 +11,9 @@ export const usage = () => commandUsage({
|
|
|
10
11
|
There is no default registry, so a registry must either
|
|
11
12
|
already be configured or be provided with
|
|
12
13
|
\`--registry=<url>\`. On success the registry is written to
|
|
13
|
-
the project's \`vlt.json
|
|
14
|
+
the project's \`vlt.json\`, so that it applies to this
|
|
15
|
+
project only. Pass \`--config=user\` to configure it for
|
|
16
|
+
every project instead.`,
|
|
14
17
|
options: {
|
|
15
18
|
registry: {
|
|
16
19
|
value: '<url>',
|
|
@@ -20,6 +23,11 @@ export const usage = () => commandUsage({
|
|
|
20
23
|
value: '<name>',
|
|
21
24
|
description: 'Identity namespace used to store auth tokens.',
|
|
22
25
|
},
|
|
26
|
+
config: {
|
|
27
|
+
value: '<user | project>',
|
|
28
|
+
description: 'Which config file to write the registry to. Defaults to ' +
|
|
29
|
+
'`project`.',
|
|
30
|
+
},
|
|
23
31
|
},
|
|
24
32
|
});
|
|
25
33
|
export const command = async (conf) => {
|
|
@@ -31,5 +39,7 @@ export const command = async (conf) => {
|
|
|
31
39
|
const rc = new RegistryClient(conf.options);
|
|
32
40
|
await rc.login(registry);
|
|
33
41
|
// persist the registry so subsequent commands are configured
|
|
34
|
-
await conf.addConfigToFile('project', {
|
|
42
|
+
await conf.addConfigToFile(configWriteTarget(conf, 'project'), {
|
|
43
|
+
registry,
|
|
44
|
+
});
|
|
35
45
|
};
|
package/dist/commands/publish.js
CHANGED
|
@@ -13,6 +13,7 @@ import { createHostContextsMap } from "../query-host-contexts.js";
|
|
|
13
13
|
import { minimatch } from 'minimatch';
|
|
14
14
|
import { resolveRegistry } from "../require-registry.js";
|
|
15
15
|
import { registryErrorMessage } from "../registry-error-message.js";
|
|
16
|
+
import { stderr } from "../output.js";
|
|
16
17
|
export const needsRegistry = true;
|
|
17
18
|
export const usage = () => commandUsage({
|
|
18
19
|
command: 'publish',
|
|
@@ -264,7 +265,10 @@ const commandSingle = async (location, conf) => {
|
|
|
264
265
|
cause: asError(err),
|
|
265
266
|
});
|
|
266
267
|
}
|
|
267
|
-
|
|
268
|
+
// Any 2xx response is a successful publish. The npm registry may
|
|
269
|
+
// respond with a 202 Accepted when processing is deferred (e.g. for
|
|
270
|
+
// publish-time malware scanning).
|
|
271
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
268
272
|
let advice = '';
|
|
269
273
|
if (response.statusCode === 409) {
|
|
270
274
|
advice = `\n⚠️ ${name}@${version} already exists in the registry. Bump the version and try again.`;
|
|
@@ -278,6 +282,12 @@ const commandSingle = async (location, conf) => {
|
|
|
278
282
|
response,
|
|
279
283
|
});
|
|
280
284
|
}
|
|
285
|
+
// On deferred publishes the registry explains the delay in an
|
|
286
|
+
// npm-notice header, e.g. "Your package is being processed and may
|
|
287
|
+
// take a few minutes to become available."
|
|
288
|
+
const notice = response.getHeaderString('npm-notice');
|
|
289
|
+
if (notice)
|
|
290
|
+
stderr(`⚠️ ${notice}`);
|
|
281
291
|
}
|
|
282
292
|
await run({
|
|
283
293
|
...runOptions,
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -17,6 +17,11 @@ export type SetupResult = {
|
|
|
17
17
|
which: 'user' | 'project';
|
|
18
18
|
/** the alias -> url map that was staged into config */
|
|
19
19
|
registries: Record<string, string>;
|
|
20
|
+
/**
|
|
21
|
+
* set when the config we wrote to is outranked for registry selection by
|
|
22
|
+
* the project's `vlt.json`, so the result won't take effect here.
|
|
23
|
+
*/
|
|
24
|
+
shadowedByProject?: boolean;
|
|
20
25
|
};
|
|
21
26
|
export declare const views: {
|
|
22
27
|
readonly human: (result: SetupResult) => string;
|
package/dist/commands/setup.js
CHANGED
|
@@ -2,6 +2,8 @@ import { error } from '@vltpkg/error-cause';
|
|
|
2
2
|
import { RegistryClient } from '@vltpkg/registry-client';
|
|
3
3
|
import { defaultRegistries } from '@vltpkg/spec';
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
|
+
import { configWriteTarget } from "../config/index.js";
|
|
6
|
+
import { registrySelectionFields } from "../config/merge-layers.js";
|
|
5
7
|
import { commandUsage } from "../config/usage.js";
|
|
6
8
|
import { stdout } from "../output.js";
|
|
7
9
|
/** Where users sign up for / log into a vlt.io account. */
|
|
@@ -13,17 +15,6 @@ export const VLT_REGISTRY_BASE = 'https://registry.vlt.io';
|
|
|
13
15
|
* `npm` is the {@link https://docs.vlt.sh | default bare-spec alias}.
|
|
14
16
|
*/
|
|
15
17
|
export const accountRegistries = ['npm', 'main'];
|
|
16
|
-
/**
|
|
17
|
-
* What each account registry is, in the order {@link accountRegistries}
|
|
18
|
-
* is authenticated. Every registry has its own credentials, so the
|
|
19
|
-
* web-login flow opens the browser once per registry -- naming what is
|
|
20
|
-
* being authenticated keeps the second prompt from reading like a repeat
|
|
21
|
-
* of the first.
|
|
22
|
-
*/
|
|
23
|
-
const accountRegistryAuthPrompts = {
|
|
24
|
-
npm: `First, let's authenticate your public npm registry mirror`,
|
|
25
|
-
main: `Now let's authenticate your private registry`,
|
|
26
|
-
};
|
|
27
18
|
/** Build the per-account registry URL for a given registry name. */
|
|
28
19
|
export const accountRegistryURL = (account, name) => `${VLT_REGISTRY_BASE}/${encodeURIComponent(account)}/${name}/`;
|
|
29
20
|
/** Ensure a registry URL ends with a single trailing slash. */
|
|
@@ -38,6 +29,9 @@ export const views = {
|
|
|
38
29
|
for (const [name, url] of Object.entries(result.registries)) {
|
|
39
30
|
lines.push(` ${name} -> ${url}`);
|
|
40
31
|
}
|
|
32
|
+
if (result.shadowedByProject) {
|
|
33
|
+
lines.push('', `Note: this project's vlt.json configures its own registries, and`, 'project config takes precedence. Re-run with `--config=project`', 'to configure this project instead.');
|
|
34
|
+
}
|
|
41
35
|
lines.push('', 'You can now install packages, e.g.:', ' vlt install <pkg>');
|
|
42
36
|
return lines.join('\n');
|
|
43
37
|
},
|
|
@@ -71,7 +65,7 @@ export const usage = () => commandUsage({
|
|
|
71
65
|
});
|
|
72
66
|
export const command = async (conf) => {
|
|
73
67
|
const yes = !!conf.get('yes');
|
|
74
|
-
const which = conf
|
|
68
|
+
const which = configWriteTarget(conf, 'user');
|
|
75
69
|
let rl;
|
|
76
70
|
const ask = async (question) => {
|
|
77
71
|
rl ??= createInterface(process.stdin, process.stdout);
|
|
@@ -96,11 +90,19 @@ export const command = async (conf) => {
|
|
|
96
90
|
for (const name of accountRegistries) {
|
|
97
91
|
registries[name] = accountRegistryURL(account, name);
|
|
98
92
|
}
|
|
99
|
-
// 3. merge
|
|
93
|
+
// 3. merge in registry aliases supplied for this invocation. aliases
|
|
94
|
+
// that came from a config file are left where they are: copying them
|
|
95
|
+
// would turn a project-local alias into a global one (or vice versa).
|
|
100
96
|
const builtinRegistries = defaultRegistries;
|
|
97
|
+
const fileRegistries = {
|
|
98
|
+
...conf.layers.user?.registries,
|
|
99
|
+
...conf.layers.project?.registries,
|
|
100
|
+
};
|
|
101
101
|
for (const [name, url] of Object.entries(conf.options.registries)) {
|
|
102
102
|
if (builtinRegistries[name] === url)
|
|
103
103
|
continue;
|
|
104
|
+
if (fileRegistries[name] === url)
|
|
105
|
+
continue;
|
|
104
106
|
registries[name] = normalizeRegistryURL(url);
|
|
105
107
|
}
|
|
106
108
|
const rc = new RegistryClient(conf.options);
|
|
@@ -108,12 +110,10 @@ export const command = async (conf) => {
|
|
|
108
110
|
if (!yes) {
|
|
109
111
|
const doAuth = await ask('Authenticate with your vlt.io account now? (Y/n) ');
|
|
110
112
|
if (!/^n/i.test(doAuth)) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
await rc.login(accountRegistryURL(account, name));
|
|
116
|
-
}
|
|
113
|
+
// one token covers every registry on the account, so the browser
|
|
114
|
+
// opens once and the token is stored for all of them.
|
|
115
|
+
stdout(`Authenticating your account registries (${accountRegistries.join(', ')})...`);
|
|
116
|
+
await rc.login(accountRegistries.map(name => accountRegistryURL(account, name)));
|
|
117
117
|
}
|
|
118
118
|
// 5. offer to add further custom aliases
|
|
119
119
|
for (;;) {
|
|
@@ -144,7 +144,17 @@ export const command = async (conf) => {
|
|
|
144
144
|
}
|
|
145
145
|
// 6. persist the staged registries (merged, not clobbered)
|
|
146
146
|
await conf.addConfigToFile(which, { registries });
|
|
147
|
-
|
|
147
|
+
// writing the user config from inside a project that configures its own
|
|
148
|
+
// registries has no effect here, so say so rather than looking like it
|
|
149
|
+
// worked.
|
|
150
|
+
const shadowedByProject = which === 'user' &&
|
|
151
|
+
registrySelectionFields.some(f => f in (conf.layers.project ?? {}));
|
|
152
|
+
return {
|
|
153
|
+
account,
|
|
154
|
+
which,
|
|
155
|
+
registries,
|
|
156
|
+
...(shadowedByProject ? { shadowedByProject } : undefined),
|
|
157
|
+
};
|
|
148
158
|
}
|
|
149
159
|
finally {
|
|
150
160
|
if (rl) {
|
|
@@ -141,7 +141,7 @@ export const command = async (conf) => {
|
|
|
141
141
|
cause: asError(err),
|
|
142
142
|
});
|
|
143
143
|
}
|
|
144
|
-
if (response.statusCode
|
|
144
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
145
145
|
throw error('Failed to unpublish package version', {
|
|
146
146
|
url: putUrl,
|
|
147
147
|
response,
|
|
@@ -188,7 +188,7 @@ export const command = async (conf) => {
|
|
|
188
188
|
cause: asError(err),
|
|
189
189
|
});
|
|
190
190
|
}
|
|
191
|
-
if (response.statusCode
|
|
191
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
192
192
|
throw error('Failed to unpublish package', {
|
|
193
193
|
url: deleteUrl,
|
|
194
194
|
response,
|
|
@@ -454,5 +454,16 @@ export declare const definition: import("jackspeak").Jack<{
|
|
|
454
454
|
help: import("jackspeak").ConfigOption<"boolean", false, undefined>;
|
|
455
455
|
all: import("jackspeak").ConfigOption<"boolean", false, undefined>;
|
|
456
456
|
}>;
|
|
457
|
+
/**
|
|
458
|
+
* The pristine `default` of every known config field, captured here at
|
|
459
|
+
* module load, before anything can call `jack.setConfigValues()`.
|
|
460
|
+
*
|
|
461
|
+
* Config layers are applied by overwriting jackspeak's defaults, and
|
|
462
|
+
* `setConfigValues()` can only set them. So when a layer stops setting a
|
|
463
|
+
* field (or removes it with `null`), the definitional default has to be
|
|
464
|
+
* put back explicitly, otherwise a value left behind by a previous apply
|
|
465
|
+
* would leak through.
|
|
466
|
+
*/
|
|
467
|
+
export declare const defaultValues: Record<string, unknown>;
|
|
457
468
|
export declare const getSortedCliOptions: () => string[];
|
|
458
469
|
export declare const getSortedKeys: () => string[];
|
|
@@ -132,14 +132,22 @@ export const definition = j
|
|
|
132
132
|
* Definition of all configuration values used by vlt.
|
|
133
133
|
*/
|
|
134
134
|
.heading('Configuration')
|
|
135
|
-
.description(`
|
|
136
|
-
then
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
135
|
+
.description(`The \`vlt.json\` file in the XDG specified config directory is loaded
|
|
136
|
+
first, and then the \`vlt.json\` file in the root of the current
|
|
137
|
+
project, if present, is layered on top of it.
|
|
138
|
+
|
|
139
|
+
Scalar values are overridden by the innermost layer that sets them.
|
|
140
|
+
Object type values are merged together key by key. Set a field, or a
|
|
141
|
+
key within an object field, to \`null\` in the JSON configuration to
|
|
142
|
+
explicitly remove it.
|
|
143
|
+
|
|
144
|
+
Registry *selection* is an exception, since \`--registry\` outranks any
|
|
145
|
+
\`--registries\` alias. A layer that sets any of \`registry\`,
|
|
146
|
+
\`registries\`, or \`default-registry-alias\` owns selection: the
|
|
147
|
+
\`registry\` and \`default-registry-alias\` it does *not* set are
|
|
148
|
+
dropped, rather than inherited from an outer layer. So a project that
|
|
149
|
+
configures its own registry is never sent to the user-level one, while
|
|
150
|
+
the aliases in the user config stay addressable by name.
|
|
143
151
|
|
|
144
152
|
Command-specific fields may be set in a nested \`command\` object that
|
|
145
153
|
overrides any options defined at the top level.
|
|
@@ -750,6 +758,20 @@ export const definition = j
|
|
|
750
758
|
description: 'Show all commands, bins, and flags',
|
|
751
759
|
},
|
|
752
760
|
});
|
|
761
|
+
/**
|
|
762
|
+
* The pristine `default` of every known config field, captured here at
|
|
763
|
+
* module load, before anything can call `jack.setConfigValues()`.
|
|
764
|
+
*
|
|
765
|
+
* Config layers are applied by overwriting jackspeak's defaults, and
|
|
766
|
+
* `setConfigValues()` can only set them. So when a layer stops setting a
|
|
767
|
+
* field (or removes it with `null`), the definitional default has to be
|
|
768
|
+
* put back explicitly, otherwise a value left behind by a previous apply
|
|
769
|
+
* would leak through.
|
|
770
|
+
*/
|
|
771
|
+
export const defaultValues = Object.fromEntries(Object.entries(definition.toJSON()).map(([field, def]) => [
|
|
772
|
+
field,
|
|
773
|
+
def.default,
|
|
774
|
+
]));
|
|
753
775
|
export const getSortedCliOptions = () => {
|
|
754
776
|
const defs = definition.toJSON();
|
|
755
777
|
return getSortedKeys().map((k) => {
|
package/dist/config/index.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Module that handles all vlt configuration needs
|
|
3
3
|
*
|
|
4
4
|
* Project-level configs are set in a `vlt.json` file in the local project
|
|
5
|
-
* if present
|
|
6
|
-
* XDG config path.
|
|
5
|
+
* if present, and are layered over the user-level configs in the appropriate
|
|
6
|
+
* XDG config path. See `./merge-layers.ts` for the layering rules.
|
|
7
7
|
*
|
|
8
8
|
* Command-specific configuration can be specified by putting options in a
|
|
9
9
|
* field in the `command` object. For example:
|
|
@@ -38,6 +38,11 @@ export type PairsAsRecords = ConfigOptionsNoExtras & {
|
|
|
38
38
|
[k in keyof Commands]?: ConfigOptionsNoExtras;
|
|
39
39
|
};
|
|
40
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* One `vlt.json` file's `config` object, in record form. Every field is
|
|
43
|
+
* optional, since a config file only sets what it sets.
|
|
44
|
+
*/
|
|
45
|
+
export type ConfigFileLayer = Partial<PairsAsRecords>;
|
|
41
46
|
export declare const pairsToRecords: (obj: NonNullable<ConfigFileData> | OptionsResults<ConfigDefinitions>) => PairsAsRecords;
|
|
42
47
|
export declare const recordsToPairs: (obj: RecordPairs) => RecordPairs;
|
|
43
48
|
export type ConfigDataNoCommand = {
|
|
@@ -169,6 +174,14 @@ export declare class Config {
|
|
|
169
174
|
* in the config file.
|
|
170
175
|
*/
|
|
171
176
|
addConfigToFile(this: LoadedConfig, which: WhichConfig, values: NonNullable<ConfigFileData>): Promise<void>;
|
|
177
|
+
/**
|
|
178
|
+
* The raw config data from each `vlt.json` file, normalized to record
|
|
179
|
+
* form, before the layers are merged together. `undefined` for a layer
|
|
180
|
+
* with no file, or a file with no `config` field.
|
|
181
|
+
*/
|
|
182
|
+
get layers(): Readonly<{
|
|
183
|
+
[k in WhichConfig]?: ConfigFileLayer;
|
|
184
|
+
}>;
|
|
172
185
|
/**
|
|
173
186
|
* Deletes the specified config fields from the named file
|
|
174
187
|
* Returns `true` if anything was changed.
|
|
@@ -216,3 +229,11 @@ export type ParsedConfig = Config & {
|
|
|
216
229
|
* A fully loaded {@link Config} object
|
|
217
230
|
*/
|
|
218
231
|
export type LoadedConfig = ParsedConfig;
|
|
232
|
+
/**
|
|
233
|
+
* Which `vlt.json` a command that persists settings should write to.
|
|
234
|
+
*
|
|
235
|
+
* `--config=user` and `--config=project` are explicit. Anything else means
|
|
236
|
+
* the caller's own default, since `--config` also takes `all` (its default,
|
|
237
|
+
* meaningful only for `vlt config` reads).
|
|
238
|
+
*/
|
|
239
|
+
export declare const configWriteTarget: (conf: Pick<LoadedConfig, "get">, fallback: WhichConfig) => WhichConfig;
|
package/dist/config/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Module that handles all vlt configuration needs
|
|
3
3
|
*
|
|
4
4
|
* Project-level configs are set in a `vlt.json` file in the local project
|
|
5
|
-
* if present
|
|
6
|
-
* XDG config path.
|
|
5
|
+
* if present, and are layered over the user-level configs in the appropriate
|
|
6
|
+
* XDG config path. See `./merge-layers.ts` for the layering rules.
|
|
7
7
|
*
|
|
8
8
|
* Command-specific configuration can be specified by putting options in a
|
|
9
9
|
* field in the `command` object. For example:
|
|
@@ -31,8 +31,9 @@ import { Monorepo } from '@vltpkg/workspaces';
|
|
|
31
31
|
import { readFile, rm, writeFile } from 'node:fs/promises';
|
|
32
32
|
import { dirname } from 'node:path';
|
|
33
33
|
import { PathScurry } from 'path-scurry';
|
|
34
|
-
import { commands, definition, getCommand, isRecordField, recordFields, } from "./definition.js";
|
|
34
|
+
import { commands, definition, defaultValues, getCommand, isRecordField, recordFields, } from "./definition.js";
|
|
35
35
|
import { merge } from "./merge.js";
|
|
36
|
+
import { cloneLayer, mergeLayers } from "./merge-layers.js";
|
|
36
37
|
export { commands, definition, isRecordField, recordFields, };
|
|
37
38
|
export const kCustomInspect = Symbol.for('nodejs.util.inspect.custom');
|
|
38
39
|
// turn a set of pairs into a Record object.
|
|
@@ -53,6 +54,25 @@ const reducePairs = (pairs) => {
|
|
|
53
54
|
};
|
|
54
55
|
const isRecordFieldValue = (k, v) => Array.isArray(v) &&
|
|
55
56
|
recordFields.includes(k);
|
|
57
|
+
/**
|
|
58
|
+
* `registries` alias names are used as spec prefixes, where `~` is
|
|
59
|
+
* reserved, and an empty name has nothing to prefix with.
|
|
60
|
+
*/
|
|
61
|
+
const assertRegistryKeys = (registries, file) => {
|
|
62
|
+
if (!registries || typeof registries !== 'object')
|
|
63
|
+
return;
|
|
64
|
+
const obj = Array.isArray(registries) ?
|
|
65
|
+
reducePairs(registries)
|
|
66
|
+
: registries;
|
|
67
|
+
for (const key of Object.keys(obj)) {
|
|
68
|
+
if (key === '' || key.includes('~')) {
|
|
69
|
+
throw error('Reserved character found in registries name', {
|
|
70
|
+
path: file,
|
|
71
|
+
found: key,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
56
76
|
export const pairsToRecords = (obj) => {
|
|
57
77
|
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [
|
|
58
78
|
k,
|
|
@@ -75,7 +95,12 @@ export const recordsToPairs = (obj) => {
|
|
|
75
95
|
.map(([k, v]) => [
|
|
76
96
|
k,
|
|
77
97
|
k === 'command' && v && typeof v === 'object' ?
|
|
78
|
-
|
|
98
|
+
// each command's own block gets converted too, mirroring
|
|
99
|
+
// pairsToRecords
|
|
100
|
+
Object.fromEntries(Object.entries(v).map(([k, v]) => [
|
|
101
|
+
k,
|
|
102
|
+
recordsToPairs(v),
|
|
103
|
+
]))
|
|
79
104
|
: (!v ||
|
|
80
105
|
typeof v !== 'object' ||
|
|
81
106
|
Array.isArray(v) ||
|
|
@@ -277,7 +302,10 @@ export class Config {
|
|
|
277
302
|
* in the config file.
|
|
278
303
|
*/
|
|
279
304
|
async addConfigToFile(which, values) {
|
|
280
|
-
|
|
305
|
+
// normalize both sides to record form first, so that a record field
|
|
306
|
+
// written as a `key=value` list in an existing file is still merged key
|
|
307
|
+
// by key, rather than replaced wholesale.
|
|
308
|
+
return this.writeConfigFile(which, merge(pairsToRecords((await this.#maybeLoadConfigFile(which)) ?? {}), pairsToRecords(values)));
|
|
281
309
|
}
|
|
282
310
|
// called in this weird bound way so that it can be used by the
|
|
283
311
|
// vlt-json config loading module.
|
|
@@ -292,53 +320,92 @@ export class Config {
|
|
|
292
320
|
wanted: 'ConfigFileData',
|
|
293
321
|
});
|
|
294
322
|
}
|
|
295
|
-
const { command, ...values } =
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
: registries;
|
|
302
|
-
for (const key of Object.keys(registriesObj)) {
|
|
303
|
-
if (key === '' || key.includes('~')) {
|
|
304
|
-
throw error('Reserved character found in registries name', {
|
|
305
|
-
path: file,
|
|
306
|
-
found: key,
|
|
307
|
-
});
|
|
323
|
+
const { command, ...values } = c;
|
|
324
|
+
assertRegistryKeys(values.registries, file);
|
|
325
|
+
if (command && typeof command === 'object') {
|
|
326
|
+
for (const opts of Object.values(command)) {
|
|
327
|
+
if (opts && typeof opts === 'object') {
|
|
328
|
+
assertRegistryKeys(opts.registries, file);
|
|
308
329
|
}
|
|
309
330
|
}
|
|
310
331
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
+
// validate this layer on its own, so that an invalid value is blamed on
|
|
333
|
+
// the file it actually came from. `mergeLayers` on a single layer just
|
|
334
|
+
// strips the `null`s, which mean "remove this field" rather than being
|
|
335
|
+
// a value jack should ever see.
|
|
336
|
+
const layer = cloneLayer(pairsToRecords(c));
|
|
337
|
+
const { command: _cmd, ...single } = mergeLayers([layer]);
|
|
338
|
+
try {
|
|
339
|
+
this.jack.validate(recordsToPairs(single));
|
|
340
|
+
}
|
|
341
|
+
catch (er) {
|
|
342
|
+
/* c8 ignore next - for TS */
|
|
343
|
+
if (er instanceof Error) {
|
|
344
|
+
// jack only attaches the source path when going through
|
|
345
|
+
// setConfigValues(), and the CLI's "Problem in Config File" banner
|
|
346
|
+
// keys off it.
|
|
347
|
+
/* c8 ignore next - jack always throws with a cause object */
|
|
348
|
+
const cause = typeof er.cause === 'object' ? er.cause : {};
|
|
349
|
+
er.cause = { ...cause, path: file };
|
|
350
|
+
}
|
|
351
|
+
throw er;
|
|
352
|
+
}
|
|
353
|
+
this.#layers[file === find('user') ? 'user' : 'project'] = layer;
|
|
354
|
+
this.#applyLayers();
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* The raw config data from each `vlt.json` file, normalized to record
|
|
358
|
+
* form, before the layers are merged together. `undefined` for a layer
|
|
359
|
+
* with no file, or a file with no `config` field.
|
|
360
|
+
*/
|
|
361
|
+
get layers() {
|
|
362
|
+
return this.#layers;
|
|
363
|
+
}
|
|
364
|
+
#layers = {};
|
|
365
|
+
/**
|
|
366
|
+
* Recompute the jack defaults and {@link Config#commandValues} from
|
|
367
|
+
* {@link Config#layers}.
|
|
368
|
+
*
|
|
369
|
+
* Everything is derived from `#layers`, and every known field is applied,
|
|
370
|
+
* so this is idempotent: re-reading a config file (which
|
|
371
|
+
* `addConfigToFile` and `deleteConfigKeys` both do) can't stack the user
|
|
372
|
+
* layer back on top of the project layer, and a field a layer stopped
|
|
373
|
+
* setting goes back to its definitional default.
|
|
374
|
+
*/
|
|
375
|
+
#applyLayers() {
|
|
376
|
+
const { command, ...values } = mergeLayers([
|
|
377
|
+
this.#layers.user,
|
|
378
|
+
this.#layers.project,
|
|
379
|
+
]);
|
|
380
|
+
this.commandValues = {};
|
|
381
|
+
for (const [c, opts] of Object.entries(command ?? {})) {
|
|
382
|
+
const cmd = getCommand(c);
|
|
383
|
+
if (cmd && opts && typeof opts === 'object') {
|
|
384
|
+
this.commandValues[cmd] = opts;
|
|
332
385
|
}
|
|
333
386
|
}
|
|
334
|
-
|
|
387
|
+
const pairs = recordsToPairs(values);
|
|
388
|
+
const applied = {};
|
|
389
|
+
for (const [field, dflt] of Object.entries(defaultValues)) {
|
|
390
|
+
applied[field] = field in pairs ? pairs[field] : dflt;
|
|
391
|
+
}
|
|
392
|
+
this.jack.setConfigValues(applied);
|
|
393
|
+
this.#options = undefined;
|
|
335
394
|
}
|
|
336
395
|
/**
|
|
337
396
|
* if the file exists, parse and load it. returns object if data was
|
|
338
397
|
* loaded, or undefined if not.
|
|
339
398
|
*/
|
|
340
399
|
async #maybeLoadConfigFile(whichConfig) {
|
|
341
|
-
|
|
400
|
+
const data = load('config', this.#validator, whichConfig);
|
|
401
|
+
if (data === undefined &&
|
|
402
|
+
this.#layers[whichConfig] !== undefined) {
|
|
403
|
+
// the file (or its `config` field) went away, so the validator never
|
|
404
|
+
// ran. drop the layer it left behind.
|
|
405
|
+
delete this.#layers[whichConfig];
|
|
406
|
+
this.#applyLayers();
|
|
407
|
+
}
|
|
408
|
+
return data;
|
|
342
409
|
}
|
|
343
410
|
/**
|
|
344
411
|
* Deletes the specified config fields from the named file
|
|
@@ -493,3 +560,14 @@ export class Config {
|
|
|
493
560
|
}
|
|
494
561
|
}
|
|
495
562
|
const isParsed = (c) => !!(c.values && c.positionals && c.command);
|
|
563
|
+
/**
|
|
564
|
+
* Which `vlt.json` a command that persists settings should write to.
|
|
565
|
+
*
|
|
566
|
+
* `--config=user` and `--config=project` are explicit. Anything else means
|
|
567
|
+
* the caller's own default, since `--config` also takes `all` (its default,
|
|
568
|
+
* meaningful only for `vlt config` reads).
|
|
569
|
+
*/
|
|
570
|
+
export const configWriteTarget = (conf, fallback) => {
|
|
571
|
+
const which = conf.get('config');
|
|
572
|
+
return which === 'user' || which === 'project' ? which : fallback;
|
|
573
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge the `config` objects found in the user and project `vlt.json`
|
|
3
|
+
* files into the single set of values that gets applied to the
|
|
4
|
+
* {@link https://npmjs.com/jackspeak | jackspeak} definition.
|
|
5
|
+
*
|
|
6
|
+
* Layers are supplied outermost first (user, then project), and follow the
|
|
7
|
+
* rules documented in `vlt help` and at
|
|
8
|
+
* <https://docs.vlt.sh/cli/configuring>:
|
|
9
|
+
*
|
|
10
|
+
* - Scalar fields come from the innermost layer that sets them.
|
|
11
|
+
* - Object (`key=value` record) fields are merged key by key.
|
|
12
|
+
* - A `null` value removes a field, or a single key within a record field.
|
|
13
|
+
* - Registry *selection* belongs to a single layer, see
|
|
14
|
+
* {@link registrySelectionFields}.
|
|
15
|
+
*
|
|
16
|
+
* Note that this operates on the record form of a config object (the shape
|
|
17
|
+
* it has in `vlt.json`), not on jackspeak's `key=value` string lists, so
|
|
18
|
+
* record fields can be merged per key. Run values through
|
|
19
|
+
* `pairsToRecords` first.
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
/** One config layer, in record (not `key=value` pair) form. */
|
|
23
|
+
export type ConfigLayer = Record<string, unknown>;
|
|
24
|
+
/**
|
|
25
|
+
* The fields that *select* which registry is used, as opposed to the
|
|
26
|
+
* catalogs of aliases that can be selected from.
|
|
27
|
+
*/
|
|
28
|
+
export declare const registrySelectors: readonly ["registry", "default-registry-alias"];
|
|
29
|
+
/**
|
|
30
|
+
* When a layer sets any of these, that layer owns registry selection: the
|
|
31
|
+
* {@link registrySelectors} it does *not* set are removed from the merged
|
|
32
|
+
* result, rather than inherited from an outer layer.
|
|
33
|
+
*
|
|
34
|
+
* Without this, a `registry` in the user `vlt.json` would outrank a
|
|
35
|
+
* project's `registries.npm`, since the `registry` scalar beats any alias
|
|
36
|
+
* in `requireRegistry()`. The alias catalogs are still merged, so aliases
|
|
37
|
+
* configured at the user level stay addressable by name.
|
|
38
|
+
*
|
|
39
|
+
* Deliberately narrow: `scoped-registries` and `jsr-registries` are purely
|
|
40
|
+
* additive, so adding one of those in a project must not drop the user's
|
|
41
|
+
* default registry.
|
|
42
|
+
*/
|
|
43
|
+
export declare const registrySelectionFields: readonly ["registry", "registries", "default-registry-alias"];
|
|
44
|
+
export declare const cloneLayer: <T>(data: T) => T;
|
|
45
|
+
/**
|
|
46
|
+
* Merge config layers, outermost first. Layers are never mutated.
|
|
47
|
+
*/
|
|
48
|
+
export declare const mergeLayers: (layers: (ConfigLayer | undefined)[]) => ConfigLayer;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge the `config` objects found in the user and project `vlt.json`
|
|
3
|
+
* files into the single set of values that gets applied to the
|
|
4
|
+
* {@link https://npmjs.com/jackspeak | jackspeak} definition.
|
|
5
|
+
*
|
|
6
|
+
* Layers are supplied outermost first (user, then project), and follow the
|
|
7
|
+
* rules documented in `vlt help` and at
|
|
8
|
+
* <https://docs.vlt.sh/cli/configuring>:
|
|
9
|
+
*
|
|
10
|
+
* - Scalar fields come from the innermost layer that sets them.
|
|
11
|
+
* - Object (`key=value` record) fields are merged key by key.
|
|
12
|
+
* - A `null` value removes a field, or a single key within a record field.
|
|
13
|
+
* - Registry *selection* belongs to a single layer, see
|
|
14
|
+
* {@link registrySelectionFields}.
|
|
15
|
+
*
|
|
16
|
+
* Note that this operates on the record form of a config object (the shape
|
|
17
|
+
* it has in `vlt.json`), not on jackspeak's `key=value` string lists, so
|
|
18
|
+
* record fields can be merged per key. Run values through
|
|
19
|
+
* `pairsToRecords` first.
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import { isRecordField } from "./definition.js";
|
|
23
|
+
/**
|
|
24
|
+
* The fields that *select* which registry is used, as opposed to the
|
|
25
|
+
* catalogs of aliases that can be selected from.
|
|
26
|
+
*/
|
|
27
|
+
export const registrySelectors = [
|
|
28
|
+
'registry',
|
|
29
|
+
'default-registry-alias',
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* When a layer sets any of these, that layer owns registry selection: the
|
|
33
|
+
* {@link registrySelectors} it does *not* set are removed from the merged
|
|
34
|
+
* result, rather than inherited from an outer layer.
|
|
35
|
+
*
|
|
36
|
+
* Without this, a `registry` in the user `vlt.json` would outrank a
|
|
37
|
+
* project's `registries.npm`, since the `registry` scalar beats any alias
|
|
38
|
+
* in `requireRegistry()`. The alias catalogs are still merged, so aliases
|
|
39
|
+
* configured at the user level stay addressable by name.
|
|
40
|
+
*
|
|
41
|
+
* Deliberately narrow: `scoped-registries` and `jsr-registries` are purely
|
|
42
|
+
* additive, so adding one of those in a project must not drop the user's
|
|
43
|
+
* default registry.
|
|
44
|
+
*/
|
|
45
|
+
export const registrySelectionFields = [
|
|
46
|
+
'registry',
|
|
47
|
+
'registries',
|
|
48
|
+
'default-registry-alias',
|
|
49
|
+
];
|
|
50
|
+
const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
51
|
+
/**
|
|
52
|
+
* Deep copy of a config layer, preserving `null` values.
|
|
53
|
+
*
|
|
54
|
+
* Layers are held onto for the life of the `Config` object, but the objects
|
|
55
|
+
* nested in them are shared with the `@vltpkg/vlt-json` cache, which
|
|
56
|
+
* `deleteConfigKeys` mutates in place. Copying keeps a stored layer an
|
|
57
|
+
* accurate snapshot of what was read.
|
|
58
|
+
*/
|
|
59
|
+
const cloneValue = (v) => Array.isArray(v) ? v.map(x => cloneValue(x))
|
|
60
|
+
: isObj(v) ?
|
|
61
|
+
Object.fromEntries(Object.entries(v).map(([k, x]) => [k, cloneValue(x)]))
|
|
62
|
+
: v;
|
|
63
|
+
export const cloneLayer = (data) => cloneValue(data);
|
|
64
|
+
/**
|
|
65
|
+
* Fold one record field's keys into the value from the outer layers.
|
|
66
|
+
* `null` removes a key. Returns `undefined` when nothing is left, so that
|
|
67
|
+
* the field falls back to its definitional default instead of applying an
|
|
68
|
+
* empty record.
|
|
69
|
+
*/
|
|
70
|
+
const mergeRecord = (base, add) => {
|
|
71
|
+
const result = isObj(base) ? { ...base } : {};
|
|
72
|
+
for (const [k, v] of Object.entries(add)) {
|
|
73
|
+
if (v === null)
|
|
74
|
+
delete result[k];
|
|
75
|
+
else
|
|
76
|
+
result[k] = v;
|
|
77
|
+
}
|
|
78
|
+
return Object.keys(result).length ? result : undefined;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Fold the `command` object in, merging each command's block with the same
|
|
82
|
+
* rules as the top level.
|
|
83
|
+
*/
|
|
84
|
+
const mergeCommands = (base, add) => {
|
|
85
|
+
const result = isObj(base) ? { ...base } : {};
|
|
86
|
+
for (const [cmd, opts] of Object.entries(add)) {
|
|
87
|
+
if (opts === null)
|
|
88
|
+
delete result[cmd];
|
|
89
|
+
else if (isObj(opts)) {
|
|
90
|
+
result[cmd] = mergeLayers([
|
|
91
|
+
isObj(result[cmd]) ? result[cmd] : undefined,
|
|
92
|
+
opts,
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return Object.keys(result).length ? result : undefined;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Merge config layers, outermost first. Layers are never mutated.
|
|
100
|
+
*/
|
|
101
|
+
export const mergeLayers = (layers) => {
|
|
102
|
+
const result = {};
|
|
103
|
+
for (const layer of layers) {
|
|
104
|
+
if (!isObj(layer))
|
|
105
|
+
continue;
|
|
106
|
+
for (const [k, v] of Object.entries(layer)) {
|
|
107
|
+
if (k === 'command') {
|
|
108
|
+
const merged = isObj(v) ? mergeCommands(result.command, v) : undefined;
|
|
109
|
+
if (merged)
|
|
110
|
+
result.command = merged;
|
|
111
|
+
else
|
|
112
|
+
delete result.command;
|
|
113
|
+
}
|
|
114
|
+
else if (v === null) {
|
|
115
|
+
delete result[k];
|
|
116
|
+
}
|
|
117
|
+
else if (isRecordField(k) && isObj(v)) {
|
|
118
|
+
const merged = mergeRecord(result[k], v);
|
|
119
|
+
if (merged)
|
|
120
|
+
result[k] = merged;
|
|
121
|
+
else
|
|
122
|
+
delete result[k];
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
result[k] = v;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// the innermost layer that configures a registry at all owns which
|
|
129
|
+
// registry is selected.
|
|
130
|
+
if (registrySelectionFields.some(f => f in layer)) {
|
|
131
|
+
for (const f of registrySelectors) {
|
|
132
|
+
if (!(f in layer))
|
|
133
|
+
delete result[f];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vltpkg/cli-sdk",
|
|
3
3
|
"description": "The source for the vlt CLI",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.7",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/vltpkg/vltpkg.git",
|
|
@@ -14,30 +14,30 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@resvg/resvg-wasm": "^2.6.2",
|
|
17
|
-
"@vltpkg/config": "1.0.
|
|
18
|
-
"@vltpkg/dep-id": "1.0.
|
|
19
|
-
"@vltpkg/dot-prop": "1.0.
|
|
20
|
-
"@vltpkg/error-cause": "1.0.
|
|
21
|
-
"@vltpkg/git": "1.0.
|
|
22
|
-
"@vltpkg/graph": "1.0.
|
|
23
|
-
"@vltpkg/graph-run": "1.0.
|
|
24
|
-
"@vltpkg/init": "1.0.
|
|
25
|
-
"@vltpkg/output": "1.0.
|
|
26
|
-
"@vltpkg/package-info": "1.0.
|
|
27
|
-
"@vltpkg/package-json": "1.0.
|
|
28
|
-
"@vltpkg/promise-spawn": "1.0.
|
|
29
|
-
"@vltpkg/query": "1.0.
|
|
30
|
-
"@vltpkg/registry-client": "1.0.
|
|
31
|
-
"@vltpkg/rollback-remove": "1.0.
|
|
32
|
-
"@vltpkg/run": "1.0.
|
|
33
|
-
"@vltpkg/security-archive": "1.0.
|
|
34
|
-
"@vltpkg/spec": "1.0.
|
|
35
|
-
"@vltpkg/types": "1.0.
|
|
36
|
-
"@vltpkg/url-open": "1.0.
|
|
37
|
-
"@vltpkg/vlt-json": "1.0.
|
|
38
|
-
"@vltpkg/vlx": "1.0.
|
|
39
|
-
"@vltpkg/workspaces": "1.0.
|
|
40
|
-
"@vltpkg/xdg": "1.0.
|
|
17
|
+
"@vltpkg/config": "1.0.7",
|
|
18
|
+
"@vltpkg/dep-id": "1.0.7",
|
|
19
|
+
"@vltpkg/dot-prop": "1.0.7",
|
|
20
|
+
"@vltpkg/error-cause": "1.0.7",
|
|
21
|
+
"@vltpkg/git": "1.0.7",
|
|
22
|
+
"@vltpkg/graph": "1.0.7",
|
|
23
|
+
"@vltpkg/graph-run": "1.0.7",
|
|
24
|
+
"@vltpkg/init": "1.0.7",
|
|
25
|
+
"@vltpkg/output": "1.0.7",
|
|
26
|
+
"@vltpkg/package-info": "1.0.7",
|
|
27
|
+
"@vltpkg/package-json": "1.0.7",
|
|
28
|
+
"@vltpkg/promise-spawn": "1.0.7",
|
|
29
|
+
"@vltpkg/query": "1.0.7",
|
|
30
|
+
"@vltpkg/registry-client": "1.0.7",
|
|
31
|
+
"@vltpkg/rollback-remove": "1.0.7",
|
|
32
|
+
"@vltpkg/run": "1.0.7",
|
|
33
|
+
"@vltpkg/security-archive": "1.0.7",
|
|
34
|
+
"@vltpkg/spec": "1.0.7",
|
|
35
|
+
"@vltpkg/types": "1.0.7",
|
|
36
|
+
"@vltpkg/url-open": "1.0.7",
|
|
37
|
+
"@vltpkg/vlt-json": "1.0.7",
|
|
38
|
+
"@vltpkg/vlx": "1.0.7",
|
|
39
|
+
"@vltpkg/workspaces": "1.0.7",
|
|
40
|
+
"@vltpkg/xdg": "1.0.7",
|
|
41
41
|
"ansi-to-pre": "^1.0.6",
|
|
42
42
|
"beautiful-mermaid": "^1.1.3",
|
|
43
43
|
"chalk": "^5.6.2",
|