@aws/ml-container-creator 0.2.1 → 0.2.3

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 (36) hide show
  1. package/bin/cli.js +88 -86
  2. package/config/bootstrap-stack.json +211 -0
  3. package/config/parameter-schema.json +88 -0
  4. package/infra/ci-harness/bin/ci-harness.ts +26 -0
  5. package/infra/ci-harness/buildspec.yml +352 -0
  6. package/infra/ci-harness/cdk.json +27 -0
  7. package/infra/ci-harness/lambda/scanner/index.ts +199 -0
  8. package/infra/ci-harness/lib/ci-harness-stack.ts +609 -0
  9. package/infra/ci-harness/package-lock.json +3979 -0
  10. package/infra/ci-harness/package.json +32 -0
  11. package/infra/ci-harness/tsconfig.json +38 -0
  12. package/package.json +13 -3
  13. package/src/app.js +318 -318
  14. package/src/copy-tpl.js +19 -19
  15. package/src/lib/asset-manager.js +74 -74
  16. package/src/lib/aws-profile-parser.js +45 -45
  17. package/src/lib/bootstrap-command-handler.js +560 -547
  18. package/src/lib/bootstrap-config.js +45 -45
  19. package/src/lib/ci-register-helpers.js +19 -19
  20. package/src/lib/ci-report-helpers.js +37 -37
  21. package/src/lib/ci-stage-helpers.js +49 -49
  22. package/src/lib/comment-generator.js +4 -4
  23. package/src/lib/config-manager.js +105 -105
  24. package/src/lib/deployment-config-resolver.js +10 -10
  25. package/src/lib/deployment-registry.js +153 -153
  26. package/src/lib/engine-prefix-resolver.js +8 -8
  27. package/src/lib/key-value-parser.js +6 -6
  28. package/src/lib/manifest-cli.js +108 -108
  29. package/src/lib/prompt-runner.js +224 -224
  30. package/src/lib/prompts.js +121 -121
  31. package/src/lib/registry-command-handler.js +174 -174
  32. package/src/lib/registry-loader.js +52 -52
  33. package/src/lib/sensitive-redactor.js +9 -9
  34. package/src/lib/template-engine.js +1 -1
  35. package/src/lib/template-manager.js +62 -62
  36. package/src/prompt-adapter.js +18 -18
@@ -9,12 +9,12 @@
9
9
  * and the generator's internal data shapes. No MCP runtime dependency.
10
10
  */
11
11
 
12
- import { readFileSync } from 'node:fs'
13
- import { resolve, dirname } from 'node:path'
14
- import { fileURLToPath } from 'node:url'
12
+ import { readFileSync } from 'node:fs';
13
+ import { resolve, dirname } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
15
 
16
- const __filename = fileURLToPath(import.meta.url)
17
- const __dirname = dirname(__filename)
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = dirname(__filename);
18
18
 
19
19
  // Catalog file paths relative to this module
20
20
  const CATALOG_PATHS = {
@@ -22,12 +22,12 @@ const CATALOG_PATHS = {
22
22
  tritonBackends: resolve(__dirname, '../../servers/base-image-picker/catalogs/triton-backends.json'),
23
23
  instances: resolve(__dirname, '../../servers/instance-recommender/catalogs/instances.json'),
24
24
  popularTransformers: resolve(__dirname, '../../servers/model-picker/catalogs/popular-transformers.json'),
25
- popularDiffusors: resolve(__dirname, '../../servers/model-picker/catalogs/popular-diffusors.json'),
26
- }
25
+ popularDiffusors: resolve(__dirname, '../../servers/model-picker/catalogs/popular-diffusors.json')
26
+ };
27
27
 
28
28
  class RegistryLoader {
29
29
  constructor() {
30
- this._catalogCache = {}
30
+ this._catalogCache = {};
31
31
  }
32
32
 
33
33
  /**
@@ -36,17 +36,17 @@ class RegistryLoader {
36
36
  */
37
37
  _loadCatalog(catalogPath) {
38
38
  if (this._catalogCache[catalogPath] !== undefined) {
39
- return this._catalogCache[catalogPath]
39
+ return this._catalogCache[catalogPath];
40
40
  }
41
41
  try {
42
- const raw = readFileSync(catalogPath, 'utf8')
43
- const data = JSON.parse(raw)
44
- this._catalogCache[catalogPath] = data
45
- return data
42
+ const raw = readFileSync(catalogPath, 'utf8');
43
+ const data = JSON.parse(raw);
44
+ this._catalogCache[catalogPath] = data;
45
+ return data;
46
46
  } catch (error) {
47
- console.warn(`Failed to load catalog ${catalogPath}: ${error.message}`)
48
- this._catalogCache[catalogPath] = null
49
- return null
47
+ console.warn(`Failed to load catalog ${catalogPath}: ${error.message}`);
48
+ this._catalogCache[catalogPath] = null;
49
+ return null;
50
50
  }
51
51
  }
52
52
 
@@ -65,17 +65,17 @@ class RegistryLoader {
65
65
  */
66
66
  async loadFrameworkRegistry() {
67
67
  try {
68
- const catalog = this._loadCatalog(CATALOG_PATHS.modelServers)
69
- if (!catalog) return {}
68
+ const catalog = this._loadCatalog(CATALOG_PATHS.modelServers);
69
+ if (!catalog) return {};
70
70
 
71
- const registry = {}
71
+ const registry = {};
72
72
  for (const [frameworkName, entries] of Object.entries(catalog)) {
73
- if (!Array.isArray(entries)) continue
74
- registry[frameworkName] = {}
73
+ if (!Array.isArray(entries)) continue;
74
+ registry[frameworkName] = {};
75
75
 
76
76
  for (const entry of entries) {
77
- const version = entry.labels?.framework_version
78
- if (!version) continue
77
+ const version = entry.labels?.framework_version;
78
+ if (!version) continue;
79
79
 
80
80
  registry[frameworkName][version] = {
81
81
  baseImage: entry.image,
@@ -85,14 +85,14 @@ class RegistryLoader {
85
85
  recommendedInstanceTypes: entry.defaults?.recommendedInstanceTypes || [],
86
86
  validationLevel: entry.validationLevel || 'untested',
87
87
  profiles: entry.profiles || {},
88
- notes: entry.notes || '',
89
- }
88
+ notes: entry.notes || ''
89
+ };
90
90
  }
91
91
  }
92
- return registry
92
+ return registry;
93
93
  } catch (error) {
94
- console.warn(`Failed to load framework registry: ${error.message}`)
95
- return {}
94
+ console.warn(`Failed to load framework registry: ${error.message}`);
95
+ return {};
96
96
  }
97
97
  }
98
98
 
@@ -116,27 +116,27 @@ class RegistryLoader {
116
116
  */
117
117
  async loadModelRegistry() {
118
118
  try {
119
- const transformers = this._loadCatalog(CATALOG_PATHS.popularTransformers) || {}
120
- const diffusors = this._loadCatalog(CATALOG_PATHS.popularDiffusors) || {}
119
+ const transformers = this._loadCatalog(CATALOG_PATHS.popularTransformers) || {};
120
+ const diffusors = this._loadCatalog(CATALOG_PATHS.popularDiffusors) || {};
121
121
 
122
- const registry = {}
123
- const allModels = { ...transformers, ...diffusors }
122
+ const registry = {};
123
+ const allModels = { ...transformers, ...diffusors };
124
124
 
125
125
  for (const [modelId, entry] of Object.entries(allModels)) {
126
126
  registry[modelId] = {
127
127
  family: entry.family || '',
128
128
  chatTemplate: entry.chat_template ?? null,
129
- requiresTemplate: entry.chat_template != null && entry.chat_template !== '',
129
+ requiresTemplate: entry.chat_template !== null && entry.chat_template !== undefined && entry.chat_template !== '',
130
130
  validationLevel: entry.validation_level || 'experimental',
131
131
  frameworkCompatibility: entry.framework_compatibility || {},
132
132
  profiles: entry.profiles || {},
133
- notes: entry.notes || '',
134
- }
133
+ notes: entry.notes || ''
134
+ };
135
135
  }
136
- return registry
136
+ return registry;
137
137
  } catch (error) {
138
- console.warn(`Failed to load model registry: ${error.message}`)
139
- return {}
138
+ console.warn(`Failed to load model registry: ${error.message}`);
139
+ return {};
140
140
  }
141
141
  }
142
142
 
@@ -150,10 +150,10 @@ class RegistryLoader {
150
150
  */
151
151
  async loadInstanceAcceleratorMapping() {
152
152
  try {
153
- const catalog = this._loadCatalog(CATALOG_PATHS.instances)
154
- if (!catalog || !catalog.catalog) return {}
153
+ const catalog = this._loadCatalog(CATALOG_PATHS.instances);
154
+ if (!catalog || !catalog.catalog) return {};
155
155
 
156
- const mapping = {}
156
+ const mapping = {};
157
157
  for (const [instanceType, entry] of Object.entries(catalog.catalog)) {
158
158
  mapping[instanceType] = {
159
159
  family: entry.family || '',
@@ -162,17 +162,17 @@ class RegistryLoader {
162
162
  hardware: entry.hardware || 'None',
163
163
  architecture: entry.gpuArchitecture || 'None',
164
164
  versions: entry.cudaVersions || null,
165
- default: entry.defaultCudaVersion || null,
165
+ default: entry.defaultCudaVersion || null
166
166
  },
167
167
  memory: entry.memGb ? `${entry.memGb} GB` : '0 GB',
168
168
  vcpus: entry.vcpus || 0,
169
- notes: entry.notes || '',
170
- }
169
+ notes: entry.notes || ''
170
+ };
171
171
  }
172
- return mapping
172
+ return mapping;
173
173
  } catch (error) {
174
- console.warn(`Failed to load instance accelerator mapping: ${error.message}`)
175
- return {}
174
+ console.warn(`Failed to load instance accelerator mapping: ${error.message}`);
175
+ return {};
176
176
  }
177
177
  }
178
178
 
@@ -186,13 +186,13 @@ class RegistryLoader {
186
186
  */
187
187
  async loadTritonBackends() {
188
188
  try {
189
- const catalog = this._loadCatalog(CATALOG_PATHS.tritonBackends)
190
- return catalog || {}
189
+ const catalog = this._loadCatalog(CATALOG_PATHS.tritonBackends);
190
+ return catalog || {};
191
191
  } catch (error) {
192
- console.warn(`Failed to load triton backends: ${error.message}`)
193
- return {}
192
+ console.warn(`Failed to load triton backends: ${error.message}`);
193
+ return {};
194
194
  }
195
195
  }
196
196
  }
197
197
 
198
- export default RegistryLoader
198
+ export default RegistryLoader;
@@ -14,17 +14,17 @@
14
14
  /**
15
15
  * Redaction marker used to replace sensitive values.
16
16
  */
17
- export const REDACTION_MARKER = '***REDACTED***'
17
+ export const REDACTION_MARKER = '***REDACTED***';
18
18
 
19
19
  /**
20
20
  * Exact key names that are always considered sensitive.
21
21
  */
22
- export const SENSITIVE_EXACT_KEYS = ['HF_TOKEN', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN']
22
+ export const SENSITIVE_EXACT_KEYS = ['HF_TOKEN', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN'];
23
23
 
24
24
  /**
25
25
  * Substrings that, when found in a key (case-insensitive), mark it as sensitive.
26
26
  */
27
- export const SENSITIVE_SUBSTRINGS = ['SECRET', 'TOKEN']
27
+ export const SENSITIVE_SUBSTRINGS = ['SECRET', 'TOKEN'];
28
28
 
29
29
  /**
30
30
  * Determine whether a given key matches sensitive patterns.
@@ -37,9 +37,9 @@ export const SENSITIVE_SUBSTRINGS = ['SECRET', 'TOKEN']
37
37
  * @returns {boolean} True if the key is sensitive
38
38
  */
39
39
  export function isSensitiveKey(key) {
40
- if (SENSITIVE_EXACT_KEYS.includes(key)) return true
41
- const upper = key.toUpperCase()
42
- return SENSITIVE_SUBSTRINGS.some(sub => upper.includes(sub))
40
+ if (SENSITIVE_EXACT_KEYS.includes(key)) return true;
41
+ const upper = key.toUpperCase();
42
+ return SENSITIVE_SUBSTRINGS.some(sub => upper.includes(sub));
43
43
  }
44
44
 
45
45
  /**
@@ -51,9 +51,9 @@ export function isSensitiveKey(key) {
51
51
  * @returns {Object<string, string>} New object with sensitive values redacted
52
52
  */
53
53
  export function redactSensitiveValues(params) {
54
- const result = {}
54
+ const result = {};
55
55
  for (const [key, value] of Object.entries(params)) {
56
- result[key] = isSensitiveKey(key) ? REDACTION_MARKER : value
56
+ result[key] = isSensitiveKey(key) ? REDACTION_MARKER : value;
57
57
  }
58
- return result
58
+ return result;
59
59
  }
@@ -51,7 +51,7 @@ export default class TemplateEngine {
51
51
  * @param {Object} config - Configuration profile
52
52
  * @returns {void}
53
53
  */
54
- generateDeploymentScript(config) {
54
+ generateDeploymentScript(_config) {
55
55
  // No-op: legacy deploy/ scripts have been removed.
56
56
  // Deployment is handled by do/deploy in the do-framework.
57
57
  }
@@ -15,7 +15,7 @@
15
15
  /**
16
16
  * GPU-requiring Triton backends that must use GPU instance types
17
17
  */
18
- const GPU_REQUIRING_BACKENDS = ['triton-vllm', 'triton-tensorrtllm', 'diffusors-vllm-omni']
18
+ const GPU_REQUIRING_BACKENDS = ['triton-vllm', 'triton-tensorrtllm', 'diffusors-vllm-omni'];
19
19
 
20
20
  /**
21
21
  * CPU-only instance type families (patterns that indicate non-GPU instances)
@@ -24,8 +24,8 @@ const CPU_ONLY_INSTANCE_PATTERNS = [
24
24
  /^ml\.m[0-9]+\./, // ml.m4.*, ml.m5.*, ml.m6i.*, etc.
25
25
  /^ml\.c[0-9]+\./, // ml.c4.*, ml.c5.*, ml.c6i.*, etc.
26
26
  /^ml\.t[0-9]+\./, // ml.t2.*, ml.t3.*, etc.
27
- /^ml\.r[0-9]+\./, // ml.r5.*, ml.r6i.*, etc.
28
- ]
27
+ /^ml\.r[0-9]+\./ // ml.r5.*, ml.r6i.*, etc.
28
+ ];
29
29
 
30
30
  /**
31
31
  * Check if an instance type is CPU-only (no GPU)
@@ -34,14 +34,14 @@ const CPU_ONLY_INSTANCE_PATTERNS = [
34
34
  */
35
35
  function isCpuOnlyInstance(instanceType) {
36
36
  if (!instanceType || instanceType === 'custom') {
37
- return false
37
+ return false;
38
38
  }
39
- return CPU_ONLY_INSTANCE_PATTERNS.some(pattern => pattern.test(instanceType))
39
+ return CPU_ONLY_INSTANCE_PATTERNS.some(pattern => pattern.test(instanceType));
40
40
  }
41
41
 
42
42
  export default class TemplateManager {
43
43
  constructor(answers) {
44
- this.answers = answers
44
+ this.answers = answers;
45
45
  }
46
46
 
47
47
  /**
@@ -72,17 +72,17 @@ export default class TemplateManager {
72
72
  'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1',
73
73
  'ca-central-1', 'sa-east-1'
74
74
  ]
75
- }
75
+ };
76
76
 
77
77
  // Validate deployment configuration if present
78
78
  if (this.answers.deploymentConfig) {
79
- this._validateChoice('deploymentConfig', supportedOptions.deploymentConfigs)
79
+ this._validateChoice('deploymentConfig', supportedOptions.deploymentConfigs);
80
80
 
81
81
  // GPU instance type enforcement for GPU-requiring backends
82
- this._validateGpuRequirement()
82
+ this._validateGpuRequirement();
83
83
  } else {
84
84
  // Fallback: validate architecture and backend separately (new canonical format)
85
- const architectures = ['http', 'transformers', 'triton', 'diffusors']
85
+ const architectures = ['http', 'transformers', 'triton', 'diffusors'];
86
86
  const backends = [
87
87
  // http backends
88
88
  'flask', 'fastapi',
@@ -92,63 +92,63 @@ export default class TemplateManager {
92
92
  'fil', 'onnxruntime', 'tensorflow', 'pytorch', 'tensorrtllm', 'python',
93
93
  // diffusors backends
94
94
  'vllm-omni'
95
- ]
95
+ ];
96
96
 
97
- this._validateChoice('architecture', architectures)
98
- this._validateChoice('backend', backends)
97
+ this._validateChoice('architecture', architectures);
98
+ this._validateChoice('backend', backends);
99
99
 
100
100
  // Validate tensorrt-llm is only used with transformers architecture
101
101
  if (this.answers.backend === 'tensorrt-llm' && this.answers.architecture !== 'transformers') {
102
- throw new Error('⚠️ TensorRT-LLM is only supported with the transformers architecture. Please select "transformers" as your architecture or choose a different backend.')
102
+ throw new Error('⚠️ TensorRT-LLM is only supported with the transformers architecture. Please select "transformers" as your architecture or choose a different backend.');
103
103
  }
104
104
 
105
105
  // GPU instance type enforcement for GPU-requiring backends (fallback path)
106
106
  const deploymentConfig = this.answers.architecture && this.answers.backend
107
107
  ? `${this.answers.architecture}-${this.answers.backend}`
108
- : null
108
+ : null;
109
109
  if (deploymentConfig && GPU_REQUIRING_BACKENDS.includes(deploymentConfig)) {
110
- this._validateGpuRequirementForConfig(deploymentConfig)
110
+ this._validateGpuRequirementForConfig(deploymentConfig);
111
111
  }
112
112
  }
113
113
 
114
114
  // Validate buildTarget (replaces deployTarget)
115
115
  if (this.answers.buildTarget) {
116
- this._validateChoice('buildTarget', supportedOptions.buildTargets)
116
+ this._validateChoice('buildTarget', supportedOptions.buildTargets);
117
117
  } else if (this.answers.deployTarget) {
118
118
  // Backward compatibility: validate deployTarget against buildTargets
119
- this._validateChoice('deployTarget', supportedOptions.buildTargets)
119
+ this._validateChoice('deployTarget', supportedOptions.buildTargets);
120
120
  }
121
121
 
122
122
  // Validate deploymentTarget
123
123
  if (this.answers.deploymentTarget) {
124
- this._validateChoice('deploymentTarget', supportedOptions.deploymentTargets)
124
+ this._validateChoice('deploymentTarget', supportedOptions.deploymentTargets);
125
125
  }
126
126
 
127
127
  // Validate HyperPod EKS specific fields
128
128
  if (this.answers.deploymentTarget === 'hyperpod-eks') {
129
- this._validateHyperPodConfig()
129
+ this._validateHyperPodConfig();
130
130
  }
131
131
 
132
132
  // Validate async inference specific fields
133
- this._validateAsyncConfig()
133
+ this._validateAsyncConfig();
134
134
 
135
135
  // Validate batch transform specific fields
136
- this._validateBatchTransformConfig()
136
+ this._validateBatchTransformConfig();
137
137
 
138
138
  // Validate instance type format (ml.*.*) - only for managed-inference
139
139
  if (this.answers.instanceType && this.answers.instanceType !== 'custom') {
140
- const instancePattern = /^ml\.[a-z0-9]+\.(nano|micro|small|medium|large|xlarge|[0-9]+xlarge)$/
140
+ const instancePattern = /^ml\.[a-z0-9]+\.(nano|micro|small|medium|large|xlarge|[0-9]+xlarge)$/;
141
141
  if (!instancePattern.test(this.answers.instanceType)) {
142
- throw new Error(`⚠️ Invalid instance type format: ${this.answers.instanceType}. Expected format: ml.{family}.{size} (e.g., ml.m5.large, ml.g5.xlarge)`)
142
+ throw new Error(`⚠️ Invalid instance type format: ${this.answers.instanceType}. Expected format: ml.{family}.{size} (e.g., ml.m5.large, ml.g5.xlarge)`);
143
143
  }
144
144
  }
145
145
 
146
- this._validateChoice('awsRegion', supportedOptions.awsRegions)
146
+ this._validateChoice('awsRegion', supportedOptions.awsRegions);
147
147
 
148
148
  // Validate test types if testing is enabled
149
149
  if (this.answers.includeTesting && this.answers.testTypes) {
150
150
  for (const testType of this.answers.testTypes) {
151
- this._validateChoice('testType', supportedOptions.testTypes, testType)
151
+ this._validateChoice('testType', supportedOptions.testTypes, testType);
152
152
  }
153
153
  }
154
154
  }
@@ -161,21 +161,21 @@ export default class TemplateManager {
161
161
  _validateHyperPodConfig() {
162
162
  // Validate hyperPodCluster is non-empty
163
163
  if (!this.answers.hyperPodCluster || this.answers.hyperPodCluster.trim() === '') {
164
- throw new Error('⚠️ hyperPodCluster is required when deploymentTarget is "hyperpod-eks". Please provide a valid HyperPod cluster name.')
164
+ throw new Error('⚠️ hyperPodCluster is required when deploymentTarget is "hyperpod-eks". Please provide a valid HyperPod cluster name.');
165
165
  }
166
166
 
167
167
  // Validate hyperPodNamespace conforms to RFC 1123 DNS label format
168
168
  if (this.answers.hyperPodNamespace) {
169
169
  if (!this._isValidRfc1123DnsLabel(this.answers.hyperPodNamespace)) {
170
- throw new Error(`⚠️ Invalid hyperPodNamespace: "${this.answers.hyperPodNamespace}". Namespace must conform to RFC 1123 DNS label format: lowercase alphanumeric characters or hyphens, must start and end with an alphanumeric character, and be at most 63 characters.`)
170
+ throw new Error(`⚠️ Invalid hyperPodNamespace: "${this.answers.hyperPodNamespace}". Namespace must conform to RFC 1123 DNS label format: lowercase alphanumeric characters or hyphens, must start and end with an alphanumeric character, and be at most 63 characters.`);
171
171
  }
172
172
  }
173
173
 
174
174
  // Validate hyperPodReplicas is an integer >= 1
175
175
  if (this.answers.hyperPodReplicas !== undefined) {
176
- const replicas = this.answers.hyperPodReplicas
176
+ const replicas = this.answers.hyperPodReplicas;
177
177
  if (!Number.isInteger(replicas) || replicas < 1) {
178
- throw new Error(`⚠️ Invalid hyperPodReplicas: "${replicas}". Replicas must be an integer greater than or equal to 1.`)
178
+ throw new Error(`⚠️ Invalid hyperPodReplicas: "${replicas}". Replicas must be an integer greater than or equal to 1.`);
179
179
  }
180
180
  }
181
181
  }
@@ -188,11 +188,11 @@ export default class TemplateManager {
188
188
  */
189
189
  _isValidRfc1123DnsLabel(value) {
190
190
  if (!value || typeof value !== 'string') {
191
- return false
191
+ return false;
192
192
  }
193
193
  // RFC 1123 DNS label: lowercase alphanumeric, hyphens allowed (not at start/end), max 63 chars
194
- const rfc1123Pattern = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/
195
- return value.length <= 63 && rfc1123Pattern.test(value)
194
+ const rfc1123Pattern = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
195
+ return value.length <= 63 && rfc1123Pattern.test(value);
196
196
  }
197
197
 
198
198
  /**
@@ -201,33 +201,33 @@ export default class TemplateManager {
201
201
  * @throws {Error} If async configuration is invalid
202
202
  */
203
203
  _validateAsyncConfig() {
204
- if (this.answers.deploymentTarget !== 'async-inference') return
204
+ if (this.answers.deploymentTarget !== 'async-inference') return;
205
205
 
206
206
  // Validate S3 output path format if explicitly provided
207
207
  if (this.answers.asyncS3OutputPath && this.answers.asyncS3OutputPath.trim() !== '') {
208
208
  if (!this.answers.asyncS3OutputPath.startsWith('s3://')) {
209
- throw new Error('⚠️ asyncS3OutputPath must start with "s3://". Example: s3://my-bucket/output/')
209
+ throw new Error('⚠️ asyncS3OutputPath must start with "s3://". Example: s3://my-bucket/output/');
210
210
  }
211
211
  }
212
212
 
213
213
  // Validate SNS topic ARN format if explicitly provided
214
- const snsArnPattern = /^arn:aws:sns:[a-z0-9-]+:\d{12}:.+$/
214
+ const snsArnPattern = /^arn:aws:sns:[a-z0-9-]+:\d{12}:.+$/;
215
215
  if (this.answers.asyncSnsSuccessTopic && this.answers.asyncSnsSuccessTopic.trim() !== '') {
216
216
  if (!snsArnPattern.test(this.answers.asyncSnsSuccessTopic)) {
217
- throw new Error('⚠️ asyncSnsSuccessTopic must be a valid SNS ARN. Format: arn:aws:sns:<region>:<account-id>:<topic-name>')
217
+ throw new Error('⚠️ asyncSnsSuccessTopic must be a valid SNS ARN. Format: arn:aws:sns:<region>:<account-id>:<topic-name>');
218
218
  }
219
219
  }
220
220
  if (this.answers.asyncSnsErrorTopic && this.answers.asyncSnsErrorTopic.trim() !== '') {
221
221
  if (!snsArnPattern.test(this.answers.asyncSnsErrorTopic)) {
222
- throw new Error('⚠️ asyncSnsErrorTopic must be a valid SNS ARN. Format: arn:aws:sns:<region>:<account-id>:<topic-name>')
222
+ throw new Error('⚠️ asyncSnsErrorTopic must be a valid SNS ARN. Format: arn:aws:sns:<region>:<account-id>:<topic-name>');
223
223
  }
224
224
  }
225
225
 
226
226
  // Validate max concurrent invocations
227
227
  if (this.answers.asyncMaxConcurrentInvocations !== undefined) {
228
- const val = this.answers.asyncMaxConcurrentInvocations
228
+ const val = this.answers.asyncMaxConcurrentInvocations;
229
229
  if (!Number.isInteger(val) || val < 1) {
230
- throw new Error('⚠️ asyncMaxConcurrentInvocations must be an integer >= 1')
230
+ throw new Error('⚠️ asyncMaxConcurrentInvocations must be an integer >= 1');
231
231
  }
232
232
  }
233
233
  }
@@ -238,61 +238,61 @@ export default class TemplateManager {
238
238
  * @throws {Error} If batch transform configuration is invalid
239
239
  */
240
240
  _validateBatchTransformConfig() {
241
- if (this.answers.deploymentTarget !== 'batch-transform') return
241
+ if (this.answers.deploymentTarget !== 'batch-transform') return;
242
242
 
243
243
  // Validate S3 input path format if provided
244
244
  if (this.answers.batchInputPath && this.answers.batchInputPath.trim() !== '') {
245
245
  if (!this.answers.batchInputPath.startsWith('s3://')) {
246
- throw new Error('⚠️ batchInputPath must start with "s3://". Example: s3://my-bucket/input/')
246
+ throw new Error('⚠️ batchInputPath must start with "s3://". Example: s3://my-bucket/input/');
247
247
  }
248
248
  }
249
249
 
250
250
  // Validate S3 output path format if provided
251
251
  if (this.answers.batchOutputPath && this.answers.batchOutputPath.trim() !== '') {
252
252
  if (!this.answers.batchOutputPath.startsWith('s3://')) {
253
- throw new Error('⚠️ batchOutputPath must start with "s3://". Example: s3://my-bucket/output/')
253
+ throw new Error('⚠️ batchOutputPath must start with "s3://". Example: s3://my-bucket/output/');
254
254
  }
255
255
  }
256
256
 
257
257
  // Validate instance count
258
258
  if (this.answers.batchInstanceCount !== undefined) {
259
- const val = this.answers.batchInstanceCount
259
+ const val = this.answers.batchInstanceCount;
260
260
  if (!Number.isInteger(val) || val < 1) {
261
- throw new Error('⚠️ batchInstanceCount must be an integer >= 1')
261
+ throw new Error('⚠️ batchInstanceCount must be an integer >= 1');
262
262
  }
263
263
  }
264
264
 
265
265
  // Validate split type
266
- const validSplitTypes = ['Line', 'RecordIO', 'None']
266
+ const validSplitTypes = ['Line', 'RecordIO', 'None'];
267
267
  if (this.answers.batchSplitType && !validSplitTypes.includes(this.answers.batchSplitType)) {
268
- throw new Error(`⚠️ batchSplitType must be one of: ${validSplitTypes.join(', ')}`)
268
+ throw new Error(`⚠️ batchSplitType must be one of: ${validSplitTypes.join(', ')}`);
269
269
  }
270
270
 
271
271
  // Validate batch strategy
272
- const validStrategies = ['MultiRecord', 'SingleRecord']
272
+ const validStrategies = ['MultiRecord', 'SingleRecord'];
273
273
  if (this.answers.batchStrategy && !validStrategies.includes(this.answers.batchStrategy)) {
274
- throw new Error(`⚠️ batchStrategy must be one of: ${validStrategies.join(', ')}`)
274
+ throw new Error(`⚠️ batchStrategy must be one of: ${validStrategies.join(', ')}`);
275
275
  }
276
276
 
277
277
  // Validate join source
278
- const validJoinSources = ['Input', 'None']
278
+ const validJoinSources = ['Input', 'None'];
279
279
  if (this.answers.batchJoinSource && !validJoinSources.includes(this.answers.batchJoinSource)) {
280
- throw new Error(`⚠️ batchJoinSource must be one of: ${validJoinSources.join(', ')}`)
280
+ throw new Error(`⚠️ batchJoinSource must be one of: ${validJoinSources.join(', ')}`);
281
281
  }
282
282
 
283
283
  // Validate max concurrent transforms
284
284
  if (this.answers.batchMaxConcurrentTransforms !== undefined) {
285
- const val = this.answers.batchMaxConcurrentTransforms
285
+ const val = this.answers.batchMaxConcurrentTransforms;
286
286
  if (!Number.isInteger(val) || val < 0) {
287
- throw new Error('⚠️ batchMaxConcurrentTransforms must be an integer >= 0')
287
+ throw new Error('⚠️ batchMaxConcurrentTransforms must be an integer >= 0');
288
288
  }
289
289
  }
290
290
 
291
291
  // Validate max payload in MB
292
292
  if (this.answers.batchMaxPayloadInMB !== undefined) {
293
- const val = this.answers.batchMaxPayloadInMB
293
+ const val = this.answers.batchMaxPayloadInMB;
294
294
  if (!Number.isInteger(val) || val < 0 || val > 100) {
295
- throw new Error('⚠️ batchMaxPayloadInMB must be an integer between 0 and 100')
295
+ throw new Error('⚠️ batchMaxPayloadInMB must be an integer between 0 and 100');
296
296
  }
297
297
  }
298
298
  }
@@ -304,9 +304,9 @@ export default class TemplateManager {
304
304
  * @throws {Error} If a GPU-requiring backend is paired with a CPU-only instance
305
305
  */
306
306
  _validateGpuRequirement() {
307
- const dc = this.answers.deploymentConfig
307
+ const dc = this.answers.deploymentConfig;
308
308
  if (GPU_REQUIRING_BACKENDS.includes(dc)) {
309
- this._validateGpuRequirementForConfig(dc)
309
+ this._validateGpuRequirementForConfig(dc);
310
310
  }
311
311
  }
312
312
 
@@ -317,13 +317,13 @@ export default class TemplateManager {
317
317
  * @throws {Error} If instance type is CPU-only
318
318
  */
319
319
  _validateGpuRequirementForConfig(deploymentConfig) {
320
- const instanceType = this.answers.instanceType
320
+ const instanceType = this.answers.instanceType;
321
321
  if (isCpuOnlyInstance(instanceType)) {
322
322
  throw new Error(
323
323
  `⚠️ ${deploymentConfig} requires a GPU instance type. ` +
324
324
  `Selected: ${instanceType}. ` +
325
- `Recommended: ml.g5.xlarge, ml.g5.2xlarge`
326
- )
325
+ 'Recommended: ml.g5.xlarge, ml.g5.2xlarge'
326
+ );
327
327
  }
328
328
  }
329
329
 
@@ -332,9 +332,9 @@ export default class TemplateManager {
332
332
  * @private
333
333
  */
334
334
  _validateChoice(field, supportedValues, value = null) {
335
- const actualValue = value || this.answers[field]
335
+ const actualValue = value || this.answers[field];
336
336
  if (actualValue && !supportedValues.includes(actualValue)) {
337
- throw new Error(`⚠️ ${actualValue} not implemented yet for ${field}.`)
337
+ throw new Error(`⚠️ ${actualValue} not implemented yet for ${field}.`);
338
338
  }
339
339
  }
340
340
  }