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