@guanghechen/commander 4.7.5 → 4.7.6

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