@guanghechen/commander 4.7.5 → 4.7.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/CHANGELOG.md +17 -0
- package/README.md +46 -39
- package/lib/cjs/browser.cjs +428 -175
- package/lib/cjs/node.cjs +427 -174
- package/lib/esm/browser.mjs +428 -175
- package/lib/esm/node.mjs +427 -174
- package/lib/schema/preset.config.schema.json +152 -0
- package/lib/schema/preset.schema.json +152 -0
- package/lib/types/browser.d.ts +43 -7
- package/lib/types/node.d.ts +43 -7
- package/package.json +1 -1
- package/lib/cjs/index.cjs +0 -1237
- package/lib/esm/index.mjs +0 -1208
- package/lib/types/index.d.ts +0 -384
package/lib/esm/node.mjs
CHANGED
|
@@ -184,11 +184,9 @@ class CommanderError extends Error {
|
|
|
184
184
|
|
|
185
185
|
const LONG_OPTION_REGEX = /^--[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
186
186
|
const NEGATIVE_OPTION_REGEX = /^--no-[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
const DEFAULT_PRESET_OPTS_FILENAME = '.opt.local';
|
|
191
|
-
const DEFAULT_PRESET_ENVS_FILENAME = '.env.local';
|
|
187
|
+
const PRESET_FILE_FLAG = '--preset-file';
|
|
188
|
+
const PRESET_PROFILE_FLAG = '--preset-profile';
|
|
189
|
+
const PRESET_SELECTOR_DELIMITER = ':';
|
|
192
190
|
function kebabToCamelCase(str) {
|
|
193
191
|
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
194
192
|
}
|
|
@@ -202,6 +200,8 @@ const DECIMAL_EXPONENT_REGEX = /^[eE][+-]?\d(?:_?\d)*$/;
|
|
|
202
200
|
const BINARY_LITERAL_REGEX = /^0[bB][01](?:_?[01])*$/;
|
|
203
201
|
const OCTAL_LITERAL_REGEX = /^0[oO][0-7](?:_?[0-7])*$/;
|
|
204
202
|
const HEX_LITERAL_REGEX = /^0[xX][0-9a-fA-F](?:_?[0-9a-fA-F])*$/;
|
|
203
|
+
const PRESET_PROFILE_NAME_REGEX = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
204
|
+
const PRESET_VARIANT_NAME_REGEX = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
205
205
|
function stripAnsi(value) {
|
|
206
206
|
return value.replace(ANSI_ESCAPE_REGEX, '');
|
|
207
207
|
}
|
|
@@ -1030,24 +1030,32 @@ class Command {
|
|
|
1030
1030
|
const separatorIndex = controlTailArgv.indexOf('--');
|
|
1031
1031
|
const beforeSeparator = separatorIndex === -1 ? controlTailArgv : controlTailArgv.slice(0, separatorIndex);
|
|
1032
1032
|
const afterSeparator = separatorIndex === -1 ? [] : controlTailArgv.slice(separatorIndex + 1);
|
|
1033
|
-
const
|
|
1034
|
-
const commandPreset = this.#resolveCommandPresetFromChain(ctx.chain);
|
|
1035
|
-
const presetRoot = await this.#resolveEffectivePresetRoot(rootScanResult.cliPresetRoots, commandPreset, commandPath);
|
|
1036
|
-
const fileScanResult = this.#scanPresetFileDirectives(rootScanResult.cleanArgv, commandPath);
|
|
1033
|
+
const profileScanResult = this.#scanPresetProfileDirectives(beforeSeparator, commandPath);
|
|
1037
1034
|
const cleanArgv = separatorIndex === -1
|
|
1038
|
-
?
|
|
1039
|
-
: [...
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1035
|
+
? profileScanResult.cleanArgv
|
|
1036
|
+
: [...profileScanResult.cleanArgv, '--', ...afterSeparator];
|
|
1037
|
+
const commandChain = ctx.chain;
|
|
1038
|
+
const commandPresetFile = this.#resolveCommandPresetFileFromChain(commandChain);
|
|
1039
|
+
const effectivePresetFile = profileScanResult.presetFile ?? commandPresetFile;
|
|
1040
|
+
const commandPresetProfile = this.#resolveCommandPresetProfileFromChain(commandChain);
|
|
1041
|
+
const useCommandPresetProfile = profileScanResult.presetProfile === undefined && commandPresetProfile !== undefined;
|
|
1042
|
+
if (useCommandPresetProfile) {
|
|
1043
|
+
this.#assertPresetProfileSelectorValue(commandPresetProfile, 'command.preset.profile', commandPath);
|
|
1044
|
+
}
|
|
1045
|
+
const effectivePresetProfile = profileScanResult.presetProfile ?? commandPresetProfile;
|
|
1046
|
+
const effectivePresetProfileSourceName = profileScanResult.presetProfile !== undefined
|
|
1047
|
+
? PRESET_PROFILE_FLAG
|
|
1048
|
+
: commandPresetProfile !== undefined
|
|
1049
|
+
? 'command.preset.profile'
|
|
1050
|
+
: undefined;
|
|
1051
|
+
if (effectivePresetFile === undefined && useCommandPresetProfile) {
|
|
1052
|
+
throw new CommanderError('ConfigurationError', 'cannot use "command.preset.profile" without "command.preset.file" or "--preset-file"', commandPath);
|
|
1053
|
+
}
|
|
1054
|
+
const resolvedProfile = await this.#resolvePresetProfile({
|
|
1055
|
+
presetFile: effectivePresetFile,
|
|
1056
|
+
presetProfile: effectivePresetProfile,
|
|
1057
|
+
presetProfileSourceName: effectivePresetProfileSourceName,
|
|
1058
|
+
commandPath,
|
|
1051
1059
|
});
|
|
1052
1060
|
const userSources = {
|
|
1053
1061
|
cmds: [...ctx.sources.user.cmds],
|
|
@@ -1055,30 +1063,29 @@ class Command {
|
|
|
1055
1063
|
envs: { ...ctx.sources.user.envs },
|
|
1056
1064
|
};
|
|
1057
1065
|
const presetArgv = [];
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
}
|
|
1063
|
-
const tokens = this.#tokenizePresetOptions(content);
|
|
1064
|
-
this.#validatePresetOptionTokens(tokens, file.displayPath, commandPath);
|
|
1065
|
-
this.#assertPresetOptionFragments(tokens, file.displayPath, ctx.chain, optionPolicyMap);
|
|
1066
|
-
presetArgv.push(...tokens);
|
|
1066
|
+
if (resolvedProfile !== undefined && resolvedProfile.optsArgv.length > 0) {
|
|
1067
|
+
this.#validatePresetOptionTokens(resolvedProfile.optsArgv, resolvedProfile.optsSourceLabel, commandPath);
|
|
1068
|
+
this.#assertPresetOptionFragments(resolvedProfile.optsArgv, resolvedProfile.optsSourceLabel, commandChain, optionPolicyMap);
|
|
1069
|
+
presetArgv.push(...resolvedProfile.optsArgv);
|
|
1067
1070
|
}
|
|
1068
1071
|
const presetEnvs = {};
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
parsed = parse(content);
|
|
1072
|
+
if (resolvedProfile !== undefined) {
|
|
1073
|
+
if (resolvedProfile.profileEnvFileSource !== undefined) {
|
|
1074
|
+
const content = await this.#readPresetFile(resolvedProfile.profileEnvFileSource, commandPath);
|
|
1075
|
+
if (content !== undefined) {
|
|
1076
|
+
const parsed = this.#parsePresetEnvsContent(content, resolvedProfile.profileEnvFileSource, commandPath);
|
|
1077
|
+
Object.assign(presetEnvs, parsed);
|
|
1078
|
+
}
|
|
1077
1079
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
+
Object.assign(presetEnvs, resolvedProfile.profileInlineEnvs);
|
|
1081
|
+
if (resolvedProfile.variantEnvFileSource !== undefined) {
|
|
1082
|
+
const content = await this.#readPresetFile(resolvedProfile.variantEnvFileSource, commandPath);
|
|
1083
|
+
if (content !== undefined) {
|
|
1084
|
+
const parsed = this.#parsePresetEnvsContent(content, resolvedProfile.variantEnvFileSource, commandPath);
|
|
1085
|
+
Object.assign(presetEnvs, parsed);
|
|
1086
|
+
}
|
|
1080
1087
|
}
|
|
1081
|
-
Object.assign(presetEnvs,
|
|
1088
|
+
Object.assign(presetEnvs, resolvedProfile.variantInlineEnvs);
|
|
1082
1089
|
}
|
|
1083
1090
|
const sources = {
|
|
1084
1091
|
user: userSources,
|
|
@@ -1091,200 +1098,446 @@ class Command {
|
|
|
1091
1098
|
const tailArgv = [...sources.preset.argv, ...sources.user.argv];
|
|
1092
1099
|
return { tailArgv, envs, sources };
|
|
1093
1100
|
}
|
|
1094
|
-
#
|
|
1101
|
+
#resolveCommandPresetFileFromChain(chain) {
|
|
1095
1102
|
for (let index = chain.length - 1; index >= 0; index -= 1) {
|
|
1096
1103
|
const preset = chain[index].#presetConfig;
|
|
1097
|
-
if (preset?.
|
|
1098
|
-
return preset;
|
|
1104
|
+
if (preset?.file !== undefined) {
|
|
1105
|
+
return preset.file;
|
|
1099
1106
|
}
|
|
1100
1107
|
}
|
|
1101
1108
|
return undefined;
|
|
1102
1109
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1110
|
+
#resolveCommandPresetProfileFromChain(chain) {
|
|
1111
|
+
for (let index = chain.length - 1; index >= 0; index -= 1) {
|
|
1112
|
+
const preset = chain[index].#presetConfig;
|
|
1113
|
+
if (preset?.profile !== undefined) {
|
|
1114
|
+
return preset.profile;
|
|
1115
|
+
}
|
|
1107
1116
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1117
|
+
return undefined;
|
|
1118
|
+
}
|
|
1119
|
+
#resolvePresetFileAbsolutePath(filepath, baseDirectory) {
|
|
1120
|
+
if (this.#runtime.isAbsolute(filepath)) {
|
|
1121
|
+
return filepath;
|
|
1110
1122
|
}
|
|
1111
|
-
return
|
|
1123
|
+
return this.#runtime.resolve(baseDirectory ?? this.#runtime.cwd(), filepath);
|
|
1112
1124
|
}
|
|
1113
|
-
async #
|
|
1114
|
-
|
|
1115
|
-
|
|
1125
|
+
async #resolvePresetProfile(params) {
|
|
1126
|
+
const { presetFile, presetProfile, presetProfileSourceName, commandPath } = params;
|
|
1127
|
+
if (presetFile === undefined) {
|
|
1128
|
+
if (presetProfile !== undefined) {
|
|
1129
|
+
throw new CommanderError('ConfigurationError', `cannot use "${PRESET_PROFILE_FLAG}" without "${PRESET_FILE_FLAG}"`, commandPath);
|
|
1130
|
+
}
|
|
1131
|
+
return undefined;
|
|
1116
1132
|
}
|
|
1117
|
-
|
|
1133
|
+
const profileFile = {
|
|
1134
|
+
displayPath: presetFile,
|
|
1135
|
+
absolutePath: this.#resolvePresetFileAbsolutePath(presetFile),
|
|
1136
|
+
explicit: true,
|
|
1137
|
+
};
|
|
1138
|
+
const content = await this.#readPresetFile(profileFile, commandPath);
|
|
1139
|
+
if (content === undefined) {
|
|
1140
|
+
return undefined;
|
|
1141
|
+
}
|
|
1142
|
+
const manifest = this.#parsePresetProfileManifest(content, profileFile.displayPath, commandPath);
|
|
1143
|
+
const resolvedProfileSelector = presetProfile ?? manifest.defaults?.profile;
|
|
1144
|
+
if (resolvedProfileSelector === undefined) {
|
|
1145
|
+
throw new CommanderError('ConfigurationError', `missing profile for preset file "${profileFile.displayPath}": provide "${PRESET_PROFILE_FLAG}" or defaults.profile`, commandPath);
|
|
1146
|
+
}
|
|
1147
|
+
const { profileName: resolvedProfileName, variantName: explicitVariantName } = this.#parsePresetProfileSelector(resolvedProfileSelector, presetProfileSourceName ?? 'defaults.profile', commandPath);
|
|
1148
|
+
const profile = manifest.profiles[resolvedProfileName];
|
|
1149
|
+
if (profile === undefined) {
|
|
1150
|
+
throw new CommanderError('ConfigurationError', `unknown preset profile "${resolvedProfileName}" in "${profileFile.displayPath}"`, commandPath);
|
|
1151
|
+
}
|
|
1152
|
+
const selectedVariantName = explicitVariantName ?? profile.defaultVariant;
|
|
1153
|
+
let selectedVariant;
|
|
1154
|
+
if (selectedVariantName !== undefined) {
|
|
1155
|
+
const variants = profile.variants ?? {};
|
|
1156
|
+
selectedVariant = variants[selectedVariantName];
|
|
1157
|
+
if (selectedVariant === undefined) {
|
|
1158
|
+
const availableVariants = Object.keys(variants);
|
|
1159
|
+
const availableText = availableVariants.length > 0 ? availableVariants.join(', ') : '<none>';
|
|
1160
|
+
throw new CommanderError('ConfigurationError', `unknown preset variant "${selectedVariantName}" for profile "${resolvedProfileName}" in "${profileFile.displayPath}" (available: ${availableText})`, commandPath);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
const profileSelectorLabel = selectedVariantName === undefined
|
|
1164
|
+
? resolvedProfileName
|
|
1165
|
+
: `${resolvedProfileName}${PRESET_SELECTOR_DELIMITER}${selectedVariantName}`;
|
|
1166
|
+
const mergedOpts = { ...(profile.opts ?? {}), ...(selectedVariant?.opts ?? {}) };
|
|
1167
|
+
const optsArgv = this.#buildPresetArgvFromProfileOptions(mergedOpts, profileSelectorLabel, commandPath);
|
|
1168
|
+
const profileInlineEnvs = this.#normalizePresetProfileEnvs(profile.envs, profileSelectorLabel, commandPath);
|
|
1169
|
+
const variantInlineEnvs = this.#normalizePresetProfileEnvs(selectedVariant?.envs, profileSelectorLabel, commandPath);
|
|
1170
|
+
const profileDir = this.#runtime.resolve(profileFile.absolutePath, '..');
|
|
1171
|
+
let profileEnvFileSource;
|
|
1172
|
+
if (profile.envFile !== undefined) {
|
|
1173
|
+
profileEnvFileSource = {
|
|
1174
|
+
displayPath: profile.envFile,
|
|
1175
|
+
absolutePath: this.#resolvePresetFileAbsolutePath(profile.envFile, profileDir),
|
|
1176
|
+
explicit: true,
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
let variantEnvFileSource;
|
|
1180
|
+
if (selectedVariant?.envFile !== undefined) {
|
|
1181
|
+
variantEnvFileSource = {
|
|
1182
|
+
displayPath: selectedVariant.envFile,
|
|
1183
|
+
absolutePath: this.#resolvePresetFileAbsolutePath(selectedVariant.envFile, profileDir),
|
|
1184
|
+
explicit: true,
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
return {
|
|
1188
|
+
profileName: resolvedProfileName,
|
|
1189
|
+
variantName: selectedVariantName,
|
|
1190
|
+
optsArgv,
|
|
1191
|
+
optsSourceLabel: `${profileFile.displayPath}#${profileSelectorLabel}.opts`,
|
|
1192
|
+
profileInlineEnvs,
|
|
1193
|
+
variantInlineEnvs,
|
|
1194
|
+
profileEnvFileSource,
|
|
1195
|
+
variantEnvFileSource,
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
#parsePresetProfileManifest(content, filepath, commandPath) {
|
|
1199
|
+
let parsed;
|
|
1118
1200
|
try {
|
|
1119
|
-
|
|
1201
|
+
parsed = JSON.parse(content);
|
|
1120
1202
|
}
|
|
1121
1203
|
catch (error) {
|
|
1122
|
-
throw new CommanderError('ConfigurationError', `
|
|
1204
|
+
throw new CommanderError('ConfigurationError', `failed to parse preset file "${filepath}": ${error.message}`, commandPath);
|
|
1123
1205
|
}
|
|
1124
|
-
if (
|
|
1125
|
-
throw new CommanderError('ConfigurationError', `invalid preset
|
|
1206
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
1207
|
+
throw new CommanderError('ConfigurationError', `invalid preset file "${filepath}": root must be an object`, commandPath);
|
|
1126
1208
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
if (filepath === undefined) {
|
|
1131
|
-
return undefined;
|
|
1209
|
+
const root = parsed;
|
|
1210
|
+
if (root.version !== 1) {
|
|
1211
|
+
throw new CommanderError('ConfigurationError', `invalid preset file "${filepath}": "version" must be 1`, commandPath);
|
|
1132
1212
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1213
|
+
let defaults;
|
|
1214
|
+
const rawDefaults = root.defaults;
|
|
1215
|
+
if (rawDefaults !== undefined) {
|
|
1216
|
+
if (typeof rawDefaults !== 'object' || rawDefaults === null || Array.isArray(rawDefaults)) {
|
|
1217
|
+
throw new CommanderError('ConfigurationError', `invalid preset file "${filepath}": "defaults" must be an object`, commandPath);
|
|
1218
|
+
}
|
|
1219
|
+
const defaultsRecord = rawDefaults;
|
|
1220
|
+
if (defaultsRecord.profile !== undefined) {
|
|
1221
|
+
if (typeof defaultsRecord.profile !== 'string') {
|
|
1222
|
+
throw new CommanderError('ConfigurationError', `invalid preset file "${filepath}": "defaults.profile" must be a string`, commandPath);
|
|
1223
|
+
}
|
|
1224
|
+
this.#assertPresetProfileSelectorValue(defaultsRecord.profile, 'defaults.profile', commandPath);
|
|
1225
|
+
}
|
|
1226
|
+
defaults = { profile: defaultsRecord.profile };
|
|
1135
1227
|
}
|
|
1136
|
-
|
|
1228
|
+
const rawProfiles = root.profiles;
|
|
1229
|
+
if (typeof rawProfiles !== 'object' || rawProfiles === null || Array.isArray(rawProfiles)) {
|
|
1230
|
+
throw new CommanderError('ConfigurationError', `invalid preset file "${filepath}": "profiles" must be an object`, commandPath);
|
|
1231
|
+
}
|
|
1232
|
+
const profilesRecord = {};
|
|
1233
|
+
for (const [profileName, profileValue] of Object.entries(rawProfiles)) {
|
|
1234
|
+
this.#assertPresetProfileName(profileName, `profiles["${profileName}"]`, commandPath);
|
|
1235
|
+
if (typeof profileValue !== 'object' ||
|
|
1236
|
+
profileValue === null ||
|
|
1237
|
+
Array.isArray(profileValue)) {
|
|
1238
|
+
throw new CommanderError('ConfigurationError', `invalid preset file "${filepath}": profile "${profileName}" must be an object`, commandPath);
|
|
1239
|
+
}
|
|
1240
|
+
profilesRecord[profileName] = this.#parsePresetProfileItem(profileValue, profileName, filepath, commandPath);
|
|
1241
|
+
}
|
|
1242
|
+
return {
|
|
1243
|
+
version: 1,
|
|
1244
|
+
defaults,
|
|
1245
|
+
profiles: profilesRecord,
|
|
1246
|
+
};
|
|
1137
1247
|
}
|
|
1138
|
-
#
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
}));
|
|
1248
|
+
#parsePresetProfileItem(profileValue, profileName, filepath, commandPath) {
|
|
1249
|
+
const labelPrefix = `invalid preset file "${filepath}": profile "${profileName}"`;
|
|
1250
|
+
const envFile = profileValue.envFile;
|
|
1251
|
+
if (envFile !== undefined) {
|
|
1252
|
+
if (typeof envFile !== 'string') {
|
|
1253
|
+
throw new CommanderError('ConfigurationError', `${labelPrefix}.envFile must be a string`, commandPath);
|
|
1254
|
+
}
|
|
1146
1255
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1256
|
+
const rawEnvs = profileValue.envs;
|
|
1257
|
+
let envs;
|
|
1258
|
+
if (rawEnvs !== undefined) {
|
|
1259
|
+
if (typeof rawEnvs !== 'object' || rawEnvs === null || Array.isArray(rawEnvs)) {
|
|
1260
|
+
throw new CommanderError('ConfigurationError', `${labelPrefix}.envs must be an object`, commandPath);
|
|
1261
|
+
}
|
|
1262
|
+
envs = {};
|
|
1263
|
+
for (const [key, value] of Object.entries(rawEnvs)) {
|
|
1264
|
+
if (typeof value !== 'string') {
|
|
1265
|
+
throw new CommanderError('ConfigurationError', `${labelPrefix}.envs["${key}"] must be a string`, commandPath);
|
|
1266
|
+
}
|
|
1267
|
+
envs[key] = value;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
const rawOpts = profileValue.opts;
|
|
1271
|
+
let opts;
|
|
1272
|
+
if (rawOpts !== undefined) {
|
|
1273
|
+
if (typeof rawOpts !== 'object' || rawOpts === null || Array.isArray(rawOpts)) {
|
|
1274
|
+
throw new CommanderError('ConfigurationError', `${labelPrefix}.opts must be an object`, commandPath);
|
|
1275
|
+
}
|
|
1276
|
+
opts = {};
|
|
1277
|
+
for (const [key, value] of Object.entries(rawOpts)) {
|
|
1278
|
+
opts[key] = this.#parsePresetProfileOptionValue(value, `${labelPrefix}.opts["${key}"]`, commandPath);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
const rawDefaultVariant = profileValue.defaultVariant;
|
|
1282
|
+
let defaultVariant;
|
|
1283
|
+
if (rawDefaultVariant !== undefined) {
|
|
1284
|
+
if (typeof rawDefaultVariant !== 'string') {
|
|
1285
|
+
throw new CommanderError('ConfigurationError', `${labelPrefix}.defaultVariant must be a string`, commandPath);
|
|
1286
|
+
}
|
|
1287
|
+
this.#assertPresetVariantName(rawDefaultVariant, `${labelPrefix}.defaultVariant`, commandPath);
|
|
1288
|
+
defaultVariant = rawDefaultVariant;
|
|
1289
|
+
}
|
|
1290
|
+
const rawVariants = profileValue.variants;
|
|
1291
|
+
let variants;
|
|
1292
|
+
if (rawVariants !== undefined) {
|
|
1293
|
+
if (typeof rawVariants !== 'object' || rawVariants === null || Array.isArray(rawVariants)) {
|
|
1294
|
+
throw new CommanderError('ConfigurationError', `${labelPrefix}.variants must be an object`, commandPath);
|
|
1295
|
+
}
|
|
1296
|
+
variants = {};
|
|
1297
|
+
for (const [variantName, variantValue] of Object.entries(rawVariants)) {
|
|
1298
|
+
this.#assertPresetVariantName(variantName, `${labelPrefix}.variants["${variantName}"]`, commandPath);
|
|
1299
|
+
if (typeof variantValue !== 'object' ||
|
|
1300
|
+
variantValue === null ||
|
|
1301
|
+
Array.isArray(variantValue)) {
|
|
1302
|
+
throw new CommanderError('ConfigurationError', `${labelPrefix}.variants["${variantName}"] must be an object`, commandPath);
|
|
1303
|
+
}
|
|
1304
|
+
variants[variantName] = this.#parsePresetProfileVariantItem(variantValue, `${labelPrefix}.variants["${variantName}"]`, commandPath);
|
|
1305
|
+
}
|
|
1149
1306
|
}
|
|
1150
|
-
if (
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
displayPath: commandPresetFile,
|
|
1154
|
-
absolutePath: this.#resolvePresetFileAbsolutePath(commandPresetFile, presetRoot),
|
|
1155
|
-
explicit: true,
|
|
1156
|
-
},
|
|
1157
|
-
];
|
|
1307
|
+
if (defaultVariant !== undefined &&
|
|
1308
|
+
(variants === undefined || variants[defaultVariant] === undefined)) {
|
|
1309
|
+
throw new CommanderError('ConfigurationError', `${labelPrefix}.defaultVariant "${defaultVariant}" is not found in variants`, commandPath);
|
|
1158
1310
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
];
|
|
1311
|
+
return {
|
|
1312
|
+
envFile,
|
|
1313
|
+
envs,
|
|
1314
|
+
opts,
|
|
1315
|
+
defaultVariant,
|
|
1316
|
+
variants,
|
|
1317
|
+
};
|
|
1167
1318
|
}
|
|
1168
|
-
#
|
|
1169
|
-
|
|
1170
|
-
|
|
1319
|
+
#parsePresetProfileVariantItem(variantValue, labelPrefix, commandPath) {
|
|
1320
|
+
const envFile = variantValue.envFile;
|
|
1321
|
+
if (envFile !== undefined) {
|
|
1322
|
+
if (typeof envFile !== 'string') {
|
|
1323
|
+
throw new CommanderError('ConfigurationError', `${labelPrefix}.envFile must be a string`, commandPath);
|
|
1324
|
+
}
|
|
1171
1325
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1326
|
+
const rawEnvs = variantValue.envs;
|
|
1327
|
+
let envs;
|
|
1328
|
+
if (rawEnvs !== undefined) {
|
|
1329
|
+
if (typeof rawEnvs !== 'object' || rawEnvs === null || Array.isArray(rawEnvs)) {
|
|
1330
|
+
throw new CommanderError('ConfigurationError', `${labelPrefix}.envs must be an object`, commandPath);
|
|
1331
|
+
}
|
|
1332
|
+
envs = {};
|
|
1333
|
+
for (const [key, value] of Object.entries(rawEnvs)) {
|
|
1334
|
+
if (typeof value !== 'string') {
|
|
1335
|
+
throw new CommanderError('ConfigurationError', `${labelPrefix}.envs["${key}"] must be a string`, commandPath);
|
|
1336
|
+
}
|
|
1337
|
+
envs[key] = value;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
const rawOpts = variantValue.opts;
|
|
1341
|
+
let opts;
|
|
1342
|
+
if (rawOpts !== undefined) {
|
|
1343
|
+
if (typeof rawOpts !== 'object' || rawOpts === null || Array.isArray(rawOpts)) {
|
|
1344
|
+
throw new CommanderError('ConfigurationError', `${labelPrefix}.opts must be an object`, commandPath);
|
|
1345
|
+
}
|
|
1346
|
+
opts = {};
|
|
1347
|
+
for (const [key, value] of Object.entries(rawOpts)) {
|
|
1348
|
+
opts[key] = this.#parsePresetProfileOptionValue(value, `${labelPrefix}.opts["${key}"]`, commandPath);
|
|
1349
|
+
}
|
|
1174
1350
|
}
|
|
1175
|
-
return
|
|
1351
|
+
return {
|
|
1352
|
+
envFile,
|
|
1353
|
+
envs,
|
|
1354
|
+
opts,
|
|
1355
|
+
};
|
|
1176
1356
|
}
|
|
1177
|
-
#
|
|
1178
|
-
if (
|
|
1179
|
-
return;
|
|
1357
|
+
#parsePresetProfileOptionValue(value, valueLabel, commandPath) {
|
|
1358
|
+
if (typeof value === 'boolean' || typeof value === 'string') {
|
|
1359
|
+
return value;
|
|
1180
1360
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
void restArgs;
|
|
1185
|
-
const { argTokens } = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
1186
|
-
if (argTokens.length > 0) {
|
|
1187
|
-
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": token "${argTokens[0].original}" cannot be resolved as an option fragment`, commandPath);
|
|
1361
|
+
if (typeof value === 'number') {
|
|
1362
|
+
if (!Number.isFinite(value)) {
|
|
1363
|
+
throw new CommanderError('ConfigurationError', `${valueLabel} must be a finite number`, commandPath);
|
|
1188
1364
|
}
|
|
1365
|
+
return value;
|
|
1189
1366
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
if (
|
|
1193
|
-
|
|
1367
|
+
if (Array.isArray(value)) {
|
|
1368
|
+
return value.map((item, index) => {
|
|
1369
|
+
if (typeof item === 'string') {
|
|
1370
|
+
return item;
|
|
1194
1371
|
}
|
|
1195
|
-
|
|
1372
|
+
if (typeof item === 'number' && Number.isFinite(item)) {
|
|
1373
|
+
return item;
|
|
1374
|
+
}
|
|
1375
|
+
throw new CommanderError('ConfigurationError', `${valueLabel}[${index}] must be a string or finite number`, commandPath);
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
throw new CommanderError('ConfigurationError', `${valueLabel} must be boolean|string|number|(string|number)[]`, commandPath);
|
|
1379
|
+
}
|
|
1380
|
+
#normalizePresetProfileEnvs(envs, _profileName, _commandPath) {
|
|
1381
|
+
return envs === undefined ? {} : { ...envs };
|
|
1382
|
+
}
|
|
1383
|
+
#normalizePresetOptionName(rawName, profileName, commandPath) {
|
|
1384
|
+
const value = rawName.trim();
|
|
1385
|
+
if (value.length === 0) {
|
|
1386
|
+
throw new CommanderError('ConfigurationError', `invalid option name "" in preset profile "${profileName}"`, commandPath);
|
|
1387
|
+
}
|
|
1388
|
+
const stripped = value.startsWith('--') ? value.slice(2) : value;
|
|
1389
|
+
if (stripped.length === 0) {
|
|
1390
|
+
throw new CommanderError('ConfigurationError', `invalid option name "${rawName}" in preset profile "${profileName}"`, commandPath);
|
|
1391
|
+
}
|
|
1392
|
+
if (stripped.includes('-')) {
|
|
1393
|
+
const lowered = stripped.toLowerCase();
|
|
1394
|
+
if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(lowered)) {
|
|
1395
|
+
throw new CommanderError('ConfigurationError', `invalid option name "${rawName}" in preset profile "${profileName}"`, commandPath);
|
|
1196
1396
|
}
|
|
1197
|
-
|
|
1397
|
+
return kebabToCamelCase(lowered);
|
|
1398
|
+
}
|
|
1399
|
+
if (!/^[a-z][a-zA-Z0-9]*$/.test(stripped)) {
|
|
1400
|
+
throw new CommanderError('ConfigurationError', `invalid option name "${rawName}" in preset profile "${profileName}"`, commandPath);
|
|
1198
1401
|
}
|
|
1402
|
+
return stripped;
|
|
1199
1403
|
}
|
|
1200
|
-
#
|
|
1201
|
-
const
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_ROOT_FLAG}"`, commandPath);
|
|
1210
|
-
}
|
|
1211
|
-
cliPresetRoots.push(value);
|
|
1212
|
-
index += 2;
|
|
1404
|
+
#buildPresetArgvFromProfileOptions(opts, profileName, commandPath) {
|
|
1405
|
+
const argv = [];
|
|
1406
|
+
for (const [rawName, rawValue] of Object.entries(opts)) {
|
|
1407
|
+
const optionName = this.#normalizePresetOptionName(rawName, profileName, commandPath);
|
|
1408
|
+
const kebabName = camelToKebabCase$1(optionName);
|
|
1409
|
+
const positiveFlag = `--${kebabName}`;
|
|
1410
|
+
const negativeFlag = `--no-${kebabName}`;
|
|
1411
|
+
if (typeof rawValue === 'boolean') {
|
|
1412
|
+
argv.push(rawValue ? positiveFlag : negativeFlag);
|
|
1213
1413
|
continue;
|
|
1214
1414
|
}
|
|
1215
|
-
if (
|
|
1216
|
-
|
|
1217
|
-
if (value.length === 0) {
|
|
1218
|
-
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_ROOT_FLAG}"`, commandPath);
|
|
1219
|
-
}
|
|
1220
|
-
cliPresetRoots.push(value);
|
|
1221
|
-
index += 1;
|
|
1415
|
+
if (typeof rawValue === 'string') {
|
|
1416
|
+
argv.push(positiveFlag, rawValue);
|
|
1222
1417
|
continue;
|
|
1223
1418
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1419
|
+
if (typeof rawValue === 'number') {
|
|
1420
|
+
argv.push(positiveFlag, String(rawValue));
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
if (rawValue.length === 0) {
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
argv.push(positiveFlag, ...rawValue.map(value => String(value)));
|
|
1226
1427
|
}
|
|
1227
|
-
return
|
|
1428
|
+
return argv;
|
|
1228
1429
|
}
|
|
1229
|
-
#
|
|
1430
|
+
#scanPresetProfileDirectives(argv, commandPath) {
|
|
1230
1431
|
const cleanArgv = [];
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
const
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
cliPresetOptsFiles.push(value);
|
|
1432
|
+
let presetFile;
|
|
1433
|
+
let presetProfile;
|
|
1434
|
+
const assignDirective = (flag, value) => {
|
|
1435
|
+
if (flag === PRESET_FILE_FLAG) {
|
|
1436
|
+
presetFile = value;
|
|
1237
1437
|
}
|
|
1238
1438
|
else {
|
|
1239
|
-
|
|
1439
|
+
this.#assertPresetProfileSelectorValue(value, PRESET_PROFILE_FLAG, commandPath);
|
|
1440
|
+
presetProfile = value;
|
|
1240
1441
|
}
|
|
1241
1442
|
};
|
|
1242
1443
|
let index = 0;
|
|
1243
1444
|
while (index < argv.length) {
|
|
1244
1445
|
const token = argv[index];
|
|
1245
|
-
if (token ===
|
|
1446
|
+
if (token === PRESET_FILE_FLAG || token === PRESET_PROFILE_FLAG) {
|
|
1246
1447
|
const value = argv[index + 1];
|
|
1247
1448
|
if (value === undefined || value.length === 0) {
|
|
1248
1449
|
throw new CommanderError('ConfigurationError', `missing value for "${token}"`, commandPath);
|
|
1249
1450
|
}
|
|
1250
|
-
|
|
1451
|
+
assignDirective(token, value);
|
|
1251
1452
|
index += 2;
|
|
1252
1453
|
continue;
|
|
1253
1454
|
}
|
|
1254
|
-
if (token.startsWith(`${
|
|
1255
|
-
const value = token.slice(
|
|
1455
|
+
if (token.startsWith(`${PRESET_FILE_FLAG}=`)) {
|
|
1456
|
+
const value = token.slice(PRESET_FILE_FLAG.length + 1);
|
|
1256
1457
|
if (value.length === 0) {
|
|
1257
|
-
throw new CommanderError('ConfigurationError', `missing value for "${
|
|
1458
|
+
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_FILE_FLAG}"`, commandPath);
|
|
1258
1459
|
}
|
|
1259
|
-
|
|
1460
|
+
assignDirective(PRESET_FILE_FLAG, value);
|
|
1260
1461
|
index += 1;
|
|
1261
1462
|
continue;
|
|
1262
1463
|
}
|
|
1263
|
-
if (token.startsWith(`${
|
|
1264
|
-
const value = token.slice(
|
|
1464
|
+
if (token.startsWith(`${PRESET_PROFILE_FLAG}=`)) {
|
|
1465
|
+
const value = token.slice(PRESET_PROFILE_FLAG.length + 1);
|
|
1265
1466
|
if (value.length === 0) {
|
|
1266
|
-
throw new CommanderError('ConfigurationError', `missing value for "${
|
|
1467
|
+
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_PROFILE_FLAG}"`, commandPath);
|
|
1267
1468
|
}
|
|
1268
|
-
|
|
1469
|
+
assignDirective(PRESET_PROFILE_FLAG, value);
|
|
1269
1470
|
index += 1;
|
|
1270
1471
|
continue;
|
|
1271
1472
|
}
|
|
1272
1473
|
cleanArgv.push(token);
|
|
1273
1474
|
index += 1;
|
|
1274
1475
|
}
|
|
1275
|
-
return { cleanArgv,
|
|
1476
|
+
return { cleanArgv, presetFile, presetProfile };
|
|
1477
|
+
}
|
|
1478
|
+
#assertPresetOptionFragments(tokens, filepath, chain, optionPolicyMap) {
|
|
1479
|
+
if (tokens.length === 0) {
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const commandPath = chain[chain.length - 1].#getCommandPath();
|
|
1483
|
+
try {
|
|
1484
|
+
const { optionTokens, restArgs } = tokenize(tokens, commandPath);
|
|
1485
|
+
void restArgs;
|
|
1486
|
+
const { argTokens } = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
1487
|
+
if (argTokens.length > 0) {
|
|
1488
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": token "${argTokens[0].original}" cannot be resolved as an option fragment`, commandPath);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
catch (error) {
|
|
1492
|
+
if (error instanceof CommanderError) {
|
|
1493
|
+
if (error.kind === 'ConfigurationError') {
|
|
1494
|
+
throw error;
|
|
1495
|
+
}
|
|
1496
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": ${error.message}`, commandPath);
|
|
1497
|
+
}
|
|
1498
|
+
throw error;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
#assertPresetProfileSelectorValue(selector, sourceName, commandPath) {
|
|
1502
|
+
void this.#parsePresetProfileSelector(selector, sourceName, commandPath);
|
|
1503
|
+
}
|
|
1504
|
+
#parsePresetProfileSelector(selector, sourceName, commandPath) {
|
|
1505
|
+
const normalizedSelector = selector.trim();
|
|
1506
|
+
const separatorIndex = normalizedSelector.indexOf(PRESET_SELECTOR_DELIMITER);
|
|
1507
|
+
if (separatorIndex < 0) {
|
|
1508
|
+
this.#assertPresetProfileName(normalizedSelector, sourceName, commandPath);
|
|
1509
|
+
return { profileName: normalizedSelector };
|
|
1510
|
+
}
|
|
1511
|
+
if (normalizedSelector.indexOf(PRESET_SELECTOR_DELIMITER, separatorIndex + 1) >= 0) {
|
|
1512
|
+
throw new CommanderError('ConfigurationError', `invalid value for "${sourceName}": "${selector}" (must be "<profile>" or "<profile>:<variant>")`, commandPath);
|
|
1513
|
+
}
|
|
1514
|
+
const profileName = normalizedSelector.slice(0, separatorIndex);
|
|
1515
|
+
const variantName = normalizedSelector.slice(separatorIndex + 1);
|
|
1516
|
+
if (profileName.length === 0 || variantName.length === 0) {
|
|
1517
|
+
throw new CommanderError('ConfigurationError', `invalid value for "${sourceName}": "${selector}" (must be "<profile>" or "<profile>:<variant>")`, commandPath);
|
|
1518
|
+
}
|
|
1519
|
+
this.#assertPresetProfileName(profileName, sourceName, commandPath);
|
|
1520
|
+
this.#assertPresetVariantName(variantName, sourceName, commandPath);
|
|
1521
|
+
return { profileName, variantName };
|
|
1276
1522
|
}
|
|
1277
|
-
#
|
|
1278
|
-
|
|
1523
|
+
#assertPresetProfileName(profileName, sourceName, commandPath) {
|
|
1524
|
+
if (PRESET_PROFILE_NAME_REGEX.test(profileName)) {
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
throw new CommanderError('ConfigurationError', `invalid profile name for "${sourceName}": "${profileName}" (must match ${PRESET_PROFILE_NAME_REGEX.source})`, commandPath);
|
|
1279
1528
|
}
|
|
1280
|
-
#
|
|
1281
|
-
if (
|
|
1529
|
+
#assertPresetVariantName(variantName, sourceName, commandPath) {
|
|
1530
|
+
if (PRESET_VARIANT_NAME_REGEX.test(variantName)) {
|
|
1282
1531
|
return;
|
|
1283
1532
|
}
|
|
1284
|
-
throw new CommanderError('ConfigurationError', `invalid
|
|
1533
|
+
throw new CommanderError('ConfigurationError', `invalid variant name for "${sourceName}": "${variantName}" (must match ${PRESET_VARIANT_NAME_REGEX.source})`, commandPath);
|
|
1285
1534
|
}
|
|
1286
1535
|
async #readPresetFile(file, commandPath) {
|
|
1287
1536
|
try {
|
|
1537
|
+
const stats = await this.#runtime.stat(file.absolutePath);
|
|
1538
|
+
if (stats.isDirectory()) {
|
|
1539
|
+
throw new Error('target is a directory');
|
|
1540
|
+
}
|
|
1288
1541
|
return await this.#runtime.readFile(file.absolutePath);
|
|
1289
1542
|
}
|
|
1290
1543
|
catch (error) {
|
|
@@ -1295,11 +1548,13 @@ class Command {
|
|
|
1295
1548
|
throw new CommanderError('ConfigurationError', `failed to read preset file "${file.displayPath}": ${error.message}`, commandPath);
|
|
1296
1549
|
}
|
|
1297
1550
|
}
|
|
1298
|
-
#
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1551
|
+
#parsePresetEnvsContent(content, file, commandPath) {
|
|
1552
|
+
try {
|
|
1553
|
+
return parse(content);
|
|
1554
|
+
}
|
|
1555
|
+
catch (error) {
|
|
1556
|
+
throw new CommanderError('ConfigurationError', `failed to parse preset env file "${file.displayPath}": ${error.message}`, commandPath);
|
|
1557
|
+
}
|
|
1303
1558
|
}
|
|
1304
1559
|
#validatePresetOptionTokens(tokens, filepath, commandPath) {
|
|
1305
1560
|
if (tokens.length === 0) {
|
|
@@ -1315,12 +1570,10 @@ class Command {
|
|
|
1315
1570
|
if (token === 'help' || token === '--help' || token === '--version') {
|
|
1316
1571
|
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": control token "${token}" is not allowed`, commandPath);
|
|
1317
1572
|
}
|
|
1318
|
-
if (token ===
|
|
1319
|
-
token.startsWith(`${
|
|
1320
|
-
token ===
|
|
1321
|
-
token.startsWith(`${
|
|
1322
|
-
token === PRESET_ENVS_FLAG ||
|
|
1323
|
-
token.startsWith(`${PRESET_ENVS_FLAG}=`)) {
|
|
1573
|
+
if (token === PRESET_FILE_FLAG ||
|
|
1574
|
+
token.startsWith(`${PRESET_FILE_FLAG}=`) ||
|
|
1575
|
+
token === PRESET_PROFILE_FLAG ||
|
|
1576
|
+
token.startsWith(`${PRESET_PROFILE_FLAG}=`)) {
|
|
1324
1577
|
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": preset directive "${token}" is not allowed`, commandPath);
|
|
1325
1578
|
}
|
|
1326
1579
|
}
|