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