@aws/ml-container-creator 1.0.3 → 1.1.0

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.
Files changed (79) hide show
  1. package/README.md +10 -1
  2. package/bin/cli.js +57 -0
  3. package/config/agent.json +16 -0
  4. package/infra/ci-harness/lib/ci-harness-stack.ts +43 -0
  5. package/package.json +5 -2
  6. package/pyproject.toml +3 -0
  7. package/servers/agent-knowledge/index.js +592 -0
  8. package/servers/agent-knowledge/package.json +15 -0
  9. package/servers/base-image-picker/index.js +65 -18
  10. package/servers/instance-sizer/index.js +32 -0
  11. package/servers/lib/catalogs/fleet-drivers.json +38 -0
  12. package/servers/lib/catalogs/model-arch-support.json +51 -0
  13. package/servers/lib/catalogs/model-servers.json +2842 -1730
  14. package/servers/lib/schemas/image-catalog.schema.json +12 -0
  15. package/src/agent/__init__.py +2 -0
  16. package/src/agent/__pycache__/__init__.cpython-312.pyc +0 -0
  17. package/src/agent/__pycache__/config_loader.cpython-312.pyc +0 -0
  18. package/src/agent/__pycache__/context.cpython-312.pyc +0 -0
  19. package/src/agent/__pycache__/health_check.cpython-312.pyc +0 -0
  20. package/src/agent/agent.py +513 -0
  21. package/src/agent/config_loader.py +215 -0
  22. package/src/agent/context.py +380 -0
  23. package/src/agent/data/capability-matrix.json +106 -0
  24. package/src/agent/health_check.py +341 -0
  25. package/src/agent/prompts/system.md +173 -0
  26. package/src/agent/requirements-agent.txt +3 -0
  27. package/src/app.js +6 -4
  28. package/src/lib/generated/cli-options.js +1 -1
  29. package/src/lib/generated/parameter-matrix.js +1 -1
  30. package/src/lib/generated/validation-rules.js +1 -1
  31. package/src/lib/mcp-query-runner.js +110 -3
  32. package/src/lib/prompt-runner.js +66 -22
  33. package/src/lib/template-variable-resolver.js +8 -0
  34. package/src/lib/train-config-builder.js +339 -0
  35. package/src/lib/tune-config-state.js +89 -68
  36. package/templates/do/.benchmark_writer.py +3 -0
  37. package/templates/do/.eval_helper.py +409 -0
  38. package/templates/do/.register_helper.py +185 -11
  39. package/templates/do/.train_build_request.py +102 -113
  40. package/templates/do/.train_helper.py +433 -0
  41. package/templates/do/__pycache__/.register_helper.cpython-312.pyc +0 -0
  42. package/templates/do/adapter +157 -0
  43. package/templates/do/benchmark +60 -3
  44. package/templates/do/config +6 -1
  45. package/templates/do/deploy.d/managed-inference.ejs +83 -0
  46. package/templates/do/evaluate +272 -0
  47. package/templates/do/lib/resolve-instance.sh +155 -0
  48. package/templates/do/register +5 -0
  49. package/templates/do/test +1 -0
  50. package/templates/do/train +879 -126
  51. package/templates/do/training/config.yaml +83 -11
  52. package/templates/do/training/dpo/accelerate_config.yaml +24 -0
  53. package/templates/do/training/dpo/defaults.yaml +26 -0
  54. package/templates/do/training/dpo/prompts.json +8 -0
  55. package/templates/do/training/dpo/train.py +363 -0
  56. package/templates/do/training/sft/accelerate_config.yaml +22 -0
  57. package/templates/do/training/sft/defaults.yaml +18 -0
  58. package/templates/do/training/sft/prompts.json +7 -0
  59. package/templates/do/training/sft/train.py +310 -0
  60. package/templates/do/tune +11 -2
  61. package/src/lib/auto-prompt-builder.js +0 -172
  62. package/src/lib/cli-handler.js +0 -529
  63. package/src/lib/community-reports-validator.js +0 -91
  64. package/src/lib/configuration-exporter.js +0 -204
  65. package/src/lib/dataset-slug.js +0 -152
  66. package/src/lib/docker-introspection-validator.js +0 -51
  67. package/src/lib/known-flags-validator.js +0 -200
  68. package/src/lib/schema-validator.js +0 -157
  69. package/src/lib/train-config-parser.js +0 -136
  70. package/src/lib/train-config-persistence.js +0 -143
  71. package/src/lib/train-config-validator.js +0 -112
  72. package/src/lib/train-feedback.js +0 -46
  73. package/src/lib/train-idempotency.js +0 -97
  74. package/src/lib/train-request-builder.js +0 -120
  75. package/src/lib/tune-dataset-validator.js +0 -279
  76. package/src/lib/tune-output-resolver.js +0 -66
  77. package/templates/do/.train_poll_parser.py +0 -135
  78. package/templates/do/.train_status_parser.py +0 -187
  79. /package/templates/do/training/{train.py → custom/train.py} +0 -0
@@ -1,204 +0,0 @@
1
- /**
2
- * Configuration Exporter
3
- *
4
- * Handles prompting users to export configurations for community contribution
5
- * and saving exported configurations to files.
6
- *
7
- * Requirements: 7.1, 7.2, 7.3, 7.4
8
- */
9
-
10
- import fs from 'fs';
11
- import path from 'path';
12
- import { runPrompts } from '../prompt-adapter.js';
13
-
14
- export default class ConfigurationExporter {
15
- constructor() {
16
- // No generator dependency — uses console.log and runPrompts directly
17
- }
18
-
19
- /**
20
- * Detect if configuration should be offered for export
21
- * Offers export for experimental or unknown configurations
22
- *
23
- * @param {Object} config - Configuration profile
24
- * @returns {boolean} Whether to offer export
25
- *
26
- * Requirements: 7.1
27
- */
28
- shouldOfferExport(config) {
29
- if (!config) return false;
30
-
31
- const validationLevel = config.validationLevel || 'unknown';
32
-
33
- // Offer export for experimental or unknown configurations
34
- return validationLevel === 'experimental' || validationLevel === 'unknown';
35
- }
36
-
37
- /**
38
- * Prompt user to export configuration
39
- * Captures testing notes and deployment results
40
- *
41
- * @param {Object} config - Configuration profile to export
42
- * @returns {Promise<Object|null>} Export data or null if user declined
43
- *
44
- * Requirements: 7.1, 7.2, 7.3, 7.4
45
- */
46
- async promptForExport(config) {
47
- if (!this.shouldOfferExport(config)) {
48
- return null;
49
- }
50
-
51
- console.log('\n📤 Configuration Export');
52
- console.log('━'.repeat(50));
53
- console.log(`This configuration has validation level: ${config.validationLevel || 'unknown'}`);
54
- console.log('If you successfully deploy and test this configuration, please consider');
55
- console.log('sharing it with the community to help others!');
56
- console.log('');
57
-
58
- // Ask if user wants to export
59
- const { wantsToExport } = await runPrompts([
60
- {
61
- type: 'confirm',
62
- name: 'wantsToExport',
63
- message: 'Would you like to export this configuration for community contribution?',
64
- default: false
65
- }
66
- ]);
67
-
68
- if (!wantsToExport) {
69
- console.log('Skipping export. You can always export later after testing.');
70
- return null;
71
- }
72
-
73
- // Collect testing information
74
- const exportData = await runPrompts([
75
- {
76
- type: 'input',
77
- name: 'instanceType',
78
- message: 'What instance type did you use (or plan to use) for testing?',
79
- default: config.recommendedInstanceTypes?.[0] || 'ml.g5.xlarge',
80
- validate: (input) => {
81
- if (!input || input.trim() === '') {
82
- return 'Instance type is required';
83
- }
84
- return true;
85
- }
86
- },
87
- {
88
- type: 'confirm',
89
- name: 'deploymentSuccess',
90
- message: 'Did the deployment succeed?',
91
- default: false
92
- },
93
- {
94
- type: 'confirm',
95
- name: 'inferenceSuccess',
96
- message: 'Did inference work correctly?',
97
- default: false,
98
- when: (answers) => answers.deploymentSuccess
99
- },
100
- {
101
- type: 'input',
102
- name: 'testingNotes',
103
- message: 'Any notes about your testing experience? (optional)',
104
- default: ''
105
- },
106
- {
107
- type: 'input',
108
- name: 'testerName',
109
- message: 'Your name or GitHub handle (optional, for attribution):',
110
- default: 'Anonymous'
111
- }
112
- ]);
113
-
114
- return exportData;
115
- }
116
-
117
- /**
118
- * Save exported configuration to file
119
- * Creates a file in the project directory with export instructions
120
- *
121
- * @param {Object} exportResult - Result from ConfigurationManager.exportConfiguration()
122
- * @param {string} destinationPath - Destination directory path
123
- *
124
- * Requirements: 7.2, 7.3, 7.5, 7.6
125
- */
126
- saveExportToFile(exportResult, destinationPath) {
127
- const { registryType, configEntry, submissionInstructions, metadata } = exportResult;
128
-
129
- // Create export filename
130
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
131
- const filename = `config-export-${registryType}-${timestamp}.md`;
132
- const filepath = path.join(destinationPath, filename);
133
-
134
- // Create export file content
135
- const content = `${submissionInstructions}
136
-
137
- ## Export Metadata
138
-
139
- \`\`\`json
140
- ${JSON.stringify(metadata, null, 2)}
141
- \`\`\`
142
-
143
- ## Configuration Entry (JSON)
144
-
145
- \`\`\`json
146
- ${JSON.stringify(configEntry, null, 2)}
147
- \`\`\`
148
- `;
149
-
150
- // Write file
151
- fs.writeFileSync(filepath, content, 'utf-8');
152
-
153
- return filename;
154
- }
155
-
156
- /**
157
- * Display export success message
158
- *
159
- * @param {string} filename - Name of exported file
160
- */
161
- displayExportSuccess(filename) {
162
- console.log('');
163
- console.log('✅ Configuration exported successfully!');
164
- console.log(`📄 Export saved to: ${filename}`);
165
- console.log('');
166
- console.log('Next steps:');
167
- console.log('1. Review the export file');
168
- console.log('2. Test your deployment');
169
- console.log('3. Submit via GitHub issue or pull request');
170
- console.log('4. Help the community! 🎉');
171
- console.log('');
172
- }
173
-
174
- /**
175
- * Complete export workflow
176
- * Prompts user, exports configuration, and saves to file
177
- *
178
- * @param {Object} config - Configuration profile
179
- * @param {Object} configurationManager - ConfigurationManager instance
180
- * @param {string} destinationPath - Destination directory path
181
- * @returns {Promise<boolean>} Whether export was completed
182
- *
183
- * Requirements: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6
184
- */
185
- async exportWorkflow(config, configurationManager, destinationPath) {
186
- // Prompt for export
187
- const exportData = await this.promptForExport(config);
188
-
189
- if (!exportData) {
190
- return false;
191
- }
192
-
193
- // Export configuration
194
- const exportResult = configurationManager.exportConfiguration(config, exportData);
195
-
196
- // Save to file
197
- const filename = this.saveExportToFile(exportResult, destinationPath);
198
-
199
- // Display success message
200
- this.displayExportSuccess(filename);
201
-
202
- return true;
203
- }
204
- }
@@ -1,152 +0,0 @@
1
- // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
- // SPDX-License-Identifier: Apache-2.0
3
-
4
- /**
5
- * Dataset Slug Derivation
6
- *
7
- * Derives a deterministic, short slug from a dataset URI for use in
8
- * tuning-job-aware adapter naming conventions.
9
- *
10
- * Slugification rules:
11
- * - Lowercase
12
- * - Strip non-alphanumeric characters (keep hyphens)
13
- * - Truncate to 20 characters
14
- * - Replace consecutive hyphens with single hyphen
15
- * - Strip leading/trailing hyphens
16
- *
17
- * Examples:
18
- * hf://org/name -> "name"
19
- * hf://tatsu-lab/alpaca -> "alpaca"
20
- * hf://Open-Orca/OpenOrca -> "openorca"
21
- * s3://bucket/path/file.jsonl -> "file"
22
- *
23
- * Requirements: US-4 (AC-4.2)
24
- */
25
-
26
- /**
27
- * Derive a dataset slug from a dataset URI.
28
- *
29
- * @param {string} datasetUri - Dataset URI (s3://... or hf://...)
30
- * @returns {string} The derived slug, or empty string if extraction fails
31
- */
32
- export function deriveDatasetSlug(datasetUri) {
33
- if (!datasetUri || typeof datasetUri !== 'string') {
34
- return '';
35
- }
36
-
37
- let rawName = '';
38
-
39
- if (datasetUri.startsWith('hf://')) {
40
- // hf://org/name[/split][?file=pattern]
41
- // Extract the dataset name (second path component)
42
- const hfPath = datasetUri.slice(5); // remove "hf://"
43
- const withoutQuery = hfPath.split('?')[0]; // remove ?file=...
44
- const parts = withoutQuery.split('/');
45
- // parts[0] = org, parts[1] = name, parts[2+] = split
46
- rawName = parts[1] || parts[0] || '';
47
- } else if (datasetUri.startsWith('s3://')) {
48
- // s3://bucket/path/file.jsonl -> slug from filename (without extension)
49
- const s3Path = datasetUri.slice(5); // remove "s3://"
50
- const parts = s3Path.split('/');
51
- const filename = parts[parts.length - 1] || '';
52
- // Remove file extension
53
- const dotIndex = filename.lastIndexOf('.');
54
- rawName = dotIndex > 0 ? filename.substring(0, dotIndex) : filename;
55
- } else {
56
- // Unknown format — try to extract last path component
57
- const parts = datasetUri.split('/');
58
- rawName = parts[parts.length - 1] || '';
59
- }
60
-
61
- return slugify(rawName);
62
- }
63
-
64
- /**
65
- * Apply slugification rules to a raw name.
66
- *
67
- * @param {string} raw - Raw name to slugify
68
- * @returns {string} Slugified string
69
- */
70
- export function slugify(raw) {
71
- if (!raw) return '';
72
-
73
- let slug = raw
74
- .toLowerCase() // lowercase
75
- .replace(/[^a-z0-9-]/g, '') // strip non-alphanumeric (keep hyphens)
76
- .replace(/-{2,}/g, '-') // replace consecutive hyphens
77
- .replace(/^-+/, '') // strip leading hyphens
78
- .replace(/-+$/, ''); // strip trailing hyphens
79
-
80
- // Truncate to 20 chars
81
- if (slug.length > 20) {
82
- slug = slug.substring(0, 20);
83
- // Don't end on a hyphen after truncation
84
- slug = slug.replace(/-+$/, '');
85
- }
86
-
87
- return slug;
88
- }
89
-
90
- /**
91
- * Resolve a --from-tune argument to the appropriate config variable name.
92
- *
93
- * Resolution rules:
94
- * - No arg (empty/null) -> TUNE_OUTPUT_PATH_LATEST
95
- * - technique only (e.g., "sft") -> TUNE_ADAPTER_PATH_SFT
96
- * - technique-dataset compound (e.g., "sft-alpaca") -> TUNE_ADAPTER_PATH_SFT_ALPACA
97
- *
98
- * @param {string} fromTuneArg - The --from-tune argument value
99
- * @param {function} configVarExists - Function that checks if a config var exists
100
- * @returns {{ varName: string, technique: string, slug: string, isCompound: boolean, fallback: string|null }}
101
- */
102
- export function resolveFromTuneVar(fromTuneArg, configVarExists) {
103
- if (!fromTuneArg) {
104
- return {
105
- varName: 'TUNE_OUTPUT_PATH_LATEST',
106
- technique: '',
107
- slug: '',
108
- isCompound: false,
109
- fallback: null
110
- };
111
- }
112
-
113
- const upper = fromTuneArg.toUpperCase();
114
-
115
- // Check if argument contains a hyphen — potential compound key
116
- const hyphenIndex = fromTuneArg.indexOf('-');
117
- if (hyphenIndex > 0) {
118
- const technique = fromTuneArg.substring(0, hyphenIndex);
119
- const slug = fromTuneArg.substring(hyphenIndex + 1);
120
- const techniqueUpper = technique.toUpperCase();
121
- const slugUpper = slug.toUpperCase().replace(/-/g, '_');
122
- const compoundVar = `TUNE_ADAPTER_PATH_${techniqueUpper}_${slugUpper}`;
123
-
124
- if (configVarExists(compoundVar)) {
125
- return {
126
- varName: compoundVar,
127
- technique,
128
- slug,
129
- isCompound: true,
130
- fallback: null
131
- };
132
- }
133
-
134
- // Compound key doesn't exist — fallback to technique-only
135
- return {
136
- varName: `TUNE_ADAPTER_PATH_${techniqueUpper}`,
137
- technique,
138
- slug,
139
- isCompound: false,
140
- fallback: compoundVar // the compound var that was tried but didn't exist
141
- };
142
- }
143
-
144
- // No hyphen — technique-only
145
- return {
146
- varName: `TUNE_ADAPTER_PATH_${upper}`,
147
- technique: fromTuneArg,
148
- slug: '',
149
- isCompound: false,
150
- fallback: null
151
- };
152
- }
@@ -1,51 +0,0 @@
1
- /**
2
- * Docker Introspection Validator Strategy (Opt-in)
3
- *
4
- * Validates environment variables by introspecting Docker images.
5
- * This is an experimental strategy that requires Docker to be available.
6
- *
7
- * Requirements: 13.11, 13.18
8
- */
9
- export default class DockerIntrospectionValidator {
10
- /**
11
- * Create a new DockerIntrospectionValidator.
12
- */
13
- constructor() {
14
- this.name = 'docker-introspection';
15
- }
16
-
17
- /**
18
- * Validate environment variables using Docker introspection.
19
- *
20
- * Note: This is an experimental feature and not tested in CI/CD.
21
- * It requires Docker to be available and the framework image to be pullable.
22
- *
23
- * @param {string} framework - Framework name
24
- * @param {string} version - Framework version
25
- * @param {Object} envVars - Environment variables to validate
26
- * @returns {Object} ValidationResult
27
- * @returns {Array<Object>} ValidationResult.warnings - Warning messages
28
- * @returns {Array<Object>} ValidationResult.errors - Error messages
29
- */
30
- async validate(_framework, _version, _envVars) {
31
- const warnings = [];
32
- const errors = [];
33
-
34
- // Add experimental warning
35
- warnings.push({
36
- key: null,
37
- message: 'Docker introspection validation is experimental and not tested in CI/CD'
38
- });
39
-
40
- // Docker introspection implementation would go here
41
- // This is a placeholder for the opt-in experimental feature
42
- // Actual implementation would:
43
- // 1. Pull the framework Docker image
44
- // 2. Run a container with the env vars
45
- // 3. Check if the container starts successfully
46
- // 4. Parse any error messages from the container logs
47
-
48
- // For now, just return the experimental warning
49
- return { warnings, errors };
50
- }
51
- }
@@ -1,200 +0,0 @@
1
- /**
2
- * Known Flags Validator Strategy
3
- *
4
- * Validates environment variables against a registry of known flags for each framework.
5
- * Checks variable names, types, and range constraints.
6
- *
7
- * Requirements: 13.9, 13.13, 13.14, 13.15
8
- */
9
- export default class KnownFlagsValidator {
10
- /**
11
- * Create a new KnownFlagsValidator.
12
- *
13
- * @param {Object} frameworkFlags - Framework flags registry
14
- */
15
- constructor(frameworkFlags = {}) {
16
- this.frameworkFlags = frameworkFlags;
17
- this.name = 'known-flags-registry';
18
- }
19
-
20
- /**
21
- * Validate environment variables against known flags registry.
22
- *
23
- * @param {string} framework - Framework name
24
- * @param {string} version - Framework version
25
- * @param {Object} envVars - Environment variables to validate
26
- * @returns {Object} ValidationResult
27
- * @returns {Array<Object>} ValidationResult.warnings - Warning messages
28
- * @returns {Array<Object>} ValidationResult.errors - Error messages
29
- */
30
- async validate(framework, version, envVars) {
31
- const warnings = [];
32
- const errors = [];
33
-
34
- // Get known flags for this framework version
35
- const knownFlags = this.getKnownFlags(framework, version);
36
-
37
- if (!knownFlags || Object.keys(knownFlags).length === 0) {
38
- // No known flags data available
39
- return { warnings, errors };
40
- }
41
-
42
- // Validate each environment variable
43
- for (const [key, value] of Object.entries(envVars)) {
44
- const flagSpec = knownFlags[key];
45
-
46
- if (!flagSpec) {
47
- // Unknown flag - might be valid but not in our registry
48
- warnings.push({
49
- key,
50
- message: `Unknown environment variable '${key}' for ${framework} ${version}`
51
- });
52
- continue;
53
- }
54
-
55
- // Check if flag is deprecated
56
- if (flagSpec.deprecated) {
57
- warnings.push({
58
- key,
59
- message: `Environment variable '${key}' is deprecated. ${flagSpec.deprecationMessage || ''}`
60
- });
61
-
62
- if (flagSpec.replacement) {
63
- warnings.push({
64
- key,
65
- message: `Consider using '${flagSpec.replacement}' instead of '${key}'`
66
- });
67
- }
68
- }
69
-
70
- // Validate type
71
- const typeError = this.validateType(key, value, flagSpec.type);
72
- if (typeError) {
73
- errors.push(typeError);
74
- continue; // Skip range validation if type is wrong
75
- }
76
-
77
- // Validate range constraints
78
- const rangeError = this.validateRange(key, value, flagSpec);
79
- if (rangeError) {
80
- errors.push(rangeError);
81
- }
82
- }
83
-
84
- return { warnings, errors };
85
- }
86
-
87
- /**
88
- * Get known flags for a framework version.
89
- *
90
- * @param {string} framework - Framework name
91
- * @param {string} version - Framework version
92
- * @returns {Object|null} Known flags specification
93
- * @private
94
- */
95
- getKnownFlags(framework, version) {
96
- if (!this.frameworkFlags[framework]) {
97
- return null;
98
- }
99
-
100
- // Try exact version match first
101
- if (this.frameworkFlags[framework][version]) {
102
- return this.frameworkFlags[framework][version];
103
- }
104
-
105
- // Try to find closest version (simplified - just use 'default' if available)
106
- if (this.frameworkFlags[framework].default) {
107
- return this.frameworkFlags[framework].default;
108
- }
109
-
110
- return null;
111
- }
112
-
113
- /**
114
- * Validate environment variable type.
115
- *
116
- * @param {string} key - Variable name
117
- * @param {string} value - Variable value
118
- * @param {string} expectedType - Expected type (integer, float, string, boolean)
119
- * @returns {Object|null} Error object or null if valid
120
- * @private
121
- */
122
- validateType(key, value, expectedType) {
123
- if (!expectedType) {
124
- return null; // No type constraint
125
- }
126
-
127
- switch (expectedType) {
128
- case 'integer':
129
- if (!/^-?\d+$/.test(value)) {
130
- return {
131
- key,
132
- message: `Environment variable '${key}' must be an integer, got '${value}'`
133
- };
134
- }
135
- break;
136
-
137
- case 'float':
138
- if (!/^-?\d+(\.\d+)?$/.test(value)) {
139
- return {
140
- key,
141
- message: `Environment variable '${key}' must be a float, got '${value}'`
142
- };
143
- }
144
- break;
145
-
146
- case 'boolean':
147
- if (!['true', 'false', '0', '1', 'yes', 'no'].includes(value.toLowerCase())) {
148
- return {
149
- key,
150
- message: `Environment variable '${key}' must be a boolean (true/false, 0/1, yes/no), got '${value}'`
151
- };
152
- }
153
- break;
154
-
155
- case 'string':
156
- // String is always valid
157
- break;
158
-
159
- default:
160
- // Unknown type - skip validation
161
- break;
162
- }
163
-
164
- return null;
165
- }
166
-
167
- /**
168
- * Validate environment variable range constraints.
169
- *
170
- * @param {string} key - Variable name
171
- * @param {string} value - Variable value
172
- * @param {Object} flagSpec - Flag specification with min/max constraints
173
- * @returns {Object|null} Error object or null if valid
174
- * @private
175
- */
176
- validateRange(key, value, flagSpec) {
177
- // Only validate range for numeric types
178
- if (flagSpec.type !== 'integer' && flagSpec.type !== 'float') {
179
- return null;
180
- }
181
-
182
- const numValue = parseFloat(value);
183
-
184
- if (flagSpec.min !== undefined && numValue < flagSpec.min) {
185
- return {
186
- key,
187
- message: `Environment variable '${key}' must be >= ${flagSpec.min}, got ${value}`
188
- };
189
- }
190
-
191
- if (flagSpec.max !== undefined && numValue > flagSpec.max) {
192
- return {
193
- key,
194
- message: `Environment variable '${key}' must be <= ${flagSpec.max}, got ${value}`
195
- };
196
- }
197
-
198
- return null;
199
- }
200
- }