@aws/ml-container-creator 0.2.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 (143) hide show
  1. package/LICENSE +202 -0
  2. package/LICENSE-THIRD-PARTY +68620 -0
  3. package/NOTICE +2 -0
  4. package/README.md +106 -0
  5. package/bin/cli.js +365 -0
  6. package/config/defaults.json +32 -0
  7. package/config/presets/transformers-djl.json +26 -0
  8. package/config/presets/transformers-gpu.json +24 -0
  9. package/config/presets/transformers-lmi.json +27 -0
  10. package/package.json +129 -0
  11. package/servers/README.md +419 -0
  12. package/servers/base-image-picker/catalogs/model-servers.json +1191 -0
  13. package/servers/base-image-picker/catalogs/python-slim.json +38 -0
  14. package/servers/base-image-picker/catalogs/triton-backends.json +51 -0
  15. package/servers/base-image-picker/catalogs/triton.json +38 -0
  16. package/servers/base-image-picker/index.js +495 -0
  17. package/servers/base-image-picker/manifest.json +17 -0
  18. package/servers/base-image-picker/package.json +15 -0
  19. package/servers/hyperpod-cluster-picker/LICENSE +202 -0
  20. package/servers/hyperpod-cluster-picker/index.js +424 -0
  21. package/servers/hyperpod-cluster-picker/manifest.json +14 -0
  22. package/servers/hyperpod-cluster-picker/package.json +17 -0
  23. package/servers/instance-recommender/LICENSE +202 -0
  24. package/servers/instance-recommender/catalogs/instances.json +852 -0
  25. package/servers/instance-recommender/index.js +284 -0
  26. package/servers/instance-recommender/manifest.json +16 -0
  27. package/servers/instance-recommender/package.json +15 -0
  28. package/servers/lib/LICENSE +202 -0
  29. package/servers/lib/bedrock-client.js +160 -0
  30. package/servers/lib/custom-validators.js +46 -0
  31. package/servers/lib/dynamic-resolver.js +36 -0
  32. package/servers/lib/package.json +11 -0
  33. package/servers/lib/schemas/image-catalog.schema.json +185 -0
  34. package/servers/lib/schemas/instances.schema.json +124 -0
  35. package/servers/lib/schemas/manifest.schema.json +64 -0
  36. package/servers/lib/schemas/model-catalog.schema.json +91 -0
  37. package/servers/lib/schemas/regions.schema.json +26 -0
  38. package/servers/lib/schemas/triton-backends.schema.json +51 -0
  39. package/servers/model-picker/catalogs/jumpstart-public.json +66 -0
  40. package/servers/model-picker/catalogs/popular-diffusors.json +88 -0
  41. package/servers/model-picker/catalogs/popular-transformers.json +226 -0
  42. package/servers/model-picker/index.js +1693 -0
  43. package/servers/model-picker/manifest.json +18 -0
  44. package/servers/model-picker/package.json +20 -0
  45. package/servers/region-picker/LICENSE +202 -0
  46. package/servers/region-picker/catalogs/regions.json +263 -0
  47. package/servers/region-picker/index.js +230 -0
  48. package/servers/region-picker/manifest.json +16 -0
  49. package/servers/region-picker/package.json +15 -0
  50. package/src/app.js +1007 -0
  51. package/src/copy-tpl.js +77 -0
  52. package/src/lib/accelerator-validator.js +39 -0
  53. package/src/lib/asset-manager.js +385 -0
  54. package/src/lib/aws-profile-parser.js +181 -0
  55. package/src/lib/bootstrap-command-handler.js +1647 -0
  56. package/src/lib/bootstrap-config.js +238 -0
  57. package/src/lib/ci-register-helpers.js +124 -0
  58. package/src/lib/ci-report-helpers.js +158 -0
  59. package/src/lib/ci-stage-helpers.js +268 -0
  60. package/src/lib/cli-handler.js +529 -0
  61. package/src/lib/comment-generator.js +544 -0
  62. package/src/lib/community-reports-validator.js +91 -0
  63. package/src/lib/config-manager.js +2106 -0
  64. package/src/lib/configuration-exporter.js +204 -0
  65. package/src/lib/configuration-manager.js +695 -0
  66. package/src/lib/configuration-matcher.js +221 -0
  67. package/src/lib/cpu-validator.js +36 -0
  68. package/src/lib/cuda-validator.js +57 -0
  69. package/src/lib/deployment-config-resolver.js +103 -0
  70. package/src/lib/deployment-entry-schema.js +125 -0
  71. package/src/lib/deployment-registry.js +598 -0
  72. package/src/lib/docker-introspection-validator.js +51 -0
  73. package/src/lib/engine-prefix-resolver.js +60 -0
  74. package/src/lib/huggingface-client.js +172 -0
  75. package/src/lib/key-value-parser.js +37 -0
  76. package/src/lib/known-flags-validator.js +200 -0
  77. package/src/lib/manifest-cli.js +280 -0
  78. package/src/lib/mcp-client.js +303 -0
  79. package/src/lib/mcp-command-handler.js +532 -0
  80. package/src/lib/neuron-validator.js +80 -0
  81. package/src/lib/parameter-schema-validator.js +284 -0
  82. package/src/lib/prompt-runner.js +1349 -0
  83. package/src/lib/prompts.js +1138 -0
  84. package/src/lib/registry-command-handler.js +519 -0
  85. package/src/lib/registry-loader.js +198 -0
  86. package/src/lib/rocm-validator.js +80 -0
  87. package/src/lib/schema-validator.js +157 -0
  88. package/src/lib/sensitive-redactor.js +59 -0
  89. package/src/lib/template-engine.js +156 -0
  90. package/src/lib/template-manager.js +341 -0
  91. package/src/lib/validation-engine.js +314 -0
  92. package/src/prompt-adapter.js +63 -0
  93. package/templates/Dockerfile +300 -0
  94. package/templates/IAM_PERMISSIONS.md +84 -0
  95. package/templates/MIGRATION.md +488 -0
  96. package/templates/PROJECT_README.md +439 -0
  97. package/templates/TEMPLATE_SYSTEM.md +243 -0
  98. package/templates/buildspec.yml +64 -0
  99. package/templates/code/chat_template.jinja +1 -0
  100. package/templates/code/flask/gunicorn_config.py +35 -0
  101. package/templates/code/flask/wsgi.py +10 -0
  102. package/templates/code/model_handler.py +387 -0
  103. package/templates/code/serve +300 -0
  104. package/templates/code/serve.py +175 -0
  105. package/templates/code/serving.properties +105 -0
  106. package/templates/code/start_server.py +39 -0
  107. package/templates/code/start_server.sh +39 -0
  108. package/templates/diffusors/Dockerfile +72 -0
  109. package/templates/diffusors/patch_image_api.py +35 -0
  110. package/templates/diffusors/serve +115 -0
  111. package/templates/diffusors/start_server.sh +114 -0
  112. package/templates/do/.gitkeep +1 -0
  113. package/templates/do/README.md +541 -0
  114. package/templates/do/build +83 -0
  115. package/templates/do/ci +681 -0
  116. package/templates/do/clean +811 -0
  117. package/templates/do/config +260 -0
  118. package/templates/do/deploy +1560 -0
  119. package/templates/do/export +306 -0
  120. package/templates/do/logs +319 -0
  121. package/templates/do/manifest +12 -0
  122. package/templates/do/push +119 -0
  123. package/templates/do/register +580 -0
  124. package/templates/do/run +113 -0
  125. package/templates/do/submit +417 -0
  126. package/templates/do/test +1147 -0
  127. package/templates/hyperpod/configmap.yaml +24 -0
  128. package/templates/hyperpod/deployment.yaml +71 -0
  129. package/templates/hyperpod/pvc.yaml +42 -0
  130. package/templates/hyperpod/service.yaml +17 -0
  131. package/templates/nginx-diffusors.conf +74 -0
  132. package/templates/nginx-predictors.conf +47 -0
  133. package/templates/nginx-tensorrt.conf +74 -0
  134. package/templates/requirements.txt +61 -0
  135. package/templates/sample_model/test_inference.py +123 -0
  136. package/templates/sample_model/train_abalone.py +252 -0
  137. package/templates/test/test_endpoint.sh +79 -0
  138. package/templates/test/test_local_image.sh +80 -0
  139. package/templates/test/test_model_handler.py +180 -0
  140. package/templates/triton/Dockerfile +128 -0
  141. package/templates/triton/config.pbtxt +163 -0
  142. package/templates/triton/model.py +130 -0
  143. package/templates/triton/requirements.txt +11 -0
@@ -0,0 +1,2106 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Configuration Manager - Handles configuration precedence and merging
6
+ *
7
+ * Implements the complete precedence order (Highest → Lowest Priority):
8
+ * 1. CLI Options (--framework=transformers)
9
+ * 2. CLI Arguments (yo generator projectName)
10
+ * 3. Environment Variables (AWS_REGION=us-east-1)
11
+ * 4. CLI Config File (--config=prod.json) / Inline JSON (--config-json='...')
12
+ * 5. Custom Config File (config/mcp.json)
13
+ * 6. Package.json Section ("ml-container-creator": {...})
14
+ * 7. Bootstrap Config (~/.ml-container-creator/config.json)
15
+ * 8. Generator Defaults
16
+ * 9. Prompting (fallback)
17
+ */
18
+
19
+ import fs from 'fs';
20
+ import path from 'path';
21
+ import { readFileSync } from 'node:fs';
22
+ import { resolve, dirname } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+ import { McpClient } from './mcp-client.js';
25
+ import DeploymentConfigResolver from './deployment-config-resolver.js';
26
+ import BootstrapConfig from './bootstrap-config.js';
27
+ import { parseKeyValue } from './key-value-parser.js';
28
+ import ParameterSchemaValidator from './parameter-schema-validator.js';
29
+
30
+ const __configMgrFilename = fileURLToPath(import.meta.url);
31
+ const __configMgrDir = dirname(__configMgrFilename);
32
+ const tritonBackendsCatalogPath = resolve(__configMgrDir, '../../servers/base-image-picker/catalogs/triton-backends.json');
33
+
34
+ function loadTritonBackendsFromCatalog() {
35
+ try {
36
+ const raw = readFileSync(tritonBackendsCatalogPath, 'utf8');
37
+ return JSON.parse(raw);
38
+ } catch (error) {
39
+ console.warn(`Failed to load triton backends catalog: ${error.message}`);
40
+ return {};
41
+ }
42
+ }
43
+
44
+ const tritonBackends = loadTritonBackendsFromCatalog();
45
+
46
+ // Resolve the generator project root (two levels up from src/lib/)
47
+ const __filename = fileURLToPath(import.meta.url);
48
+ const __dirname = path.dirname(__filename);
49
+ const GENERATOR_ROOT = path.resolve(__dirname, '..', '..');
50
+
51
+ /**
52
+ * Configuration error for invalid configuration values
53
+ */
54
+ export class ConfigurationError extends Error {
55
+ constructor(message, parameter, source) {
56
+ super(message);
57
+ this.name = 'ConfigurationError';
58
+ this.parameter = parameter;
59
+ this.source = source;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Validation error for invalid parameter values
65
+ */
66
+ export class ValidationError extends Error {
67
+ constructor(message, parameter, value) {
68
+ super(message);
69
+ this.name = 'ValidationError';
70
+ this.parameter = parameter;
71
+ this.value = value;
72
+ }
73
+ }
74
+
75
+ export default class ConfigManager {
76
+ constructor({ options, args }) {
77
+ this.options = options || {};
78
+ this.args = args || [];
79
+ this.config = {};
80
+ this.skipPrompts = false;
81
+ this.deploymentConfigResolver = new DeploymentConfigResolver();
82
+ this.parameterMatrix = this._getParameterMatrix();
83
+ this.schemaValidator = new ParameterSchemaValidator();
84
+ this.mcpSources = {};
85
+ this.mcpChoices = {};
86
+ this._sourceManifest = [];
87
+ }
88
+
89
+ /**
90
+ * Loads configuration from all sources according to precedence
91
+ * @returns {Object} Merged configuration object
92
+ */
93
+ async loadConfiguration() {
94
+ // Start with generator defaults
95
+ this.config = this._getGeneratorDefaults();
96
+
97
+ // Track explicit configuration (non-default values)
98
+ this.explicitConfig = {};
99
+
100
+ // Apply configurations in reverse precedence order (lowest to highest)
101
+ await this._loadBootstrapConfig();
102
+ await this._loadPackageJsonConfig();
103
+ await this._loadCustomConfigFile();
104
+ await this._loadCliConfigFile();
105
+ await this._loadEnvironmentVariables();
106
+ await this._loadCliArguments();
107
+ await this._loadCliOptions();
108
+
109
+ // Query configured MCP servers for unbounded parameter values
110
+ await this._queryMcpServers();
111
+
112
+ // Check if we should skip prompts
113
+ this.skipPrompts = this.options['skip-prompts'] ||
114
+ this._hasCompleteConfiguration();
115
+
116
+ return this.config;
117
+ }
118
+
119
+ /**
120
+ * Checks if prompting should be skipped
121
+ * @returns {boolean}
122
+ */
123
+ shouldSkipPrompts() {
124
+ return this.skipPrompts;
125
+ }
126
+
127
+ /**
128
+ * Gets the final configuration, filling in any missing values with prompts
129
+ * @param {Object} promptAnswers - Answers from prompting phase
130
+ * @returns {Object} Complete configuration
131
+ */
132
+ getFinalConfiguration(promptAnswers = {}) {
133
+ // Prompting has lowest precedence, so only use for missing values
134
+ const finalConfig = { ...promptAnswers };
135
+
136
+ // Override with explicit configuration (not defaults)
137
+ const explicitConfig = this.getExplicitConfiguration();
138
+ Object.keys(explicitConfig).forEach(key => {
139
+ if (explicitConfig[key] !== undefined && explicitConfig[key] !== null) {
140
+ finalConfig[key] = explicitConfig[key];
141
+ }
142
+ });
143
+
144
+ // Fill in missing values with defaults from this.config
145
+ Object.keys(this.config).forEach(key => {
146
+ if (finalConfig[key] === undefined || finalConfig[key] === null) {
147
+ finalConfig[key] = this.config[key];
148
+ }
149
+ });
150
+
151
+ // Ensure env var collections are properly merged (CLI over config file over registry)
152
+ // this.config already has the fully merged result from all sources
153
+ if (this.config.modelEnvVars && typeof this.config.modelEnvVars === 'object') {
154
+ finalConfig.modelEnvVars = { ...this.config.modelEnvVars }
155
+ }
156
+ if (this.config.serverEnvVars && typeof this.config.serverEnvVars === 'object') {
157
+ finalConfig.serverEnvVars = { ...this.config.serverEnvVars }
158
+ }
159
+
160
+ // Ensure all parameters from the matrix are included in final config
161
+ // This is important for optional parameters that might be null
162
+ Object.entries(this.parameterMatrix).forEach(([param, config]) => {
163
+ if (finalConfig[param] === undefined) {
164
+ finalConfig[param] = this.config[param] || config.default;
165
+ }
166
+ });
167
+
168
+ // Derive architecture, backend, and engine from deploymentConfig using DeploymentConfigResolver.
169
+ // In prompted mode the PromptRunner may do this, but in --skip-prompts
170
+ // mode we need to do it here so the values are available for downstream logic.
171
+ if (finalConfig.deploymentConfig) {
172
+ const parts = this.deploymentConfigResolver.decompose(finalConfig.deploymentConfig)
173
+ finalConfig.architecture = parts.architecture
174
+ finalConfig.backend = parts.backend
175
+ // For http architecture, engine comes from the --engine CLI option or prompt
176
+ if (parts.architecture === 'http') {
177
+ if (!finalConfig.engine) {
178
+ finalConfig.engine = parts.engine
179
+ }
180
+ } else {
181
+ finalConfig.engine = parts.engine
182
+ }
183
+ }
184
+
185
+ // When skipping prompts, provide reasonable defaults for missing required parameters
186
+ if (this.skipPrompts) {
187
+ Object.entries(this.parameterMatrix).forEach(([param, config]) => {
188
+ if (config.required &&
189
+ (finalConfig[param] === null || finalConfig[param] === undefined)) {
190
+
191
+ // Provide reasonable defaults for missing required parameters
192
+ if (param === 'modelFormat') {
193
+ // Infer model format from architecture/engine (skip for transformers/triton)
194
+ const architecture = finalConfig.architecture || 'http'
195
+ if (architecture === 'http') {
196
+ const engine = finalConfig.engine || 'sklearn'
197
+ const formatMap = {
198
+ 'sklearn': 'pkl',
199
+ 'xgboost': 'json',
200
+ 'tensorflow': 'keras'
201
+ }
202
+ finalConfig[param] = formatMap[engine] || 'pkl'
203
+ }
204
+ } else if (param === 'instanceType') {
205
+ // Default to ml.m5.large for http, ml.g5.xlarge for transformers/triton
206
+ const architecture = finalConfig.architecture || 'http'
207
+ finalConfig[param] = architecture === 'http' ? 'ml.m5.large' : 'ml.g5.xlarge'
208
+ } else if (param === 'projectName') {
209
+ // Generate project name
210
+ finalConfig[param] = this._generateProjectName(finalConfig.architecture)
211
+ } else if (config.default !== null) {
212
+ // Use default value if available
213
+ finalConfig[param] = config.default;
214
+ }
215
+ }
216
+ });
217
+ }
218
+
219
+ // Always generate values for non-promptable required parameters that are missing
220
+ Object.entries(this.parameterMatrix).forEach(([param, config]) => {
221
+ if (config.required && !config.promptable &&
222
+ (finalConfig[param] === null || finalConfig[param] === undefined)) {
223
+
224
+ if (param === 'projectName') {
225
+ // Generate project name based on architecture or use default
226
+ finalConfig[param] = this._generateProjectName(finalConfig.architecture);
227
+ } else if (config.default !== null) {
228
+ // Use default value if available
229
+ finalConfig[param] = config.default;
230
+ }
231
+ }
232
+ });
233
+
234
+ // Apply architecture-specific overrides
235
+ if (finalConfig.architecture === 'transformers') {
236
+ finalConfig.includeSampleModel = false;
237
+ }
238
+ if (finalConfig.architecture === 'diffusors') {
239
+ finalConfig.includeSampleModel = false;
240
+ }
241
+ if (finalConfig.architecture === 'triton') {
242
+ const backendMeta = tritonBackends[finalConfig.backend];
243
+ if (!backendMeta || !backendMeta.supportsSampleModel) {
244
+ finalConfig.includeSampleModel = false;
245
+ }
246
+ }
247
+
248
+ // Set destinationDir based on projectName if not explicitly provided via --project-dir
249
+ // This matches standard CLI behavior:
250
+ // - `ml-container-creator my-app` creates `./my-app/` subdirectory
251
+ // - `ml-container-creator my-app --project-dir /tmp` uses `/tmp/` directly
252
+ // - `yo generator --project-name my-app` uses current directory (option, not argument)
253
+ //
254
+ // Only create subdirectory when:
255
+ // 1. Project name was provided as a positional CLI argument (not option/config)
256
+ // 2. --project-dir was NOT explicitly provided
257
+ // 3. destinationDir is still the default '.'
258
+
259
+ const projectNameFromArgument = this.projectNameFromArgument || false;
260
+ const explicitDestination = explicitConfig.destinationDir;
261
+
262
+ if (projectNameFromArgument &&
263
+ !explicitDestination &&
264
+ finalConfig.destinationDir === '.') {
265
+ finalConfig.destinationDir = `./${finalConfig.projectName}`;
266
+ }
267
+
268
+ // Generate CodeBuild project name if buildTarget is codebuild
269
+ if ((finalConfig.buildTarget === 'codebuild' || finalConfig.deployTarget === 'codebuild') && !finalConfig.codebuildProjectName) {
270
+ finalConfig.codebuildProjectName = this._generateCodeBuildProjectName(
271
+ finalConfig.projectName,
272
+ finalConfig.architecture
273
+ );
274
+ }
275
+
276
+ // Add build timestamp if not present
277
+ if (!finalConfig.buildTimestamp) {
278
+ finalConfig.buildTimestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
279
+ }
280
+
281
+ // Resolve HF_TOKEN environment variable references
282
+ // This happens after all configuration sources have been merged
283
+ if (finalConfig.hfToken) {
284
+ finalConfig.hfToken = this._resolveHfToken(finalConfig.hfToken);
285
+ }
286
+
287
+ // Map awsRoleArn to roleArn for templates
288
+ if (finalConfig.awsRoleArn) {
289
+ finalConfig.roleArn = finalConfig.awsRoleArn;
290
+ delete finalConfig.awsRoleArn;
291
+ }
292
+
293
+ return finalConfig;
294
+ }
295
+
296
+ /**
297
+ * Gets only the explicit configuration (not defaults) for prompting
298
+ * @returns {Object} Explicit configuration only
299
+ */
300
+ getExplicitConfiguration() {
301
+ return this.explicitConfig || {};
302
+ }
303
+
304
+ /**
305
+ * Gets the MCP source tracking information
306
+ * @returns {Object} Map of parameter names to their MCP source info
307
+ */
308
+ getMcpSources() {
309
+ return this.mcpSources || {};
310
+ }
311
+
312
+ /**
313
+ * Returns the complete configuration object with all parameter families
314
+ * separated into named collections for validation layer consumption.
315
+ * @returns {{
316
+ * core: Object,
317
+ * endpointConfig: Object,
318
+ * icConfig: Object,
319
+ * modelEnvVars: Object,
320
+ * serverEnvVars: Object,
321
+ * manifest: Array<{param: string, value: *, source: string}>
322
+ * }}
323
+ */
324
+ getFullConfiguration() {
325
+ const endpointParams = [
326
+ 'endpointInitialInstanceCount',
327
+ 'endpointDataCapturePercent',
328
+ 'endpointVariantName',
329
+ 'endpointVolumeSize'
330
+ ]
331
+ const icParams = [
332
+ 'icCpuCount',
333
+ 'icMemorySize',
334
+ 'icGpuCount',
335
+ 'icCopyCount',
336
+ 'icModelWeight'
337
+ ]
338
+
339
+ const endpointConfig = {}
340
+ for (const param of endpointParams) {
341
+ const shortKey = param.replace('endpoint', '')
342
+ const key = shortKey.charAt(0).toLowerCase() + shortKey.slice(1)
343
+ if (this.config[param] !== undefined && this.config[param] !== null) {
344
+ endpointConfig[key] = this.config[param]
345
+ }
346
+ }
347
+
348
+ const icConfig = {}
349
+ for (const param of icParams) {
350
+ const shortKey = param.replace('ic', '')
351
+ const key = shortKey.charAt(0).toLowerCase() + shortKey.slice(1)
352
+ if (this.config[param] !== undefined && this.config[param] !== null) {
353
+ icConfig[key] = this.config[param]
354
+ }
355
+ }
356
+
357
+ // Core parameters: everything that is NOT endpoint, iC, or env var collections
358
+ const excludedFromCore = new Set([
359
+ ...endpointParams,
360
+ ...icParams,
361
+ 'modelEnvVars',
362
+ 'serverEnvVars'
363
+ ])
364
+ const core = {}
365
+ for (const [key, value] of Object.entries(this.config)) {
366
+ if (!excludedFromCore.has(key) && key !== '_sourceManifest') {
367
+ core[key] = value
368
+ }
369
+ }
370
+
371
+ return {
372
+ core,
373
+ endpointConfig,
374
+ icConfig,
375
+ modelEnvVars: { ...(this.config.modelEnvVars || {}) },
376
+ serverEnvVars: { ...(this.config.serverEnvVars || {}) },
377
+ manifest: [...this._sourceManifest]
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Merge registry-provided environment variables with CLI-provided values.
383
+ * CLI values take precedence over registry values for the same key.
384
+ * Requirements: 3.3, 4.3
385
+ * @param {Object} registryModelEnvVars - Model env vars from registry
386
+ * @param {Object} registryServerEnvVars - Server env vars from registry
387
+ */
388
+ mergeRegistryEnvVars(registryModelEnvVars = {}, registryServerEnvVars = {}) {
389
+ // Initialize collections if needed
390
+ if (!this.config.modelEnvVars || typeof this.config.modelEnvVars !== 'object') {
391
+ this.config.modelEnvVars = {}
392
+ }
393
+ if (!this.config.serverEnvVars || typeof this.config.serverEnvVars !== 'object') {
394
+ this.config.serverEnvVars = {}
395
+ }
396
+
397
+ // Merge registry model env vars (CLI takes precedence)
398
+ Object.entries(registryModelEnvVars).forEach(([key, value]) => {
399
+ if (!(key in this.config.modelEnvVars)) {
400
+ this.config.modelEnvVars[key] = value
401
+ this._recordSource(`modelEnvVars.${key}`, value, 'registry')
402
+ }
403
+ })
404
+
405
+ // Merge registry server env vars (CLI takes precedence)
406
+ Object.entries(registryServerEnvVars).forEach(([key, value]) => {
407
+ if (!(key in this.config.serverEnvVars)) {
408
+ this.config.serverEnvVars[key] = value
409
+ this._recordSource(`serverEnvVars.${key}`, value, 'registry')
410
+ }
411
+ })
412
+ }
413
+
414
+ /**
415
+ * Gets the parameter matrix configuration
416
+ * @private
417
+ */
418
+ _getParameterMatrix() {
419
+ return {
420
+ deploymentConfig: {
421
+ cliOption: 'deployment-config',
422
+ envVar: null,
423
+ configFile: true,
424
+ packageJson: false,
425
+ mcp: false,
426
+ promptable: true,
427
+ required: true,
428
+ default: null,
429
+ valueSpace: 'bounded'
430
+ },
431
+ architecture: {
432
+ cliOption: null,
433
+ envVar: null,
434
+ configFile: false,
435
+ packageJson: false,
436
+ mcp: false,
437
+ promptable: false,
438
+ required: false,
439
+ default: null,
440
+ valueSpace: 'bounded'
441
+ },
442
+ backend: {
443
+ cliOption: null,
444
+ envVar: null,
445
+ configFile: false,
446
+ packageJson: false,
447
+ mcp: false,
448
+ promptable: false,
449
+ required: false,
450
+ default: null,
451
+ valueSpace: 'bounded'
452
+ },
453
+ engine: {
454
+ cliOption: 'engine',
455
+ envVar: null,
456
+ configFile: true,
457
+ packageJson: false,
458
+ mcp: false,
459
+ promptable: true,
460
+ required: false,
461
+ default: null,
462
+ valueSpace: 'bounded'
463
+ },
464
+ modelFormat: {
465
+ cliOption: 'model-format',
466
+ envVar: null,
467
+ configFile: true,
468
+ packageJson: false,
469
+ mcp: false,
470
+ promptable: true,
471
+ required: true,
472
+ default: null,
473
+ valueSpace: 'bounded'
474
+ },
475
+ modelName: {
476
+ cliOption: 'model-name',
477
+ envVar: null,
478
+ configFile: true,
479
+ packageJson: false,
480
+ mcp: false,
481
+ promptable: true,
482
+ required: false,
483
+ default: 'openai/gpt-oss-20b',
484
+ valueSpace: 'bounded'
485
+ },
486
+ includeSampleModel: {
487
+ cliOption: 'include-sample',
488
+ envVar: null,
489
+ configFile: true,
490
+ packageJson: false,
491
+ mcp: false,
492
+ promptable: true,
493
+ required: true,
494
+ default: false,
495
+ valueSpace: 'bounded'
496
+ },
497
+ includeTesting: {
498
+ cliOption: 'include-testing',
499
+ envVar: null,
500
+ configFile: true,
501
+ packageJson: false,
502
+ mcp: false,
503
+ promptable: false,
504
+ required: false,
505
+ default: true,
506
+ valueSpace: 'bounded'
507
+ },
508
+ instanceType: {
509
+ cliOption: 'instance-type',
510
+ envVar: 'ML_INSTANCE_TYPE',
511
+ configFile: true,
512
+ packageJson: false,
513
+ mcp: true,
514
+ promptable: true,
515
+ required: true,
516
+ default: null,
517
+ valueSpace: 'unbounded'
518
+ },
519
+ awsRegion: {
520
+ cliOption: 'region',
521
+ envVar: 'AWS_REGION',
522
+ ambientEnvVar: true, // AWS_REGION is commonly set in shells; treat as default, not explicit override
523
+ configFile: true,
524
+ packageJson: true,
525
+ mcp: true,
526
+ promptable: true,
527
+ required: false,
528
+ default: 'us-east-1',
529
+ valueSpace: 'unbounded'
530
+ },
531
+ awsRoleArn: {
532
+ cliOption: 'role-arn',
533
+ envVar: 'AWS_ROLE',
534
+ configFile: true,
535
+ packageJson: true,
536
+ mcp: true,
537
+ promptable: true,
538
+ required: false,
539
+ default: null,
540
+ valueSpace: 'unbounded'
541
+ },
542
+ configFile: {
543
+ cliOption: 'config',
544
+ envVar: 'ML_CONTAINER_CREATOR_CONFIG',
545
+ configFile: false,
546
+ packageJson: true,
547
+ mcp: false,
548
+ promptable: true,
549
+ required: false,
550
+ default: null,
551
+ valueSpace: 'bounded'
552
+ },
553
+ skipPrompts: {
554
+ cliOption: 'skip-prompts',
555
+ envVar: null,
556
+ configFile: false,
557
+ packageJson: false,
558
+ mcp: false,
559
+ promptable: false,
560
+ required: false,
561
+ default: false,
562
+ valueSpace: 'bounded'
563
+ },
564
+ projectName: {
565
+ cliOption: 'project-name',
566
+ envVar: null,
567
+ configFile: true,
568
+ packageJson: true,
569
+ mcp: false,
570
+ promptable: false,
571
+ required: true,
572
+ default: null,
573
+ valueSpace: 'bounded'
574
+ },
575
+ destinationDir: {
576
+ cliOption: 'project-dir',
577
+ envVar: null,
578
+ configFile: true,
579
+ packageJson: true,
580
+ mcp: false,
581
+ promptable: false,
582
+ required: true,
583
+ default: '.',
584
+ valueSpace: 'bounded'
585
+ },
586
+ buildTarget: {
587
+ cliOption: 'build-target',
588
+ envVar: 'ML_BUILD_TARGET',
589
+ configFile: true,
590
+ packageJson: false,
591
+ mcp: false,
592
+ promptable: true,
593
+ required: true,
594
+ default: 'codebuild',
595
+ valueSpace: 'bounded'
596
+ },
597
+ codebuildComputeType: {
598
+ cliOption: 'codebuild-compute-type',
599
+ envVar: 'ML_CODEBUILD_COMPUTE_TYPE',
600
+ configFile: true,
601
+ packageJson: false,
602
+ mcp: false,
603
+ promptable: true,
604
+ required: false,
605
+ default: 'BUILD_GENERAL1_MEDIUM',
606
+ valueSpace: 'bounded'
607
+ },
608
+ codebuildProjectName: {
609
+ cliOption: null,
610
+ envVar: null,
611
+ configFile: true,
612
+ packageJson: false,
613
+ mcp: false,
614
+ promptable: false,
615
+ required: false,
616
+ default: null,
617
+ valueSpace: 'bounded'
618
+ },
619
+ hfToken: {
620
+ cliOption: 'hf-token',
621
+ envVar: null,
622
+ configFile: true,
623
+ packageJson: false,
624
+ mcp: false,
625
+ promptable: true,
626
+ required: false,
627
+ default: null,
628
+ valueSpace: 'bounded'
629
+ },
630
+ deploymentTarget: {
631
+ cliOption: 'deployment-target',
632
+ envVar: 'ML_DEPLOYMENT_TARGET',
633
+ configFile: true,
634
+ packageJson: false,
635
+ mcp: false,
636
+ promptable: true,
637
+ required: true,
638
+ default: 'managed-inference',
639
+ valueSpace: 'bounded'
640
+ },
641
+ hyperPodCluster: {
642
+ cliOption: 'hyperpod-cluster',
643
+ envVar: null,
644
+ configFile: true,
645
+ packageJson: false,
646
+ mcp: true,
647
+ promptable: true,
648
+ required: false,
649
+ default: null,
650
+ valueSpace: 'unbounded'
651
+ },
652
+ hyperPodNamespace: {
653
+ cliOption: 'hyperpod-namespace',
654
+ envVar: null,
655
+ configFile: true,
656
+ packageJson: false,
657
+ mcp: false,
658
+ promptable: true,
659
+ required: false,
660
+ default: 'default',
661
+ valueSpace: 'bounded'
662
+ },
663
+ hyperPodReplicas: {
664
+ cliOption: 'hyperpod-replicas',
665
+ envVar: null,
666
+ configFile: true,
667
+ packageJson: false,
668
+ mcp: false,
669
+ promptable: true,
670
+ required: false,
671
+ default: 1,
672
+ valueSpace: 'bounded'
673
+ },
674
+ fsxVolumeHandle: {
675
+ cliOption: 'fsx-volume-handle',
676
+ envVar: null,
677
+ configFile: true,
678
+ packageJson: false,
679
+ mcp: false,
680
+ promptable: true,
681
+ required: false,
682
+ default: null,
683
+ valueSpace: 'bounded'
684
+ },
685
+ baseImage: {
686
+ cliOption: 'base-image',
687
+ envVar: null,
688
+ configFile: true,
689
+ packageJson: false,
690
+ mcp: true,
691
+ promptable: true,
692
+ required: false,
693
+ default: null,
694
+ valueSpace: 'unbounded'
695
+ },
696
+ asyncS3OutputPath: {
697
+ cliOption: 'async-s3-output-path',
698
+ envVar: 'ML_ASYNC_S3_OUTPUT_PATH',
699
+ configFile: true,
700
+ packageJson: false,
701
+ mcp: true,
702
+ promptable: true,
703
+ required: false,
704
+ default: null,
705
+ valueSpace: 'unbounded'
706
+ },
707
+ asyncSnsSuccessTopic: {
708
+ cliOption: 'async-sns-success-topic',
709
+ envVar: null,
710
+ configFile: true,
711
+ packageJson: false,
712
+ mcp: true,
713
+ promptable: true,
714
+ required: false,
715
+ default: null,
716
+ valueSpace: 'unbounded'
717
+ },
718
+ asyncSnsErrorTopic: {
719
+ cliOption: 'async-sns-error-topic',
720
+ envVar: null,
721
+ configFile: true,
722
+ packageJson: false,
723
+ mcp: true,
724
+ promptable: true,
725
+ required: false,
726
+ default: null,
727
+ valueSpace: 'unbounded'
728
+ },
729
+ asyncMaxConcurrentInvocations: {
730
+ cliOption: 'async-max-concurrent',
731
+ envVar: null,
732
+ configFile: true,
733
+ packageJson: false,
734
+ mcp: false,
735
+ promptable: true,
736
+ required: false,
737
+ default: 1,
738
+ valueSpace: 'bounded'
739
+ },
740
+ batchInputPath: {
741
+ cliOption: 'batch-input-path',
742
+ envVar: 'ML_BATCH_INPUT_PATH',
743
+ configFile: true,
744
+ packageJson: false,
745
+ mcp: true,
746
+ promptable: true,
747
+ required: false,
748
+ default: null,
749
+ valueSpace: 'unbounded'
750
+ },
751
+ batchOutputPath: {
752
+ cliOption: 'batch-output-path',
753
+ envVar: 'ML_BATCH_OUTPUT_PATH',
754
+ configFile: true,
755
+ packageJson: false,
756
+ mcp: true,
757
+ promptable: true,
758
+ required: false,
759
+ default: null,
760
+ valueSpace: 'unbounded'
761
+ },
762
+ batchInstanceCount: {
763
+ cliOption: 'batch-instance-count',
764
+ envVar: null,
765
+ configFile: true,
766
+ packageJson: false,
767
+ mcp: false,
768
+ promptable: true,
769
+ required: false,
770
+ default: 1,
771
+ valueSpace: 'bounded'
772
+ },
773
+ batchSplitType: {
774
+ cliOption: 'batch-split-type',
775
+ envVar: null,
776
+ configFile: true,
777
+ packageJson: false,
778
+ mcp: false,
779
+ promptable: true,
780
+ required: false,
781
+ default: 'Line',
782
+ valueSpace: 'bounded'
783
+ },
784
+ batchStrategy: {
785
+ cliOption: 'batch-strategy',
786
+ envVar: null,
787
+ configFile: true,
788
+ packageJson: false,
789
+ mcp: false,
790
+ promptable: true,
791
+ required: false,
792
+ default: 'MultiRecord',
793
+ valueSpace: 'bounded'
794
+ },
795
+ batchJoinSource: {
796
+ cliOption: 'batch-join-source',
797
+ envVar: null,
798
+ configFile: true,
799
+ packageJson: false,
800
+ mcp: false,
801
+ promptable: true,
802
+ required: false,
803
+ default: 'None',
804
+ valueSpace: 'bounded'
805
+ },
806
+ batchMaxConcurrentTransforms: {
807
+ cliOption: 'batch-max-concurrent',
808
+ envVar: null,
809
+ configFile: true,
810
+ packageJson: false,
811
+ mcp: false,
812
+ promptable: true,
813
+ required: false,
814
+ default: 1,
815
+ valueSpace: 'bounded'
816
+ },
817
+ batchMaxPayloadInMB: {
818
+ cliOption: 'batch-max-payload',
819
+ envVar: null,
820
+ configFile: true,
821
+ packageJson: false,
822
+ mcp: false,
823
+ promptable: true,
824
+ required: false,
825
+ default: 6,
826
+ valueSpace: 'bounded'
827
+ },
828
+ endpointInitialInstanceCount: {
829
+ cliOption: 'endpoint-initial-instance-count',
830
+ envVar: null,
831
+ configFile: true,
832
+ packageJson: false,
833
+ mcp: false,
834
+ promptable: false,
835
+ required: false,
836
+ default: 1,
837
+ valueSpace: 'bounded',
838
+ schemaValidated: true
839
+ },
840
+ endpointDataCapturePercent: {
841
+ cliOption: 'endpoint-data-capture-percent',
842
+ envVar: null,
843
+ configFile: true,
844
+ packageJson: false,
845
+ mcp: false,
846
+ promptable: false,
847
+ required: false,
848
+ default: 0,
849
+ valueSpace: 'bounded',
850
+ schemaValidated: true
851
+ },
852
+ endpointVariantName: {
853
+ cliOption: 'endpoint-variant-name',
854
+ envVar: null,
855
+ configFile: true,
856
+ packageJson: false,
857
+ mcp: false,
858
+ promptable: false,
859
+ required: false,
860
+ default: 'AllTraffic',
861
+ valueSpace: 'bounded',
862
+ schemaValidated: true
863
+ },
864
+ endpointVolumeSize: {
865
+ cliOption: 'endpoint-volume-size',
866
+ envVar: null,
867
+ configFile: true,
868
+ packageJson: false,
869
+ mcp: false,
870
+ promptable: false,
871
+ required: false,
872
+ default: null,
873
+ valueSpace: 'bounded',
874
+ schemaValidated: true
875
+ },
876
+ icCpuCount: {
877
+ cliOption: 'ic-cpu-count',
878
+ envVar: null,
879
+ configFile: true,
880
+ packageJson: false,
881
+ mcp: false,
882
+ promptable: false,
883
+ required: false,
884
+ default: null,
885
+ valueSpace: 'bounded',
886
+ schemaValidated: true
887
+ },
888
+ icMemorySize: {
889
+ cliOption: 'ic-memory-size',
890
+ envVar: null,
891
+ configFile: true,
892
+ packageJson: false,
893
+ mcp: false,
894
+ promptable: false,
895
+ required: false,
896
+ default: null,
897
+ valueSpace: 'bounded',
898
+ schemaValidated: true
899
+ },
900
+ icGpuCount: {
901
+ cliOption: 'ic-gpu-count',
902
+ envVar: null,
903
+ configFile: true,
904
+ packageJson: false,
905
+ mcp: false,
906
+ promptable: false,
907
+ required: false,
908
+ default: null,
909
+ valueSpace: 'bounded',
910
+ schemaValidated: true
911
+ },
912
+ icCopyCount: {
913
+ cliOption: 'ic-copy-count',
914
+ envVar: null,
915
+ configFile: true,
916
+ packageJson: false,
917
+ mcp: false,
918
+ promptable: false,
919
+ required: false,
920
+ default: 1,
921
+ valueSpace: 'bounded',
922
+ schemaValidated: true
923
+ },
924
+ icModelWeight: {
925
+ cliOption: 'ic-model-weight',
926
+ envVar: null,
927
+ configFile: true,
928
+ packageJson: false,
929
+ mcp: false,
930
+ promptable: false,
931
+ required: false,
932
+ default: 1.0,
933
+ valueSpace: 'bounded',
934
+ schemaValidated: true
935
+ }
936
+ };
937
+ }
938
+
939
+ /**
940
+ * Checks if a parameter source is supported according to the matrix
941
+ * @private
942
+ */
943
+ _isSourceSupported(parameter, source) {
944
+ const paramConfig = this.parameterMatrix[parameter];
945
+ if (!paramConfig) return false;
946
+
947
+ switch (source) {
948
+ case 'envVar':
949
+ return paramConfig.envVar !== null;
950
+ case 'configFile':
951
+ return paramConfig.configFile === true;
952
+ case 'packageJson':
953
+ return paramConfig.packageJson === true;
954
+ case 'cliOption':
955
+ return paramConfig.cliOption !== null;
956
+ default:
957
+ return false;
958
+ }
959
+ }
960
+
961
+ /**
962
+ * Parses a value according to its expected type
963
+ * @private
964
+ */
965
+ _parseValue(parameter, value) {
966
+ // Handle boolean parameters
967
+ if (parameter === 'includeSampleModel' || parameter === 'includeTesting' || parameter === 'skipPrompts') {
968
+ return value === true || value === 'true';
969
+ }
970
+
971
+ // Handle array parameters (if any in the future)
972
+ if (parameter === 'testTypes' && typeof value === 'string') {
973
+ return value.split(',').map(s => s.trim());
974
+ }
975
+
976
+ // Coerce numeric parameters from CLI strings to numbers.
977
+ // CLI always passes values as strings; we coerce when:
978
+ // 1. The default is already a number (e.g. endpointInitialInstanceCount default: 1)
979
+ // 2. The parameter is schema-validated, default is null, and the value is purely numeric
980
+ // (string defaults like 'AllTraffic' won't match since their default type is string)
981
+ const paramConfig = this.parameterMatrix[parameter]
982
+ if (paramConfig && typeof value === 'string') {
983
+ const hasNumericDefault = (typeof paramConfig.default === 'number')
984
+ const isNullDefaultNumericParam = (paramConfig.default === null &&
985
+ paramConfig.schemaValidated &&
986
+ /^-?\d+(\.\d+)?$/.test(value))
987
+ if (hasNumericDefault || isNullDefaultNumericParam) {
988
+ const num = Number(value)
989
+ if (!isNaN(num)) {
990
+ return num
991
+ }
992
+ }
993
+ }
994
+
995
+ // Handle string parameters
996
+ return value;
997
+ }
998
+ /**
999
+ * Generator defaults (lowest precedence before prompting)
1000
+ * @private
1001
+ */
1002
+ _getGeneratorDefaults() {
1003
+ const defaults = {};
1004
+
1005
+ // Apply defaults from parameter matrix
1006
+ Object.entries(this.parameterMatrix).forEach(([param, config]) => {
1007
+ if (config.default !== null) {
1008
+ defaults[param] = config.default;
1009
+ this._recordSource(param, config.default, 'default')
1010
+ } else {
1011
+ defaults[param] = null;
1012
+ }
1013
+ });
1014
+
1015
+ // Add legacy parameters that aren't in the matrix but are still used internally
1016
+ defaults.testTypes = null;
1017
+ defaults.includeTesting = true;
1018
+
1019
+ // Collection parameters for env vars (not in matrix, handled separately)
1020
+ defaults.modelEnvVars = {};
1021
+ defaults.serverEnvVars = {};
1022
+
1023
+ return defaults;
1024
+ }
1025
+
1026
+ /**
1027
+ * Load from bootstrap config (~/.ml-container-creator/config.json)
1028
+ * Reads the active profile and maps its keys to ConfigManager config keys.
1029
+ * Sits above generator defaults but below all other configuration sources.
1030
+ * @private
1031
+ */
1032
+ async _loadBootstrapConfig() {
1033
+ try {
1034
+ const bootstrapConfig = new BootstrapConfig();
1035
+ const activeProfile = bootstrapConfig.getActiveProfile();
1036
+ if (!activeProfile) {
1037
+ return;
1038
+ }
1039
+
1040
+ const profileConfig = activeProfile.config;
1041
+ const mapped = {};
1042
+
1043
+ if (profileConfig.roleArn) {
1044
+ mapped.awsRoleArn = profileConfig.roleArn;
1045
+ }
1046
+ if (profileConfig.awsRegion) {
1047
+ mapped.awsRegion = profileConfig.awsRegion;
1048
+ }
1049
+ if (profileConfig.awsProfile) {
1050
+ mapped.awsProfile = profileConfig.awsProfile;
1051
+ }
1052
+
1053
+ this._mergeConfig(mapped);
1054
+ } catch (error) {
1055
+ // Ignore errors — config file may not exist or may be malformed
1056
+ }
1057
+ }
1058
+
1059
+ /**
1060
+ * Load from package.json "ml-container-creator" section (filtered by matrix)
1061
+ * @private
1062
+ */
1063
+ async _loadPackageJsonConfig() {
1064
+ try {
1065
+ const packageJsonPath = path.resolve(process.cwd(), 'package.json');
1066
+ if (fs.existsSync(packageJsonPath)) {
1067
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
1068
+ const generatorConfig = packageJson['ml-container-creator'];
1069
+ if (generatorConfig) {
1070
+ // Filter config to only include parameters supported in package.json
1071
+ const filteredConfig = {};
1072
+ Object.entries(generatorConfig).forEach(([key, value]) => {
1073
+ if (this._isSourceSupported(key, 'packageJson')) {
1074
+ filteredConfig[key] = this._parseValue(key, value);
1075
+ }
1076
+ });
1077
+ this._mergeConfig(filteredConfig);
1078
+ }
1079
+ }
1080
+ } catch (error) {
1081
+ // Ignore errors - this is optional
1082
+ }
1083
+ }
1084
+
1085
+ /**
1086
+ * Load from config/mcp.json
1087
+ * @private
1088
+ */
1089
+ async _loadCustomConfigFile() {
1090
+ try {
1091
+ const configPath = path.join(GENERATOR_ROOT, 'config', 'mcp.json');
1092
+ if (fs.existsSync(configPath)) {
1093
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
1094
+ this._mergeConfig(config);
1095
+ }
1096
+ } catch (error) {
1097
+ // Ignore errors - this is optional
1098
+ }
1099
+ }
1100
+
1101
+ /**
1102
+ * Load from CLI --config file or --config-json inline string.
1103
+ *
1104
+ * --config-json accepts either:
1105
+ * 1. An inline JSON string: --config-json='{"deploymentConfig":"transformers-vllm"}'
1106
+ * 2. A path to a JSON file: --config-json=config.json
1107
+ *
1108
+ * When both --config and --config-json are provided, --config-json wins
1109
+ * (it is applied second, so its values override --config values).
1110
+ *
1111
+ * Also checks the ML_CONTAINER_CREATOR_CONFIG environment variable as a
1112
+ * fallback for --config.
1113
+ *
1114
+ * @private
1115
+ */
1116
+ async _loadCliConfigFile() {
1117
+ let configFile = this.options.config;
1118
+
1119
+ // Check environment variable if CLI option not provided
1120
+ if (!configFile && process.env.ML_CONTAINER_CREATOR_CONFIG) {
1121
+ configFile = process.env.ML_CONTAINER_CREATOR_CONFIG;
1122
+ }
1123
+
1124
+ if (configFile) {
1125
+ this._loadConfigFromFile(configFile);
1126
+ }
1127
+
1128
+ // --config-json: inline JSON string or path to a JSON file
1129
+ const configJson = this.options['config-json'];
1130
+ if (configJson) {
1131
+ this._loadConfigFromJson(configJson);
1132
+ }
1133
+ }
1134
+
1135
+ /**
1136
+ * Load configuration from a JSON file path.
1137
+ * @param {string} configFile - Path to the JSON config file
1138
+ * @private
1139
+ */
1140
+ _loadConfigFromFile(configFile) {
1141
+ try {
1142
+ const configPath = path.resolve(configFile);
1143
+ if (!fs.existsSync(configPath)) {
1144
+ throw new ConfigurationError(
1145
+ `Config file not found: ${configPath}`,
1146
+ 'configFile',
1147
+ 'cli'
1148
+ );
1149
+ }
1150
+
1151
+ // Check if file is readable
1152
+ try {
1153
+ fs.accessSync(configPath, fs.constants.R_OK);
1154
+ } catch (accessError) {
1155
+ throw new ConfigurationError(
1156
+ `Config file is not readable: ${configPath}`,
1157
+ 'configFile',
1158
+ 'cli'
1159
+ );
1160
+ }
1161
+
1162
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
1163
+ this._applyJsonConfig(config);
1164
+ } catch (error) {
1165
+ if (error instanceof ConfigurationError) {
1166
+ throw error;
1167
+ } else {
1168
+ throw new ConfigurationError(
1169
+ `Failed to load config file ${configFile}: ${error.message}`,
1170
+ 'configFile',
1171
+ 'cli'
1172
+ );
1173
+ }
1174
+ }
1175
+ }
1176
+
1177
+ /**
1178
+ * Load configuration from an inline JSON string or a JSON file path.
1179
+ * Tries to parse as JSON first; if that fails and the value looks like
1180
+ * a file path, reads and parses the file instead.
1181
+ *
1182
+ * @param {string} configJson - Inline JSON string or path to a JSON file
1183
+ * @private
1184
+ */
1185
+ _loadConfigFromJson(configJson) {
1186
+ let config;
1187
+ try {
1188
+ config = JSON.parse(configJson);
1189
+ } catch {
1190
+ // Not valid JSON — try as a file path
1191
+ try {
1192
+ const configPath = path.resolve(configJson);
1193
+ if (!fs.existsSync(configPath)) {
1194
+ throw new ConfigurationError(
1195
+ `--config-json value is not valid JSON and file not found: ${configJson}`,
1196
+ 'configJson',
1197
+ 'cli'
1198
+ );
1199
+ }
1200
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
1201
+ } catch (error) {
1202
+ if (error instanceof ConfigurationError) {
1203
+ throw error;
1204
+ }
1205
+ throw new ConfigurationError(
1206
+ `Failed to parse --config-json: ${error.message}`,
1207
+ 'configJson',
1208
+ 'cli'
1209
+ );
1210
+ }
1211
+ }
1212
+ this._applyJsonConfig(config);
1213
+ }
1214
+
1215
+ /**
1216
+ * Apply a parsed JSON config object, filtering to supported parameters.
1217
+ * Handles nested objects for endpoint, iC, and env var configuration.
1218
+ * @param {Object} config - Parsed JSON config object
1219
+ * @private
1220
+ */
1221
+ _applyJsonConfig(config) {
1222
+ const filteredConfig = {};
1223
+ Object.entries(config).forEach(([key, value]) => {
1224
+ // Handle nested endpointConfig object
1225
+ if (key === 'endpointConfig' && typeof value === 'object' && value !== null) {
1226
+ const endpointMapping = {
1227
+ initialInstanceCount: 'endpointInitialInstanceCount',
1228
+ dataCapturePercent: 'endpointDataCapturePercent',
1229
+ variantName: 'endpointVariantName',
1230
+ volumeSize: 'endpointVolumeSize'
1231
+ }
1232
+ Object.entries(value).forEach(([nestedKey, nestedValue]) => {
1233
+ const flatKey = endpointMapping[nestedKey]
1234
+ if (flatKey && this._isSourceSupported(flatKey, 'configFile')) {
1235
+ filteredConfig[flatKey] = nestedValue
1236
+ this._recordSource(flatKey, nestedValue, 'config-file')
1237
+ }
1238
+ })
1239
+ return
1240
+ }
1241
+
1242
+ // Handle nested icConfig object
1243
+ if (key === 'icConfig' && typeof value === 'object' && value !== null) {
1244
+ const icMapping = {
1245
+ cpuCount: 'icCpuCount',
1246
+ memorySize: 'icMemorySize',
1247
+ gpuCount: 'icGpuCount',
1248
+ copyCount: 'icCopyCount',
1249
+ modelWeight: 'icModelWeight'
1250
+ }
1251
+ Object.entries(value).forEach(([nestedKey, nestedValue]) => {
1252
+ const flatKey = icMapping[nestedKey]
1253
+ if (flatKey && this._isSourceSupported(flatKey, 'configFile')) {
1254
+ filteredConfig[flatKey] = nestedValue
1255
+ this._recordSource(flatKey, nestedValue, 'config-file')
1256
+ }
1257
+ })
1258
+ return
1259
+ }
1260
+
1261
+ // Handle modelEnvVars object (merge with CLI, CLI takes precedence)
1262
+ if (key === 'modelEnvVars' && typeof value === 'object' && value !== null) {
1263
+ if (!this.config.modelEnvVars) {
1264
+ this.config.modelEnvVars = {}
1265
+ }
1266
+ // Only set keys not already provided by CLI (CLI has higher precedence)
1267
+ const cliModelEnvVars = (this.explicitConfig && this.explicitConfig.modelEnvVars) || {}
1268
+ Object.entries(value).forEach(([envKey, envValue]) => {
1269
+ if (!(envKey in cliModelEnvVars)) {
1270
+ this.config.modelEnvVars[envKey] = envValue
1271
+ this._recordSource(`modelEnvVars.${envKey}`, envValue, 'config-file')
1272
+ }
1273
+ })
1274
+ return
1275
+ }
1276
+
1277
+ // Handle serverEnvVars object (merge with CLI, CLI takes precedence)
1278
+ if (key === 'serverEnvVars' && typeof value === 'object' && value !== null) {
1279
+ if (!this.config.serverEnvVars) {
1280
+ this.config.serverEnvVars = {}
1281
+ }
1282
+ // Only set keys not already provided by CLI (CLI has higher precedence)
1283
+ const cliServerEnvVars = (this.explicitConfig && this.explicitConfig.serverEnvVars) || {}
1284
+ Object.entries(value).forEach(([envKey, envValue]) => {
1285
+ if (!(envKey in cliServerEnvVars)) {
1286
+ this.config.serverEnvVars[envKey] = envValue
1287
+ this._recordSource(`serverEnvVars.${envKey}`, envValue, 'config-file')
1288
+ }
1289
+ })
1290
+ return
1291
+ }
1292
+
1293
+ if (this._isSourceSupported(key, 'configFile')) {
1294
+ filteredConfig[key] = this._parseValue(key, value);
1295
+ this._recordSource(key, this._parseValue(key, value), 'config-file')
1296
+ }
1297
+ });
1298
+ this._mergeConfig(filteredConfig);
1299
+ }
1300
+
1301
+ /**
1302
+ * Load from environment variables (filtered by matrix)
1303
+ * @private
1304
+ */
1305
+ async _loadEnvironmentVariables() {
1306
+ // Build environment variable mapping from parameter matrix
1307
+ const envMapping = {};
1308
+ Object.entries(this.parameterMatrix).forEach(([param, config]) => {
1309
+ if (config.envVar) {
1310
+ envMapping[config.envVar] = { param, ambient: config.ambientEnvVar === true };
1311
+ }
1312
+ });
1313
+
1314
+ Object.entries(envMapping).forEach(([envVar, { param: configKey, ambient }]) => {
1315
+ const value = process.env[envVar];
1316
+ if (value !== undefined && value !== '' && this._isSourceSupported(configKey, 'envVar')) {
1317
+ this.config[configKey] = this._parseValue(configKey, value);
1318
+ this._recordSource(configKey, this._parseValue(configKey, value), 'env-var')
1319
+ // Track as explicit configuration — unless the env var is ambient
1320
+ // (e.g. AWS_REGION is commonly set in shells as a default, not an override)
1321
+ if (!ambient) {
1322
+ if (!this.explicitConfig) {
1323
+ this.explicitConfig = {};
1324
+ }
1325
+ this.explicitConfig[configKey] = this._parseValue(configKey, value);
1326
+ }
1327
+ }
1328
+ });
1329
+ }
1330
+
1331
+ /**
1332
+ * Load from CLI arguments (positional)
1333
+ * @private
1334
+ */
1335
+ async _loadCliArguments() {
1336
+ // First positional argument is project name
1337
+ if (this.args && this.args.length > 0) {
1338
+ this.config.projectName = this.args[0];
1339
+ // Track as explicit configuration
1340
+ if (!this.explicitConfig) {
1341
+ this.explicitConfig = {};
1342
+ }
1343
+ this.explicitConfig.projectName = this.args[0];
1344
+ // Track that project name came from positional argument (for subdirectory creation)
1345
+ this.projectNameFromArgument = true;
1346
+ }
1347
+ }
1348
+
1349
+ /**
1350
+ * Load from CLI options (highest precedence, filtered by matrix)
1351
+ * @private
1352
+ */
1353
+ async _loadCliOptions() {
1354
+ const options = this.options;
1355
+
1356
+ // Build CLI option mapping from parameter matrix
1357
+ Object.entries(this.parameterMatrix).forEach(([param, config]) => {
1358
+ if (config.cliOption && options[config.cliOption] !== undefined) {
1359
+ this.config[param] = this._parseValue(param, options[config.cliOption]);
1360
+ this._recordSource(param, this._parseValue(param, options[config.cliOption]), 'cli')
1361
+ // Track as explicit configuration
1362
+ if (!this.explicitConfig) {
1363
+ this.explicitConfig = {};
1364
+ }
1365
+ this.explicitConfig[param] = this._parseValue(param, options[config.cliOption]);
1366
+ }
1367
+ });
1368
+
1369
+ // Parse --model-env KEY=VALUE pairs
1370
+ this._parseEnvVarOptions('model-env', 'modelEnvVars');
1371
+
1372
+ // Parse --server-env KEY=VALUE pairs
1373
+ this._parseEnvVarOptions('server-env', 'serverEnvVars');
1374
+ }
1375
+
1376
+ /**
1377
+ * Parse --model-env or --server-env CLI options into env var collections.
1378
+ * Supports both array (multiple flags) and single string values.
1379
+ * Performs eager format validation at parse time.
1380
+ * @param {string} optionName - CLI option name (e.g., 'model-env')
1381
+ * @param {string} configKey - Config key to store results (e.g., 'modelEnvVars')
1382
+ * @private
1383
+ */
1384
+ _parseEnvVarOptions(optionName, configKey) {
1385
+ const rawValue = this.options[optionName];
1386
+ if (rawValue === undefined || rawValue === null) {
1387
+ return;
1388
+ }
1389
+
1390
+ // Normalize to array (may receive a single string or an array)
1391
+ const values = Array.isArray(rawValue) ? rawValue : [rawValue];
1392
+
1393
+ // Initialize collection if not already present
1394
+ if (!this.config[configKey] || typeof this.config[configKey] !== 'object') {
1395
+ this.config[configKey] = {};
1396
+ }
1397
+
1398
+ for (const entry of values) {
1399
+ if (typeof entry !== 'string' || entry.trim() === '') {
1400
+ continue;
1401
+ }
1402
+ const { key, value } = parseKeyValue(entry);
1403
+ this.config[configKey][key] = value;
1404
+ this._recordSource(`${configKey}.${key}`, value, 'cli')
1405
+ }
1406
+
1407
+ // Track as explicit configuration
1408
+ if (Object.keys(this.config[configKey]).length > 0) {
1409
+ if (!this.explicitConfig) {
1410
+ this.explicitConfig = {};
1411
+ }
1412
+ this.explicitConfig[configKey] = { ...this.config[configKey] };
1413
+ }
1414
+ }
1415
+
1416
+ /**
1417
+ * Query configured MCP servers for unbounded parameter values.
1418
+ * Reads mcpServers from config/mcp.json, spawns each one,
1419
+ * and stores results in mcpSources/mcpChoices.
1420
+ * @private
1421
+ */
1422
+ async _queryMcpServers() {
1423
+ // No-op: MCP queries now happen on-demand during prompting
1424
+ // via queryMcpServer(). This method is kept for backward compatibility.
1425
+ }
1426
+
1427
+ /**
1428
+ * Query a single named MCP server on-demand with the given context.
1429
+ * Stores results in mcpSources/mcpChoices and returns the result.
1430
+ * @param {string} serverName - Name of the server in mcpServers config
1431
+ * @param {object} context - Context to pass to the MCP tool (e.g. { regionSearch: 'europe' })
1432
+ * @returns {Promise<{ values: object, choices: object } | null>}
1433
+ */
1434
+ async queryMcpServer(serverName, context = {}) {
1435
+ let mcpServerConfigs;
1436
+ try {
1437
+ const configPath = path.join(GENERATOR_ROOT, 'config', 'mcp.json');
1438
+ if (!fs.existsSync(configPath)) return null;
1439
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
1440
+ mcpServerConfigs = config.mcpServers;
1441
+ } catch {
1442
+ return null;
1443
+ }
1444
+
1445
+ if (!mcpServerConfigs || !mcpServerConfigs[serverName]) return null;
1446
+
1447
+ const smart = this.options.smart === true;
1448
+ const discover = this.options.discover === true;
1449
+ const serverConfig = mcpServerConfigs[serverName];
1450
+
1451
+ // Build a custom McpClient that passes context through
1452
+ const client = new McpClient(serverConfig, {
1453
+ timeout: 15000,
1454
+ parameterMatrix: this.parameterMatrix,
1455
+ smart,
1456
+ discover
1457
+ });
1458
+
1459
+ // Override the _buildContext to merge our search context
1460
+ const origBuildContext = client._buildContext.bind(client);
1461
+ client._buildContext = () => ({ ...origBuildContext(), ...context });
1462
+
1463
+ try {
1464
+ const result = await client.query();
1465
+ await client.close();
1466
+
1467
+ if (!result) {
1468
+ const diag = client.getDiagnosticMessage();
1469
+ if (diag) console.log(` ⚠️ ${serverName}: ${diag}`);
1470
+ return null;
1471
+ }
1472
+
1473
+ // Store values
1474
+ for (const [param, value] of Object.entries(result.values || {})) {
1475
+ const paramConfig = this.parameterMatrix[param];
1476
+ if (paramConfig && paramConfig.valueSpace === 'unbounded' && paramConfig.mcp === true) {
1477
+ this.mcpSources[param] = {
1478
+ server: serverName,
1479
+ value,
1480
+ timestamp: new Date().toISOString()
1481
+ };
1482
+ }
1483
+ }
1484
+
1485
+ // Store choices
1486
+ for (const [param, choices] of Object.entries(result.choices || {})) {
1487
+ const paramConfig = this.parameterMatrix[param];
1488
+ if (paramConfig && paramConfig.valueSpace === 'unbounded' && paramConfig.mcp === true && Array.isArray(choices)) {
1489
+ this.mcpChoices[param] = choices;
1490
+ }
1491
+ }
1492
+
1493
+ return result;
1494
+ } catch (err) {
1495
+ await client.close().catch(() => {});
1496
+ console.log(` ⚠️ ${serverName}: ${err.message}`);
1497
+ return null;
1498
+ }
1499
+ }
1500
+
1501
+ /**
1502
+ * Get the names of configured MCP servers.
1503
+ * @returns {string[]}
1504
+ */
1505
+ getMcpServerNames() {
1506
+ try {
1507
+ const configPath = path.join(GENERATOR_ROOT, 'config', 'mcp.json');
1508
+ if (!fs.existsSync(configPath)) return [];
1509
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
1510
+ return Object.keys(config.mcpServers || {});
1511
+ } catch {
1512
+ return [];
1513
+ }
1514
+ }
1515
+
1516
+ /**
1517
+ * Merges configuration object into current config
1518
+ * @private
1519
+ */
1520
+ _mergeConfig(newConfig) {
1521
+ Object.keys(newConfig).forEach(key => {
1522
+ if (newConfig[key] !== undefined && newConfig[key] !== null) {
1523
+ this.config[key] = newConfig[key];
1524
+ // Track as explicit configuration (not default)
1525
+ if (!this.explicitConfig) {
1526
+ this.explicitConfig = {};
1527
+ }
1528
+ this.explicitConfig[key] = newConfig[key];
1529
+ }
1530
+ });
1531
+ }
1532
+
1533
+ /**
1534
+ * Records a source manifest entry for a parameter.
1535
+ * If the parameter already has an entry, it is replaced (higher-precedence wins).
1536
+ * @param {string} param - Parameter name
1537
+ * @param {*} value - Parameter value
1538
+ * @param {string} source - Source identifier (cli, config-file, registry, env-var, default)
1539
+ * @private
1540
+ */
1541
+ _recordSource(param, value, source) {
1542
+ const existingIndex = this._sourceManifest.findIndex(entry => entry.param === param)
1543
+ if (existingIndex >= 0) {
1544
+ this._sourceManifest[existingIndex] = { param, value, source }
1545
+ } else {
1546
+ this._sourceManifest.push({ param, value, source })
1547
+ }
1548
+ }
1549
+
1550
+ /**
1551
+ * Checks if we have enough configuration to skip prompts
1552
+ * Non-promptable parameters are not required for this check since they can be auto-generated
1553
+ * @private
1554
+ */
1555
+ _hasCompleteConfiguration() {
1556
+ // Only check promptable required parameters
1557
+ const promptableRequired = Object.entries(this.parameterMatrix)
1558
+ .filter(([_param, config]) => config.required && config.promptable)
1559
+ .map(([param]) => param);
1560
+
1561
+ // Special case: modelFormat is not required for transformers/triton/diffusors architectures
1562
+ const requiredForConfig = promptableRequired.filter(param => {
1563
+ if (param === 'modelFormat') {
1564
+ const architecture = this.config.architecture
1565
+ if (architecture === 'transformers' || architecture === 'triton' || architecture === 'diffusors') {
1566
+ return false
1567
+ }
1568
+ }
1569
+ return true;
1570
+ });
1571
+
1572
+ return requiredForConfig.every(key =>
1573
+ this.config[key] !== undefined && this.config[key] !== null
1574
+ );
1575
+ }
1576
+
1577
+ /**
1578
+ * Validates the current configuration against the parameter matrix
1579
+ * Only reports errors for parameters that cannot be resolved through prompting or auto-generation
1580
+ * @returns {Array} Array of validation errors
1581
+ */
1582
+ validateConfiguration() {
1583
+ const errors = [];
1584
+
1585
+ // Old-format deployment-config migration messages
1586
+ const oldFormatMigration = {
1587
+ 'sklearn-flask': 'Use --deployment-config=http-flask --engine=sklearn instead',
1588
+ 'sklearn-fastapi': 'Use --deployment-config=http-fastapi --engine=sklearn instead',
1589
+ 'xgboost-flask': 'Use --deployment-config=http-flask --engine=xgboost instead',
1590
+ 'xgboost-fastapi': 'Use --deployment-config=http-fastapi --engine=xgboost instead',
1591
+ 'tensorflow-flask': 'Use --deployment-config=http-flask --engine=tensorflow instead',
1592
+ 'tensorflow-fastapi': 'Use --deployment-config=http-fastapi --engine=tensorflow instead'
1593
+ }
1594
+
1595
+ // Validate deployment-config
1596
+ if (this.config.deploymentConfig) {
1597
+ const migrationMsg = oldFormatMigration[this.config.deploymentConfig]
1598
+ if (migrationMsg) {
1599
+ errors.push(`Unsupported deployment-config: ${this.config.deploymentConfig}. This value has been replaced. ${migrationMsg}`)
1600
+ } else if (!this.deploymentConfigResolver.isValid(this.config.deploymentConfig)) {
1601
+ const valid = this.deploymentConfigResolver.getAllConfigs().join(', ')
1602
+ errors.push(`Unsupported deployment-config: ${this.config.deploymentConfig}. Valid configs: ${valid}`)
1603
+ }
1604
+ }
1605
+
1606
+ // Validate engine (only valid for http architecture)
1607
+ if (this.config.engine) {
1608
+ const validEngines = ['sklearn', 'xgboost', 'tensorflow']
1609
+ if (!validEngines.includes(this.config.engine)) {
1610
+ errors.push(`Unsupported engine: ${this.config.engine}. Supported: ${validEngines.join(', ')}`)
1611
+ }
1612
+ }
1613
+
1614
+ // Validate model format based on architecture/engine
1615
+ if (this.config.modelFormat && this.config.deploymentConfig) {
1616
+ try {
1617
+ const parts = this.deploymentConfigResolver.decompose(this.config.deploymentConfig)
1618
+ if (parts.architecture === 'http') {
1619
+ const engine = this.config.engine || parts.engine
1620
+ if (engine) {
1621
+ const supportedOptions = this._getSupportedOptions()
1622
+ const validFormats = supportedOptions.modelFormats[engine] || []
1623
+ if (validFormats.length > 0 && !validFormats.includes(this.config.modelFormat)) {
1624
+ errors.push(`Unsupported model format '${this.config.modelFormat}' for engine '${engine}'. Supported: ${validFormats.join(', ')}`)
1625
+ }
1626
+ }
1627
+ }
1628
+ } catch {
1629
+ // deploymentConfig already flagged as invalid above
1630
+ }
1631
+ }
1632
+
1633
+ // Validate AWS Role ARN format if provided
1634
+ if (this.config.awsRoleArn) {
1635
+ try {
1636
+ this._isValidArn(this.config.awsRoleArn);
1637
+ } catch (error) {
1638
+ if (error instanceof ValidationError) {
1639
+ errors.push(error.message);
1640
+ } else {
1641
+ errors.push(`Invalid AWS Role ARN format: ${this.config.awsRoleArn}. Expected format: arn:aws:iam::123456789012:role/RoleName`);
1642
+ }
1643
+ }
1644
+ }
1645
+
1646
+ // Validate build target (renamed from deployTarget)
1647
+ const buildTarget = this.config.buildTarget || this.config.deployTarget;
1648
+ if (buildTarget && !this._getSupportedOptions().buildTargets.includes(buildTarget)) {
1649
+ errors.push(`Unsupported build target: ${buildTarget}. Supported targets: ${this._getSupportedOptions().buildTargets.join(', ')}`);
1650
+ }
1651
+
1652
+ // Validate CodeBuild compute type
1653
+ if (this.config.codebuildComputeType && !this._getSupportedOptions().codebuildComputeTypes.includes(this.config.codebuildComputeType)) {
1654
+ errors.push(`Unsupported CodeBuild compute type: ${this.config.codebuildComputeType}. Supported types: ${this._getSupportedOptions().codebuildComputeTypes.join(', ')}`);
1655
+ }
1656
+
1657
+ // Validate CodeBuild project name format
1658
+ if (this.config.codebuildProjectName) {
1659
+ const projectNamePattern = /^[a-zA-Z0-9][a-zA-Z0-9\-_]{1,254}$/;
1660
+ if (!projectNamePattern.test(this.config.codebuildProjectName)) {
1661
+ errors.push(`Invalid CodeBuild project name: ${this.config.codebuildProjectName}. Project names must be 2-255 characters, start with a letter or number, and contain only letters, numbers, hyphens, and underscores.`);
1662
+ }
1663
+ }
1664
+
1665
+ // Only validate required parameters if we're skipping prompts
1666
+ // If prompts are available, missing parameters can be collected later
1667
+ if (this.skipPrompts) {
1668
+ Object.entries(this.parameterMatrix).forEach(([param, config]) => {
1669
+ if (config.required &&
1670
+ (this.config[param] === null || this.config[param] === undefined)) {
1671
+
1672
+ // Special case: modelFormat is not required for transformers/triton/diffusors
1673
+ if (param === 'modelFormat') {
1674
+ try {
1675
+ const parts = this.deploymentConfigResolver.decompose(this.config.deploymentConfig)
1676
+ if (parts.architecture === 'transformers' || parts.architecture === 'triton' || parts.architecture === 'diffusors') {
1677
+ return
1678
+ }
1679
+ } catch {
1680
+ // If deploymentConfig is invalid, skip this check
1681
+ return
1682
+ }
1683
+ }
1684
+
1685
+ // Only error for promptable required parameters that have no default and can't be auto-generated
1686
+ if (config.promptable && config.default === null && !this._canAutoGenerate(param)) {
1687
+ errors.push(`Required parameter '${param}' is missing and prompts are disabled`);
1688
+ }
1689
+ }
1690
+ });
1691
+
1692
+ // Validate that modelName is provided for diffusors architecture
1693
+ if (this.config.deploymentConfig) {
1694
+ try {
1695
+ const parts = this.deploymentConfigResolver.decompose(this.config.deploymentConfig)
1696
+ if (parts.architecture === 'diffusors') {
1697
+ const explicitModelName = this.explicitConfig && this.explicitConfig.modelName
1698
+ if (!explicitModelName) {
1699
+ errors.push('Model name is required for diffusors architecture. Use --model-name to specify a HuggingFace diffusion model.')
1700
+ }
1701
+ }
1702
+ } catch {
1703
+ // deploymentConfig already flagged as invalid above
1704
+ }
1705
+ }
1706
+ }
1707
+
1708
+ // Validate schema-validated parameters (endpoint, iC)
1709
+ Object.entries(this.parameterMatrix).forEach(([param, config]) => {
1710
+ if (config.schemaValidated && this.config[param] !== null && this.config[param] !== undefined) {
1711
+ const result = this.schemaValidator.validate(param, this.config[param], this.config.deploymentTarget)
1712
+ if (!result.valid) {
1713
+ errors.push(result.error)
1714
+ }
1715
+ }
1716
+ })
1717
+
1718
+ return errors;
1719
+ }
1720
+
1721
+ /**
1722
+ * Validates required parameters before file generation
1723
+ * This is called after all configuration sources have been processed and prompting is complete
1724
+ * @param {Object} finalConfig - The complete configuration object
1725
+ * @returns {Array} Array of validation errors for missing required parameters
1726
+ */
1727
+ validateRequiredParameters(finalConfig) {
1728
+ const errors = [];
1729
+
1730
+ // First, validate individual parameter values
1731
+ Object.entries(finalConfig).forEach(([param, value]) => {
1732
+ if (value !== null && value !== undefined && value !== '') {
1733
+ try {
1734
+ this._validateParameterValue(param, value, finalConfig);
1735
+ } catch (error) {
1736
+ if (error instanceof ValidationError) {
1737
+ errors.push(error.message);
1738
+ } else {
1739
+ errors.push(`Invalid value for parameter '${param}': ${error.message}`);
1740
+ }
1741
+ }
1742
+ }
1743
+ });
1744
+
1745
+ // Then, validate required parameters are present
1746
+ Object.entries(this.parameterMatrix).forEach(([param, config]) => {
1747
+ if (config.required) {
1748
+ const value = finalConfig[param];
1749
+ const isEmpty = value === null || value === undefined || value === '';
1750
+
1751
+ // Special case: modelFormat is not required for transformers/triton/diffusors
1752
+ if (param === 'modelFormat' && (finalConfig.architecture === 'transformers' || finalConfig.architecture === 'triton' || finalConfig.architecture === 'diffusors')) {
1753
+ return; // Skip validation
1754
+ }
1755
+
1756
+ // Special case: instanceType is not required for hyperpod-eks
1757
+ // when not provided (backward compatibility) — but it IS prompted now
1758
+ // so it should normally be present
1759
+ if (param === 'instanceType' && finalConfig.deploymentTarget === 'hyperpod-eks' && !finalConfig.instanceType) {
1760
+ return; // Skip validation only if truly missing for backward compat
1761
+ }
1762
+
1763
+ if (isEmpty) {
1764
+ if (config.promptable) {
1765
+ // Promptable required parameter is missing - this should not happen after prompting
1766
+ errors.push(`Required parameter '${param}' is missing. This parameter is required for ${finalConfig.architecture || 'the selected'} architecture.`);
1767
+ } else {
1768
+ // Non-promptable required parameter is missing - this is a configuration error
1769
+ errors.push(`Required non-promptable parameter '${param}' is missing. This parameter must be provided through CLI options, environment variables, or configuration files.`);
1770
+ }
1771
+ }
1772
+ }
1773
+ });
1774
+
1775
+ // Finally, validate parameter combinations and dependencies
1776
+ const combinationErrors = this._validateParameterCombinations(finalConfig);
1777
+ errors.push(...combinationErrors);
1778
+
1779
+ return errors;
1780
+ }
1781
+
1782
+ /**
1783
+ * Validates parameter combinations and dependencies
1784
+ * @param {Object} config - The configuration object to validate
1785
+ * @returns {Array} Array of validation errors for invalid combinations
1786
+ * @private
1787
+ */
1788
+ _validateParameterCombinations(config) {
1789
+ const errors = [];
1790
+
1791
+ // Additional combination validations that aren't covered by individual parameter validation
1792
+ // For example, complex business rules that involve multiple parameters
1793
+
1794
+ // Validate that transformers architecture has sample model disabled
1795
+ if (config.architecture === 'transformers' && config.includeSampleModel === true) {
1796
+ errors.push(`Architecture '${config.architecture}' does not support sample models. The 'includeSampleModel' parameter will be automatically set to false.`);
1797
+ }
1798
+ // Validate that diffusors architecture has sample model disabled
1799
+ if (config.architecture === 'diffusors' && config.includeSampleModel === true) {
1800
+ errors.push(`Architecture '${config.architecture}' does not support sample models. The 'includeSampleModel' parameter will be automatically set to false.`);
1801
+ }
1802
+ // Validate that ineligible Triton backends have sample model disabled
1803
+ if (config.architecture === 'triton' && config.includeSampleModel === true) {
1804
+ const backendMeta = tritonBackends[config.backend];
1805
+ if (!backendMeta || !backendMeta.supportsSampleModel) {
1806
+ errors.push(`Triton backend '${config.backend}' does not support sample models. The 'includeSampleModel' parameter will be automatically set to false.`);
1807
+ }
1808
+ }
1809
+
1810
+ return errors;
1811
+ }
1812
+
1813
+ /**
1814
+ * Checks if a parameter can be auto-generated when missing
1815
+ * @param {string} param - Parameter name
1816
+ * @returns {boolean} True if parameter can be auto-generated
1817
+ * @private
1818
+ */
1819
+ _canAutoGenerate(param) {
1820
+ // Parameters that can be auto-generated even when missing
1821
+ const autoGeneratable = [
1822
+ 'modelFormat', // Can be inferred from engine
1823
+ 'includeSampleModel', // Has default
1824
+ 'includeTesting', // Has default
1825
+ 'instanceType' // Has default
1826
+ ];
1827
+
1828
+ return autoGeneratable.includes(param);
1829
+ }
1830
+
1831
+ /**
1832
+ * Generates a project name based on framework
1833
+ * @param {string} framework - The ML framework
1834
+ * @returns {string} Generated project name
1835
+ * @private
1836
+ */
1837
+ _generateProjectName(architecture) {
1838
+ const adjectives = [
1839
+ 'smart', 'fast', 'clever', 'bright', 'swift', 'agile', 'sharp', 'quick',
1840
+ 'wise', 'keen', 'bold', 'sleek', 'neat', 'cool', 'fresh', 'prime'
1841
+ ];
1842
+
1843
+ const architectureNames = {
1844
+ 'http': ['http', 'api', 'serve'],
1845
+ 'transformers': ['llm', 'transformer', 'gpt', 'bert', 'ai'],
1846
+ 'triton': ['triton', 'inference', 'nvidia'],
1847
+ 'diffusors': ['diffusion', 'image', 'vllm-omni']
1848
+ };
1849
+
1850
+ const suffixes = [
1851
+ 'model', 'predictor', 'classifier', 'engine', 'service', 'api',
1852
+ 'container', 'deployment', 'inference', 'ml', 'ai', 'bot'
1853
+ ];
1854
+
1855
+ // Get random elements
1856
+ const adjective = adjectives[Math.floor(Math.random() * adjectives.length)];
1857
+ const archName = architectureNames[architecture] ?
1858
+ architectureNames[architecture][Math.floor(Math.random() * architectureNames[architecture].length)] :
1859
+ 'ml';
1860
+ const suffix = suffixes[Math.floor(Math.random() * suffixes.length)];
1861
+
1862
+ return `${adjective}-${archName}-${suffix}`;
1863
+ }
1864
+
1865
+ /**
1866
+ * Generates a descriptive CodeBuild project name
1867
+ * @param {string} projectName - The main project name
1868
+ * @param {string} framework - The ML framework being used
1869
+ * @returns {string} Generated CodeBuild project name
1870
+ * @private
1871
+ */
1872
+ _generateCodeBuildProjectName(projectName, architecture) {
1873
+ const architectureMap = {
1874
+ 'http': 'http',
1875
+ 'transformers': 'llm',
1876
+ 'triton': 'triton',
1877
+ 'diffusors': 'diffusion'
1878
+ };
1879
+
1880
+ const archName = architectureMap[architecture] || 'ml';
1881
+ const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, ''); // YYYYMMDD
1882
+
1883
+ // Create a descriptive name that indicates it's a build project
1884
+ const buildProjectName = `${projectName}-${archName}-build-${timestamp}`;
1885
+
1886
+ // Ensure it meets AWS CodeBuild naming requirements (2-255 chars, alphanumeric + hyphens/underscores)
1887
+ return buildProjectName
1888
+ .toLowerCase()
1889
+ .replace(/[^a-z0-9\-_]/g, '-') // Replace invalid chars with hyphens
1890
+ .replace(/-+/g, '-') // Replace multiple hyphens with single
1891
+ .replace(/^-|-$/g, '') // Remove leading/trailing hyphens
1892
+ .slice(0, 255); // Ensure max length
1893
+ }
1894
+
1895
+ /**
1896
+ * Validates a single parameter value
1897
+ * @param {string} parameter - Parameter name
1898
+ * @param {*} value - Parameter value
1899
+ * @param {Object} context - Additional context (e.g., other parameter values)
1900
+ * @throws {ValidationError} If parameter value is invalid
1901
+ * @private
1902
+ */
1903
+ _validateParameterValue(parameter, value, context = {}) {
1904
+ const supportedOptions = this._getSupportedOptions();
1905
+
1906
+ switch (parameter) {
1907
+ case 'deploymentConfig':
1908
+ if (value) {
1909
+ // Check for old-format configs with migration messages
1910
+ const oldFormatMigration = {
1911
+ 'sklearn-flask': 'Use --deployment-config=http-flask --engine=sklearn instead',
1912
+ 'sklearn-fastapi': 'Use --deployment-config=http-fastapi --engine=sklearn instead',
1913
+ 'xgboost-flask': 'Use --deployment-config=http-flask --engine=xgboost instead',
1914
+ 'xgboost-fastapi': 'Use --deployment-config=http-fastapi --engine=xgboost instead',
1915
+ 'tensorflow-flask': 'Use --deployment-config=http-flask --engine=tensorflow instead',
1916
+ 'tensorflow-fastapi': 'Use --deployment-config=http-fastapi --engine=tensorflow instead'
1917
+ }
1918
+ const migrationMsg = oldFormatMigration[value]
1919
+ if (migrationMsg) {
1920
+ throw new ValidationError(
1921
+ `Unsupported deployment-config: ${value}. This value has been replaced. ${migrationMsg}`,
1922
+ parameter,
1923
+ value
1924
+ )
1925
+ }
1926
+ if (!this.deploymentConfigResolver.isValid(value)) {
1927
+ const valid = this.deploymentConfigResolver.getAllConfigs().join(', ')
1928
+ throw new ValidationError(
1929
+ `Unsupported deployment-config: ${value}. Valid configs: ${valid}`,
1930
+ parameter,
1931
+ value
1932
+ )
1933
+ }
1934
+ }
1935
+ break
1936
+
1937
+ case 'engine':
1938
+ if (value) {
1939
+ const validEngines = ['sklearn', 'xgboost', 'tensorflow']
1940
+ if (!validEngines.includes(value)) {
1941
+ throw new ValidationError(
1942
+ `Unsupported engine: ${value}. Supported: ${validEngines.join(', ')}`,
1943
+ parameter,
1944
+ value
1945
+ )
1946
+ }
1947
+ }
1948
+ break
1949
+
1950
+ case 'modelFormat':
1951
+ if (value && context.architecture === 'http' && context.engine) {
1952
+ const validFormats = supportedOptions.modelFormats[context.engine] || [];
1953
+ if (validFormats.length > 0 && !validFormats.includes(value)) {
1954
+ throw new ValidationError(
1955
+ `Model format '${value}' is not compatible with engine '${context.engine}'. Compatible formats: ${validFormats.join(', ')}`,
1956
+ parameter,
1957
+ value
1958
+ );
1959
+ }
1960
+ }
1961
+ break;
1962
+
1963
+ case 'instanceType':
1964
+ if (value) {
1965
+ // Validate AWS SageMaker instance type format
1966
+ const instancePattern = /^ml\.[a-z0-9]+\.(nano|micro|small|medium|large|xlarge|[0-9]+xlarge)$/;
1967
+ if (!instancePattern.test(value)) {
1968
+ throw new ValidationError(
1969
+ `Invalid instance type format: ${value}. Expected format: ml.{family}.{size} (e.g., ml.m5.large, ml.g4dn.xlarge)`,
1970
+ parameter,
1971
+ value
1972
+ );
1973
+ }
1974
+ // Warn about CPU instances for transformers/triton (but don't block)
1975
+ if (context.architecture === 'transformers' || context.architecture === 'triton') {
1976
+ const cpuFamilies = ['t2', 't3', 't3a', 't4g', 'm4', 'm5', 'm5a', 'm5ad', 'm5d', 'm5dn', 'm5n', 'm5zn', 'm6a', 'm6g', 'm6gd', 'm6i', 'm6id', 'm6idn', 'm6in', 'c4', 'c5', 'c5a', 'c5ad', 'c5d', 'c5n', 'c6a', 'c6g', 'c6gd', 'c6gn', 'c6i', 'c6id', 'c6in', 'r4', 'r5', 'r5a', 'r5ad', 'r5b', 'r5d', 'r5dn', 'r5n', 'r6a', 'r6g', 'r6gd', 'r6i', 'r6id', 'r6idn', 'r6in'];
1977
+ const instanceFamily = value.split('.')[1];
1978
+ if (cpuFamilies.includes(instanceFamily)) {
1979
+ console.warn(`⚠️ Warning: Using CPU instance ${value} with ${context.architecture} architecture. GPU instances are recommended for better performance.`);
1980
+ }
1981
+ }
1982
+ }
1983
+ break;
1984
+
1985
+ case 'awsRegion':
1986
+ if (value && !supportedOptions.awsRegions.includes(value)) {
1987
+ throw new ValidationError(
1988
+ `Unsupported AWS region: ${value}. Supported regions: ${supportedOptions.awsRegions.join(', ')}`,
1989
+ parameter,
1990
+ value
1991
+ );
1992
+ }
1993
+ break;
1994
+
1995
+ case 'awsRoleArn':
1996
+ if (value) {
1997
+ this._isValidArn(value);
1998
+ }
1999
+ break;
2000
+
2001
+ case 'buildTarget':
2002
+ case 'deployTarget':
2003
+ if (value && !supportedOptions.buildTargets.includes(value)) {
2004
+ throw new ValidationError(
2005
+ `Unsupported build target: ${value}. Supported targets: ${supportedOptions.buildTargets.join(', ')}`,
2006
+ parameter,
2007
+ value
2008
+ );
2009
+ }
2010
+ break;
2011
+
2012
+ case 'codebuildComputeType':
2013
+ if (value && !supportedOptions.codebuildComputeTypes.includes(value)) {
2014
+ throw new ValidationError(
2015
+ `Unsupported CodeBuild compute type: ${value}. Supported types: ${supportedOptions.codebuildComputeTypes.join(', ')}`,
2016
+ parameter,
2017
+ value
2018
+ );
2019
+ }
2020
+ break;
2021
+
2022
+ case 'codebuildProjectName':
2023
+ if (value) {
2024
+ // AWS CodeBuild project names must follow specific naming rules
2025
+ const projectNamePattern = /^[a-zA-Z0-9][a-zA-Z0-9\-_]{1,254}$/;
2026
+ if (!projectNamePattern.test(value)) {
2027
+ throw new ValidationError(
2028
+ `Invalid CodeBuild project name: ${value}. Project names must be 2-255 characters, start with a letter or number, and contain only letters, numbers, hyphens, and underscores.`,
2029
+ parameter,
2030
+ value
2031
+ );
2032
+ }
2033
+ }
2034
+ break;
2035
+ }
2036
+ }
2037
+
2038
+ /**
2039
+ * Resolves HF_TOKEN references to actual token values
2040
+ * @param {string} tokenValue - The token value or "$HF_TOKEN" reference
2041
+ * @returns {string|null} Resolved token value
2042
+ * @private
2043
+ */
2044
+ _resolveHfToken(tokenValue) {
2045
+ if (!tokenValue || tokenValue.trim() === '') {
2046
+ return null;
2047
+ }
2048
+
2049
+ // Check if it's an environment variable reference
2050
+ if (tokenValue.trim() === '$HF_TOKEN') {
2051
+ const envToken = process.env.HF_TOKEN;
2052
+ if (!envToken) {
2053
+ console.warn('⚠️ Warning: $HF_TOKEN specified but HF_TOKEN environment variable is not set');
2054
+ console.warn(' The container will be built without authentication.');
2055
+ return null;
2056
+ }
2057
+ return envToken;
2058
+ }
2059
+
2060
+ // Direct token value
2061
+ return tokenValue;
2062
+ }
2063
+
2064
+ /**
2065
+ * Validates AWS Role ARN format
2066
+ * @param {string} arn - The ARN to validate
2067
+ * @throws {ValidationError} If ARN format is invalid
2068
+ * @private
2069
+ */
2070
+ _isValidArn(arn) {
2071
+ const arnPattern = /^arn:aws:iam::\d{12}:role\/[\w+=,.@-]+$/;
2072
+ if (!arnPattern.test(arn)) {
2073
+ throw new ValidationError(
2074
+ `Invalid AWS Role ARN format: ${arn}. Expected format: arn:aws:iam::123456789012:role/RoleName`,
2075
+ 'awsRoleArn',
2076
+ arn
2077
+ );
2078
+ }
2079
+ return true;
2080
+ }
2081
+
2082
+ /**
2083
+ * Gets supported options for validation
2084
+ * @private
2085
+ */
2086
+ _getSupportedOptions() {
2087
+ return {
2088
+ deploymentConfigs: this.deploymentConfigResolver.getAllConfigs(),
2089
+ engines: ['sklearn', 'xgboost', 'tensorflow'],
2090
+ modelFormats: {
2091
+ 'sklearn': ['pkl', 'joblib'],
2092
+ 'xgboost': ['json', 'model', 'ubj'],
2093
+ 'tensorflow': ['keras', 'h5', 'SavedModel']
2094
+ },
2095
+ buildTargets: ['codebuild'],
2096
+ codebuildComputeTypes: ['BUILD_GENERAL1_SMALL', 'BUILD_GENERAL1_MEDIUM', 'BUILD_GENERAL1_LARGE'],
2097
+ awsRegions: [
2098
+ 'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2',
2099
+ 'eu-west-1', 'eu-west-2', 'eu-central-1', 'eu-north-1',
2100
+ 'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1',
2101
+ 'ca-central-1', 'sa-east-1'
2102
+ ]
2103
+ };
2104
+ }
2105
+ }
2106
+