@guanghechen/commander 4.7.5 → 4.7.7

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