@flowscripter/dynamic-cli-framework 12.0.1 → 12.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/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/src/cli/BaseCLI.d.ts.map +1 -1
- package/dist/src/cli/BaseCLI.js +3 -1
- package/dist/src/cli/BaseCLIFeatureOptions.d.ts +6 -0
- package/dist/src/cli/BaseCLIFeatureOptions.d.ts.map +1 -1
- package/dist/src/runtime/runner.d.ts.map +1 -1
- package/dist/src/runtime/runner.js +4 -1
- package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts +33 -15
- package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts.map +1 -1
- package/dist/src/service/configuration/ConfigurationServiceProvider.js +80 -173
- package/dist/src/service/configuration/DefaultConfigurationService.d.ts +78 -0
- package/dist/src/service/configuration/DefaultConfigurationService.d.ts.map +1 -0
- package/dist/src/service/configuration/DefaultConfigurationService.js +296 -0
- package/dist/src/service/configuration/DefaultKeyValueService.d.ts.map +1 -1
- package/dist/src/service/configuration/DefaultKeyValueService.js +7 -3
- package/dist/src/service/configuration/DefaultSecretService.d.ts +17 -2
- package/dist/src/service/configuration/DefaultSecretService.d.ts.map +1 -1
- package/dist/src/service/configuration/DefaultSecretService.js +44 -5
- package/dist/src/service/configuration/KeyValueServiceProvider.d.ts +3 -1
- package/dist/src/service/configuration/KeyValueServiceProvider.d.ts.map +1 -1
- package/dist/src/service/configuration/KeyValueServiceProvider.js +6 -3
- package/dist/src/service/configuration/command/ConfigDumpSubCommand.d.ts +3 -7
- package/dist/src/service/configuration/command/ConfigDumpSubCommand.d.ts.map +1 -1
- package/dist/src/service/configuration/command/ConfigDumpSubCommand.js +5 -10
- package/dist/src/service/configuration/command/ConfigGetSubCommand.d.ts +16 -0
- package/dist/src/service/configuration/command/ConfigGetSubCommand.d.ts.map +1 -0
- package/dist/src/service/configuration/command/ConfigGetSubCommand.js +137 -0
- package/dist/src/service/configuration/command/ConfigSetSubCommand.d.ts +18 -0
- package/dist/src/service/configuration/command/ConfigSetSubCommand.d.ts.map +1 -0
- package/dist/src/service/configuration/command/ConfigSetSubCommand.js +289 -0
- package/dist/src/service/configuration/command/ConfigUnsetSubCommand.d.ts +15 -0
- package/dist/src/service/configuration/command/ConfigUnsetSubCommand.d.ts.map +1 -0
- package/dist/src/service/configuration/command/ConfigUnsetSubCommand.js +101 -0
- package/dist/src/service/configuration/configTargets.d.ts +91 -0
- package/dist/src/service/configuration/configTargets.d.ts.map +1 -0
- package/dist/src/service/configuration/configTargets.js +348 -0
- package/dist/src/service/configuration/format/ConfigFormat.d.ts +42 -0
- package/dist/src/service/configuration/format/ConfigFormat.d.ts.map +1 -0
- package/dist/src/service/configuration/format/ConfigFormat.js +0 -0
- package/dist/src/service/configuration/format/JsonConfigFormat.d.ts +7 -0
- package/dist/src/service/configuration/format/JsonConfigFormat.d.ts.map +1 -0
- package/dist/src/service/configuration/format/JsonConfigFormat.js +17 -0
- package/dist/src/service/configuration/format/TomlConfigFormat.d.ts +10 -0
- package/dist/src/service/configuration/format/TomlConfigFormat.d.ts.map +1 -0
- package/dist/src/service/configuration/format/TomlConfigFormat.js +78 -0
- package/dist/src/service/configuration/format/YamlConfigFormat.d.ts +9 -0
- package/dist/src/service/configuration/format/YamlConfigFormat.d.ts.map +1 -0
- package/dist/src/service/configuration/format/YamlConfigFormat.js +63 -0
- package/dist/src/service/configuration/resolveSecrets.d.ts.map +1 -1
- package/dist/src/service/configuration/resolveSecrets.js +7 -1
- package/dist/toml.d.ts +2 -0
- package/dist/toml.d.ts.map +1 -0
- package/dist/toml.js +1 -0
- package/dist/yaml.d.ts +2 -0
- package/dist/yaml.d.ts.map +1 -0
- package/dist/yaml.js +1 -0
- package/index.ts +3 -0
- package/package.json +27 -3
- package/src/cli/BaseCLI.ts +4 -0
- package/src/cli/BaseCLIFeatureOptions.ts +6 -0
- package/src/runtime/runner.ts +4 -1
- package/src/service/configuration/ConfigurationServiceProvider.ts +108 -214
- package/src/service/configuration/DefaultConfigurationService.ts +378 -0
- package/src/service/configuration/DefaultKeyValueService.ts +9 -3
- package/src/service/configuration/DefaultSecretService.ts +51 -8
- package/src/service/configuration/KeyValueServiceProvider.ts +7 -2
- package/src/service/configuration/command/ConfigDumpSubCommand.ts +6 -10
- package/src/service/configuration/command/ConfigGetSubCommand.ts +198 -0
- package/src/service/configuration/command/ConfigSetSubCommand.ts +401 -0
- package/src/service/configuration/command/ConfigUnsetSubCommand.ts +126 -0
- package/src/service/configuration/configTargets.ts +481 -0
- package/src/service/configuration/format/ConfigFormat.ts +42 -0
- package/src/service/configuration/format/JsonConfigFormat.ts +22 -0
- package/src/service/configuration/format/TomlConfigFormat.ts +92 -0
- package/src/service/configuration/format/YamlConfigFormat.ts +72 -0
- package/src/service/configuration/resolveSecrets.ts +7 -1
- package/toml.ts +1 -0
- package/yaml.ts +1 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PRINTER_SERVICE_ID,
|
|
3
|
+
ValueTypeName,
|
|
4
|
+
type Context,
|
|
5
|
+
type Option,
|
|
6
|
+
type ComplexOption,
|
|
7
|
+
type Positional,
|
|
8
|
+
type PrinterService,
|
|
9
|
+
type SubCommand,
|
|
10
|
+
type Values,
|
|
11
|
+
} from "@flowscripter/dynamic-cli-framework-api";
|
|
12
|
+
import type DefaultConfigurationService from "../DefaultConfigurationService.ts";
|
|
13
|
+
import { isComplexOption } from "../../../runtime/argument/ArgumentTypeGuards.ts";
|
|
14
|
+
import {
|
|
15
|
+
isOverriddenByEnvVar,
|
|
16
|
+
maskDefault,
|
|
17
|
+
type ConfigTarget,
|
|
18
|
+
type PathStep,
|
|
19
|
+
} from "../configTargets.ts";
|
|
20
|
+
|
|
21
|
+
function typeName(option: { type: unknown; isArray?: boolean }): string {
|
|
22
|
+
const name = isComplexOptionLike(option) ? "COMPLEX" : ValueTypeName[option.type as number];
|
|
23
|
+
return option.isArray === true ? `${name}[]` : name!;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isComplexOptionLike(option: { type: unknown }): boolean {
|
|
27
|
+
return option.type === 5;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function constraints(option: Option): string {
|
|
31
|
+
const parts: Array<string> = [];
|
|
32
|
+
if (option.allowableValues !== undefined) {
|
|
33
|
+
parts.push(`one of: ${option.allowableValues.join(", ")}`);
|
|
34
|
+
}
|
|
35
|
+
if (option.minValueInclusive !== undefined || option.maxValueInclusive !== undefined) {
|
|
36
|
+
parts.push(`range: ${option.minValueInclusive ?? ""}..${option.maxValueInclusive ?? ""}`);
|
|
37
|
+
}
|
|
38
|
+
return parts.length === 0 ? "" : ` (${parts.join("; ")})`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function mismatch(option: Option | ComplexOption, value: unknown): boolean {
|
|
42
|
+
if (value === undefined || isComplexOption(option) || option.isArray === true) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
switch (option.type) {
|
|
46
|
+
case ValueTypeName.BOOLEAN:
|
|
47
|
+
return typeof value !== "boolean";
|
|
48
|
+
case ValueTypeName.INTEGER:
|
|
49
|
+
return !Number.isInteger(value);
|
|
50
|
+
case ValueTypeName.NUMBER:
|
|
51
|
+
return typeof value !== "number";
|
|
52
|
+
default:
|
|
53
|
+
return typeof value !== "string";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Member of the `config` group which shows the configurable commands, their options and the
|
|
59
|
+
* values currently stored for them. SECRET values are masked.
|
|
60
|
+
*/
|
|
61
|
+
export default class ConfigGetSubCommand implements SubCommand {
|
|
62
|
+
readonly name = "get";
|
|
63
|
+
readonly description = "Show configurable commands, their options and stored default values";
|
|
64
|
+
readonly options: ReadonlyArray<Option> = [
|
|
65
|
+
{
|
|
66
|
+
name: "command",
|
|
67
|
+
type: ValueTypeName.STRING,
|
|
68
|
+
description: "Show only this command (or <group>:<member>)",
|
|
69
|
+
isOptional: true,
|
|
70
|
+
validate: (value) => {
|
|
71
|
+
const targets = this.#configurationService.getConfigTargets();
|
|
72
|
+
return targets.has(value as string)
|
|
73
|
+
? undefined
|
|
74
|
+
: `unknown or non-configurable command, valid commands: ${[...targets.keys()].join(", ")}`;
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
readonly positionals: ReadonlyArray<Positional> = [];
|
|
79
|
+
|
|
80
|
+
readonly #configurationService: DefaultConfigurationService;
|
|
81
|
+
|
|
82
|
+
public constructor(configurationService: DefaultConfigurationService) {
|
|
83
|
+
this.#configurationService = configurationService;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public async execute(context: Context, argumentValues: Values): Promise<void> {
|
|
87
|
+
const printer = context.getServiceById(PRINTER_SERVICE_ID) as PrinterService;
|
|
88
|
+
const targets = this.#configurationService.getConfigTargets();
|
|
89
|
+
const selected = argumentValues["command"] as string | undefined;
|
|
90
|
+
const keys = selected === undefined ? [...targets.keys()] : [selected];
|
|
91
|
+
if (selected === undefined) {
|
|
92
|
+
for (const key of this.#configurationService.defaultsData.keys()) {
|
|
93
|
+
if (!targets.has(key)) {
|
|
94
|
+
keys.push(key);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const lines: Array<string> = [];
|
|
100
|
+
for (const key of keys) {
|
|
101
|
+
const target = targets.get(key);
|
|
102
|
+
const stored = this.#configurationService.defaultsData.get(key);
|
|
103
|
+
if (target === undefined) {
|
|
104
|
+
lines.push(`${key} (unknown command, not configurable)`);
|
|
105
|
+
lines.push(` ${JSON.stringify(maskDefault(undefined, stored))}`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
lines.push(key);
|
|
109
|
+
if (target.argument !== undefined) {
|
|
110
|
+
lines.push(
|
|
111
|
+
` ${typeName(target.argument)}${constraints(target.argument as Option)} = ${this.#show(context, target, [], stored)}`,
|
|
112
|
+
);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const values = (stored ?? {}) as Record<string, unknown>;
|
|
116
|
+
this.#listOptions(context, target, target.options, [], "", values, lines);
|
|
117
|
+
for (const name of Object.keys(values)) {
|
|
118
|
+
if (!target.options.some((o) => o.name === name)) {
|
|
119
|
+
lines.push(
|
|
120
|
+
` ${name} (unknown option) = ${JSON.stringify(maskDefault(undefined, values[name]))}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
await printer.print(`${lines.join("\n")}\n`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#show(
|
|
129
|
+
context: Context,
|
|
130
|
+
target: ConfigTarget,
|
|
131
|
+
steps: ReadonlyArray<PathStep>,
|
|
132
|
+
value: unknown,
|
|
133
|
+
): string {
|
|
134
|
+
if (value === undefined) {
|
|
135
|
+
return isOverriddenByEnvVar(this.#configurationService, context.cliConfig, target, steps)
|
|
136
|
+
? "(not set; overridden by an environment variable)"
|
|
137
|
+
: "(not set)";
|
|
138
|
+
}
|
|
139
|
+
const text = JSON.stringify(maskDefault(target, value));
|
|
140
|
+
return isOverriddenByEnvVar(this.#configurationService, context.cliConfig, target, steps)
|
|
141
|
+
? `${text} (overridden by an environment variable)`
|
|
142
|
+
: text;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#listOptions(
|
|
146
|
+
context: Context,
|
|
147
|
+
target: ConfigTarget,
|
|
148
|
+
options: ReadonlyArray<Option | ComplexOption>,
|
|
149
|
+
steps: ReadonlyArray<PathStep>,
|
|
150
|
+
prefix: string,
|
|
151
|
+
values: Record<string, unknown> | undefined,
|
|
152
|
+
lines: Array<string>,
|
|
153
|
+
): void {
|
|
154
|
+
for (const option of options) {
|
|
155
|
+
const path = `${prefix}${option.name}`;
|
|
156
|
+
const optionSteps = [...steps, { name: option.name }];
|
|
157
|
+
const value = values?.[option.name];
|
|
158
|
+
const flag = mismatch(option, value) ? " (type mismatch)" : "";
|
|
159
|
+
const indent = " ".repeat(optionSteps.length);
|
|
160
|
+
const optional =
|
|
161
|
+
option.isOptional === true || option.defaultValue !== undefined ? "" : " required";
|
|
162
|
+
const detail = isComplexOption(option) ? "" : constraints(option);
|
|
163
|
+
const shown =
|
|
164
|
+
value === undefined
|
|
165
|
+
? this.#show(context, target, optionSteps, undefined)
|
|
166
|
+
: this.#showValue(context, target, optionSteps, option, value);
|
|
167
|
+
lines.push(`${indent}${path} ${typeName(option)}${optional}${detail} = ${shown}${flag}`);
|
|
168
|
+
if (isComplexOption(option) && option.isArray !== true) {
|
|
169
|
+
this.#listOptions(
|
|
170
|
+
context,
|
|
171
|
+
target,
|
|
172
|
+
option.properties,
|
|
173
|
+
optionSteps,
|
|
174
|
+
`${path}.`,
|
|
175
|
+
typeof value === "object" && value !== null
|
|
176
|
+
? (value as Record<string, unknown>)
|
|
177
|
+
: undefined,
|
|
178
|
+
lines,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#showValue(
|
|
185
|
+
context: Context,
|
|
186
|
+
target: ConfigTarget,
|
|
187
|
+
steps: ReadonlyArray<PathStep>,
|
|
188
|
+
option: Option | ComplexOption,
|
|
189
|
+
value: unknown,
|
|
190
|
+
): string {
|
|
191
|
+
const wrapper: ConfigTarget = { ...target, argument: undefined, options: [option] };
|
|
192
|
+
const masked = maskDefault(wrapper, { [option.name]: value }) as Record<string, unknown>;
|
|
193
|
+
const text = JSON.stringify(masked[option.name]);
|
|
194
|
+
return isOverriddenByEnvVar(this.#configurationService, context.cliConfig, target, steps)
|
|
195
|
+
? `${text} (overridden by an environment variable)`
|
|
196
|
+
: text;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ARGUMENT_PROMPTER_SERVICE_ID,
|
|
3
|
+
PRINTER_SERVICE_ID,
|
|
4
|
+
ValueTypeName,
|
|
5
|
+
Icon,
|
|
6
|
+
type ArgumentPrompterService,
|
|
7
|
+
type ComplexOption,
|
|
8
|
+
type Context,
|
|
9
|
+
type Option,
|
|
10
|
+
type Positional,
|
|
11
|
+
type PrinterService,
|
|
12
|
+
type SingleValueType,
|
|
13
|
+
type SubCommand,
|
|
14
|
+
type Values,
|
|
15
|
+
} from "@flowscripter/dynamic-cli-framework-api";
|
|
16
|
+
import type DefaultConfigurationService from "../DefaultConfigurationService.ts";
|
|
17
|
+
import type { DeferredCustomValidation } from "../../../runtime/values/argumentValueValidation.ts";
|
|
18
|
+
import { isComplexOption } from "../../../runtime/argument/ArgumentTypeGuards.ts";
|
|
19
|
+
import {
|
|
20
|
+
applyChanges,
|
|
21
|
+
containsSecret,
|
|
22
|
+
isOverriddenByEnvVar,
|
|
23
|
+
prepareChange,
|
|
24
|
+
prepareGlobalValue,
|
|
25
|
+
resolveOptionPath,
|
|
26
|
+
setAtPath,
|
|
27
|
+
type ConfigChange,
|
|
28
|
+
type ConfigTarget,
|
|
29
|
+
} from "../configTargets.ts";
|
|
30
|
+
|
|
31
|
+
type Raw = Record<string, unknown>;
|
|
32
|
+
|
|
33
|
+
async function readStdin(): Promise<string> {
|
|
34
|
+
return await Bun.stdin.text();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Member of the `config` group which sets a default value for an option of a configurable command.
|
|
39
|
+
*
|
|
40
|
+
* Values are validated against the option declared by the target command (type, allowable values,
|
|
41
|
+
* range and custom validators) but the target's required checks are not applied.
|
|
42
|
+
*/
|
|
43
|
+
export default class ConfigSetSubCommand implements SubCommand {
|
|
44
|
+
readonly name = "set";
|
|
45
|
+
readonly description = "Set a default value for an option of a command";
|
|
46
|
+
readonly options: ReadonlyArray<Option> = [
|
|
47
|
+
{
|
|
48
|
+
name: "command",
|
|
49
|
+
type: ValueTypeName.STRING,
|
|
50
|
+
description: "The command (or <group>:<member>) to set a default for",
|
|
51
|
+
isOptional: true,
|
|
52
|
+
validate: (value) => this.#validateCommand(value as string),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "option",
|
|
56
|
+
type: ValueTypeName.STRING,
|
|
57
|
+
description:
|
|
58
|
+
"The option to set, for nested or array options a path such as 'address[1].port'. Omit for a global command.",
|
|
59
|
+
isOptional: true,
|
|
60
|
+
validate: (value, ctx) => {
|
|
61
|
+
const target = this.#targetOf(ctx.values);
|
|
62
|
+
if (target === undefined) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
if (target.argument !== undefined) {
|
|
66
|
+
return `command '${target.key}' has no options: omit --option`;
|
|
67
|
+
}
|
|
68
|
+
const resolved = resolveOptionPath(target, value as string);
|
|
69
|
+
return typeof resolved === "string" ? this.#withValidOptions(target, resolved) : undefined;
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "value",
|
|
74
|
+
type: ValueTypeName.STRING,
|
|
75
|
+
description:
|
|
76
|
+
"The value to set. Arrays and complex options take a JSON value. Prefer a prompt or --value-stdin for SECRET options.",
|
|
77
|
+
isOptional: true,
|
|
78
|
+
validate: (value, ctx) => {
|
|
79
|
+
const target = this.#targetOf(ctx.values);
|
|
80
|
+
if (target === undefined) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const values = ctx.values as Raw;
|
|
84
|
+
const result = this.#prepare(
|
|
85
|
+
target,
|
|
86
|
+
values["option"] as string | undefined,
|
|
87
|
+
value as string,
|
|
88
|
+
);
|
|
89
|
+
return typeof result === "string" ? result : undefined;
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "value-stdin",
|
|
94
|
+
type: ValueTypeName.BOOLEAN,
|
|
95
|
+
description: "Read the value from stdin instead of --value",
|
|
96
|
+
isOptional: true,
|
|
97
|
+
validate: (value, ctx) =>
|
|
98
|
+
value === true && (ctx.values as Raw)["value"] !== undefined
|
|
99
|
+
? "cannot be combined with --value"
|
|
100
|
+
: undefined,
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
readonly positionals: ReadonlyArray<Positional> = [];
|
|
104
|
+
|
|
105
|
+
readonly #configurationService: DefaultConfigurationService;
|
|
106
|
+
readonly #readStdin: () => Promise<string>;
|
|
107
|
+
|
|
108
|
+
public constructor(
|
|
109
|
+
configurationService: DefaultConfigurationService,
|
|
110
|
+
readStdinFn: () => Promise<string> = readStdin,
|
|
111
|
+
) {
|
|
112
|
+
this.#configurationService = configurationService;
|
|
113
|
+
this.#readStdin = readStdinFn;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#validateCommand(command: string): string | undefined {
|
|
117
|
+
const targets = this.#configurationService.getConfigTargets();
|
|
118
|
+
return targets.has(command)
|
|
119
|
+
? undefined
|
|
120
|
+
: `unknown or non-configurable command, valid commands: ${[...targets.keys()].join(", ")}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#targetOf(values: unknown): ConfigTarget | undefined {
|
|
124
|
+
const command = (values as Raw)["command"];
|
|
125
|
+
return typeof command === "string"
|
|
126
|
+
? this.#configurationService.getConfigTargets().get(command)
|
|
127
|
+
: undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#withValidOptions(target: ConfigTarget, message: string): string {
|
|
131
|
+
return `${message}, valid options: ${target.options.map((o) => o.name).join(", ")}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Validate and convert a raw value and apply it to a copy of the stored defaults (running the
|
|
136
|
+
* target's custom validators).
|
|
137
|
+
*/
|
|
138
|
+
#prepare(
|
|
139
|
+
target: ConfigTarget,
|
|
140
|
+
option: string | undefined,
|
|
141
|
+
raw: string,
|
|
142
|
+
): { change?: ConfigChange; value?: SingleValueType; isSecret: boolean } | string {
|
|
143
|
+
const deferred: Array<DeferredCustomValidation> = [];
|
|
144
|
+
if (target.argument !== undefined) {
|
|
145
|
+
if (option !== undefined) {
|
|
146
|
+
return `command '${target.key}' has no options: omit --option`;
|
|
147
|
+
}
|
|
148
|
+
return prepareGlobalValue(target, raw, deferred);
|
|
149
|
+
}
|
|
150
|
+
if (option === undefined) {
|
|
151
|
+
return "--option is required with --value";
|
|
152
|
+
}
|
|
153
|
+
const change = prepareChange(target, option, raw, deferred);
|
|
154
|
+
if (typeof change === "string") {
|
|
155
|
+
return change;
|
|
156
|
+
}
|
|
157
|
+
const applied = applyChanges(
|
|
158
|
+
target,
|
|
159
|
+
this.#configurationService.defaultsData.get(target.key) as Values | undefined,
|
|
160
|
+
[change],
|
|
161
|
+
deferred,
|
|
162
|
+
);
|
|
163
|
+
return typeof applied === "string" ? applied : { change, isSecret: change.isSecret };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async #promptFor(
|
|
167
|
+
context: Context,
|
|
168
|
+
args: ReadonlyArray<Option | ComplexOption>,
|
|
169
|
+
missing: string,
|
|
170
|
+
): Promise<Values> {
|
|
171
|
+
if (context.doesServiceExist(ARGUMENT_PROMPTER_SERVICE_ID)) {
|
|
172
|
+
const prompter = context.getServiceById(
|
|
173
|
+
ARGUMENT_PROMPTER_SERVICE_ID,
|
|
174
|
+
) as ArgumentPrompterService;
|
|
175
|
+
const values = await prompter.promptForArguments(args);
|
|
176
|
+
if (values !== undefined) {
|
|
177
|
+
return values;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
throw new Error(`${missing} (prompting is not available)`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async execute(context: Context, argumentValues: Values): Promise<void> {
|
|
184
|
+
const printer = context.getServiceById(PRINTER_SERVICE_ID) as PrinterService;
|
|
185
|
+
const targets = this.#configurationService.getConfigTargets();
|
|
186
|
+
const commandNames = [...targets.keys()];
|
|
187
|
+
|
|
188
|
+
let commandKey = argumentValues["command"] as string | undefined;
|
|
189
|
+
if (commandKey === undefined) {
|
|
190
|
+
const prompted = await this.#promptFor(
|
|
191
|
+
context,
|
|
192
|
+
[
|
|
193
|
+
{
|
|
194
|
+
name: "command",
|
|
195
|
+
description: "Command to configure",
|
|
196
|
+
type: ValueTypeName.STRING,
|
|
197
|
+
allowableValues: commandNames,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
`--command is required, valid commands: ${commandNames.join(", ")}`,
|
|
201
|
+
);
|
|
202
|
+
commandKey = prompted["command"] as string;
|
|
203
|
+
}
|
|
204
|
+
const target = targets.get(commandKey);
|
|
205
|
+
if (target === undefined) {
|
|
206
|
+
throw new Error(`Unknown or non-configurable command '${commandKey}'`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const optionPath = argumentValues["option"] as string | undefined;
|
|
210
|
+
let raw = argumentValues["value"] as string | undefined;
|
|
211
|
+
if (argumentValues["value-stdin"] === true) {
|
|
212
|
+
if (context.isTty.stdin) {
|
|
213
|
+
throw new Error("--value-stdin requires the value to be piped to stdin, or use the prompt");
|
|
214
|
+
}
|
|
215
|
+
raw = (await this.#readStdin()).replace(/\r?\n$/, "");
|
|
216
|
+
if (raw === "") {
|
|
217
|
+
throw new Error("No value was provided on stdin");
|
|
218
|
+
}
|
|
219
|
+
if (target.argument === undefined && optionPath === undefined) {
|
|
220
|
+
throw new Error("--option is required with --value-stdin");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const deferred: Array<DeferredCustomValidation> = [];
|
|
225
|
+
const changes: Array<ConfigChange> = [];
|
|
226
|
+
let globalValue: SingleValueType | undefined;
|
|
227
|
+
let globalIsSecret = false;
|
|
228
|
+
|
|
229
|
+
if (target.argument !== undefined) {
|
|
230
|
+
if (optionPath !== undefined) {
|
|
231
|
+
throw new Error(`Command '${target.key}' has no options: omit --option`);
|
|
232
|
+
}
|
|
233
|
+
if (raw === undefined) {
|
|
234
|
+
const prompted = await this.#promptFor(
|
|
235
|
+
context,
|
|
236
|
+
[
|
|
237
|
+
{
|
|
238
|
+
description: `Value for ${target.key}`,
|
|
239
|
+
...target.argument,
|
|
240
|
+
name: "value",
|
|
241
|
+
isOptional: false,
|
|
242
|
+
} as Option,
|
|
243
|
+
],
|
|
244
|
+
"--value is required",
|
|
245
|
+
);
|
|
246
|
+
globalValue = prompted["value"] as SingleValueType;
|
|
247
|
+
globalIsSecret = target.argument.type === ValueTypeName.SECRET;
|
|
248
|
+
} else {
|
|
249
|
+
const result = prepareGlobalValue(target, raw, deferred);
|
|
250
|
+
if (typeof result === "string") {
|
|
251
|
+
throw new Error(result);
|
|
252
|
+
}
|
|
253
|
+
globalValue = result.value;
|
|
254
|
+
globalIsSecret = result.isSecret;
|
|
255
|
+
await this.#warnIfSecretArgument(context, printer, globalIsSecret, argumentValues);
|
|
256
|
+
}
|
|
257
|
+
} else if (optionPath === undefined && raw !== undefined) {
|
|
258
|
+
throw new Error("--option is required with --value");
|
|
259
|
+
} else if (optionPath === undefined) {
|
|
260
|
+
const mandatory = target.options.filter(
|
|
261
|
+
(o) => o.isOptional !== true && o.defaultValue === undefined,
|
|
262
|
+
);
|
|
263
|
+
if (mandatory.length > 0) {
|
|
264
|
+
const prompted = await this.#promptFor(
|
|
265
|
+
context,
|
|
266
|
+
mandatory,
|
|
267
|
+
`--value is required, mandatory options: ${mandatory.map((o) => o.name).join(", ")}`,
|
|
268
|
+
);
|
|
269
|
+
for (const option of mandatory) {
|
|
270
|
+
changes.push(this.#promptedChange(option, prompted[option.name]!));
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
const names = target.options.map((o) => o.name);
|
|
274
|
+
const selected = await this.#promptFor(
|
|
275
|
+
context,
|
|
276
|
+
[
|
|
277
|
+
{
|
|
278
|
+
name: "option",
|
|
279
|
+
description: "Option to set",
|
|
280
|
+
type: ValueTypeName.STRING,
|
|
281
|
+
allowableValues: names,
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
`--option is required, valid options: ${names.join(", ")}`,
|
|
285
|
+
);
|
|
286
|
+
const option = target.options.find((o) => o.name === selected["option"])!;
|
|
287
|
+
const prompted = await this.#promptFor(
|
|
288
|
+
context,
|
|
289
|
+
[{ ...option, isOptional: false } as Option],
|
|
290
|
+
"--value is required",
|
|
291
|
+
);
|
|
292
|
+
changes.push(this.#promptedChange(option, prompted[option.name]!));
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
const resolved = resolveOptionPath(target, optionPath);
|
|
296
|
+
if (typeof resolved === "string") {
|
|
297
|
+
throw new Error(this.#withValidOptions(target, resolved));
|
|
298
|
+
}
|
|
299
|
+
if (raw === undefined) {
|
|
300
|
+
const { leaf, steps } = resolved;
|
|
301
|
+
const hasIndex = steps[steps.length - 1]!.index !== undefined;
|
|
302
|
+
const prompted = await this.#promptFor(
|
|
303
|
+
context,
|
|
304
|
+
[{ ...leaf, isArray: hasIndex ? false : leaf.isArray, isOptional: false } as Option],
|
|
305
|
+
"--value is required",
|
|
306
|
+
);
|
|
307
|
+
changes.push({
|
|
308
|
+
...this.#promptedChange(leaf, prompted[leaf.name]!),
|
|
309
|
+
steps,
|
|
310
|
+
path: optionPath,
|
|
311
|
+
});
|
|
312
|
+
} else {
|
|
313
|
+
const change = prepareChange(target, optionPath, raw, deferred);
|
|
314
|
+
if (typeof change === "string") {
|
|
315
|
+
throw new Error(change);
|
|
316
|
+
}
|
|
317
|
+
changes.push(change);
|
|
318
|
+
await this.#warnIfSecretArgument(context, printer, change.isSecret, argumentValues);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const existing = this.#configurationService.defaultsData.get(target.key) as Values | undefined;
|
|
323
|
+
if (target.argument === undefined) {
|
|
324
|
+
const applied = applyChanges(target, existing, changes, deferred);
|
|
325
|
+
if (typeof applied === "string") {
|
|
326
|
+
throw new Error(applied);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const secretChanges = changes.filter((c) => c.isSecret);
|
|
331
|
+
const secrets =
|
|
332
|
+
target.argument !== undefined
|
|
333
|
+
? globalIsSecret
|
|
334
|
+
? [{ key: [target.key], value: globalValue as string }]
|
|
335
|
+
: []
|
|
336
|
+
: secretChanges.map((c) => ({ key: [target.key, c.path], value: c.value as string }));
|
|
337
|
+
|
|
338
|
+
await this.#configurationService.commitDefault(target.key, secrets, (sentinels) => {
|
|
339
|
+
if (target.argument !== undefined) {
|
|
340
|
+
return globalIsSecret ? sentinels[0] : globalValue;
|
|
341
|
+
}
|
|
342
|
+
const values = structuredClone(existing ?? {}) as Record<string, unknown>;
|
|
343
|
+
for (const change of changes) {
|
|
344
|
+
const secretIndex = secretChanges.indexOf(change);
|
|
345
|
+
setAtPath(values, change.steps, secretIndex >= 0 ? sentinels[secretIndex] : change.value);
|
|
346
|
+
}
|
|
347
|
+
return values as Values;
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
for (const change of target.argument !== undefined ? [] : changes) {
|
|
351
|
+
await printer.info(`Set default ${target.key}.${change.path}\n`);
|
|
352
|
+
if (
|
|
353
|
+
isOverriddenByEnvVar(this.#configurationService, context.cliConfig, target, change.steps)
|
|
354
|
+
) {
|
|
355
|
+
await printer.warn(
|
|
356
|
+
`An environment variable overrides the default for ${target.key}.${change.path}\n`,
|
|
357
|
+
Icon.ALERT,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (target.argument !== undefined) {
|
|
362
|
+
await printer.info(`Set default ${target.key}\n`);
|
|
363
|
+
if (isOverriddenByEnvVar(this.#configurationService, context.cliConfig, target, [])) {
|
|
364
|
+
await printer.warn(
|
|
365
|
+
`An environment variable overrides the default for ${target.key}\n`,
|
|
366
|
+
Icon.ALERT,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
#promptedChange(option: Option | ComplexOption, value: unknown): ConfigChange {
|
|
373
|
+
if (containsSecret(option) && (isComplexOption(option) || option.isArray === true)) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
`Option '${option.name}' contains SECRET values: set each secret property individually using --option`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
steps: [{ name: option.name }],
|
|
380
|
+
path: option.name,
|
|
381
|
+
leaf: option,
|
|
382
|
+
value,
|
|
383
|
+
isSecret: !isComplexOption(option) && option.type === ValueTypeName.SECRET,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async #warnIfSecretArgument(
|
|
388
|
+
context: Context,
|
|
389
|
+
printer: PrinterService,
|
|
390
|
+
isSecret: boolean,
|
|
391
|
+
argumentValues: Values,
|
|
392
|
+
): Promise<void> {
|
|
393
|
+
if (isSecret && argumentValues["value-stdin"] !== true && context.isTty.stdin) {
|
|
394
|
+
await printer.warn(
|
|
395
|
+
"SECRET value passed as a plain argument. This is not recommended as it may be exposed via shell history " +
|
|
396
|
+
"and process listings (and in debug logs). Use the prompt or --value-stdin instead.\n\n",
|
|
397
|
+
Icon.ALERT,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|