@aws/ml-container-creator 0.9.1 → 0.10.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 (90) hide show
  1. package/LICENSE-THIRD-PARTY +9304 -0
  2. package/bin/cli.js +2 -0
  3. package/config/bootstrap-e2e-stack.json +341 -0
  4. package/config/bootstrap-stack.json +40 -3
  5. package/config/parameter-schema-v2.json +2049 -0
  6. package/config/tune-catalog.json +1781 -0
  7. package/infra/ci-harness/buildspec.yml +1 -0
  8. package/infra/ci-harness/lambda/path-prover/brain.ts +306 -0
  9. package/infra/ci-harness/lambda/path-prover/write-results.ts +152 -0
  10. package/infra/ci-harness/lib/ci-harness-stack.ts +837 -7
  11. package/infra/ci-harness/state-machines/path-prover.asl.json +496 -0
  12. package/package.json +53 -68
  13. package/servers/base-image-picker/index.js +121 -121
  14. package/servers/e2e-status/index.js +297 -0
  15. package/servers/e2e-status/manifest.json +14 -0
  16. package/servers/e2e-status/package.json +15 -0
  17. package/servers/endpoint-picker/LICENSE +202 -0
  18. package/servers/endpoint-picker/index.js +536 -0
  19. package/servers/endpoint-picker/manifest.json +14 -0
  20. package/servers/endpoint-picker/package.json +18 -0
  21. package/servers/hyperpod-cluster-picker/index.js +125 -125
  22. package/servers/instance-sizer/index.js +138 -138
  23. package/servers/instance-sizer/lib/instance-ranker.js +76 -76
  24. package/servers/instance-sizer/lib/model-resolver.js +61 -61
  25. package/servers/instance-sizer/lib/quota-resolver.js +113 -113
  26. package/servers/instance-sizer/lib/vram-estimator.js +31 -31
  27. package/servers/lib/bedrock-client.js +38 -38
  28. package/servers/lib/catalogs/jumpstart-public.json +101 -16
  29. package/servers/lib/catalogs/model-servers.json +201 -3
  30. package/servers/lib/catalogs/models.json +182 -26
  31. package/servers/lib/custom-validators.js +13 -13
  32. package/servers/lib/dynamic-resolver.js +4 -4
  33. package/servers/marketplace-picker/index.js +342 -0
  34. package/servers/marketplace-picker/manifest.json +14 -0
  35. package/servers/marketplace-picker/package.json +18 -0
  36. package/servers/model-picker/index.js +382 -382
  37. package/servers/region-picker/index.js +56 -56
  38. package/servers/workload-picker/LICENSE +202 -0
  39. package/servers/workload-picker/catalogs/workload-profiles.json +67 -0
  40. package/servers/workload-picker/index.js +171 -0
  41. package/servers/workload-picker/manifest.json +16 -0
  42. package/servers/workload-picker/package.json +16 -0
  43. package/src/app.js +4 -390
  44. package/src/lib/bootstrap-command-handler.js +710 -1148
  45. package/src/lib/bootstrap-config.js +36 -0
  46. package/src/lib/bootstrap-profile-manager.js +641 -0
  47. package/src/lib/bootstrap-provisioners.js +421 -0
  48. package/src/lib/ci-register-helpers.js +74 -0
  49. package/src/lib/config-loader.js +408 -0
  50. package/src/lib/config-manager.js +66 -1685
  51. package/src/lib/config-mcp-client.js +118 -0
  52. package/src/lib/config-validator.js +634 -0
  53. package/src/lib/cuda-resolver.js +149 -0
  54. package/src/lib/e2e-catalog-validator.js +251 -3
  55. package/src/lib/e2e-ci-recorder.js +103 -0
  56. package/src/lib/generated/cli-options.js +315 -311
  57. package/src/lib/generated/parameter-matrix.js +671 -0
  58. package/src/lib/generated/validation-rules.js +71 -71
  59. package/src/lib/marketplace-flow.js +276 -0
  60. package/src/lib/mcp-query-runner.js +768 -0
  61. package/src/lib/parameter-schema-validator.js +62 -18
  62. package/src/lib/path-prover-brain.js +607 -0
  63. package/src/lib/prompt-runner.js +41 -1504
  64. package/src/lib/prompts/feature-prompts.js +172 -0
  65. package/src/lib/prompts/index.js +48 -0
  66. package/src/lib/prompts/infrastructure-prompts.js +690 -0
  67. package/src/lib/prompts/model-prompts.js +552 -0
  68. package/src/lib/prompts/project-prompts.js +82 -0
  69. package/src/lib/prompts.js +2 -1446
  70. package/src/lib/registry-command-handler.js +135 -3
  71. package/src/lib/secrets-prompt-runner.js +251 -0
  72. package/src/lib/template-variable-resolver.js +422 -0
  73. package/src/lib/tune-catalog-validator.js +37 -4
  74. package/templates/Dockerfile +9 -0
  75. package/templates/code/adapter_sidecar.py +444 -0
  76. package/templates/code/serve +6 -0
  77. package/templates/code/serve.d/vllm.ejs +1 -1
  78. package/templates/do/.benchmark_writer.py +1476 -0
  79. package/templates/do/.tune_helper.py +982 -57
  80. package/templates/do/__pycache__/.benchmark_writer.cpython-312.pyc +0 -0
  81. package/templates/do/adapter +149 -0
  82. package/templates/do/benchmark +639 -85
  83. package/templates/do/config +108 -5
  84. package/templates/do/deploy.d/managed-inference.ejs +192 -11
  85. package/templates/do/optimize +106 -37
  86. package/templates/do/register +89 -0
  87. package/templates/do/test +13 -0
  88. package/templates/do/tune +378 -59
  89. package/templates/do/validate +44 -4
  90. package/config/parameter-schema.json +0 -88
@@ -0,0 +1,641 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { existsSync, unlinkSync } from 'node:fs';
5
+ import { execSync } from 'node:child_process';
6
+ import AssetManager from './asset-manager.js';
7
+
8
+ const STACK_NAME_PREFIX = 'mlcc-bootstrap';
9
+
10
+ /**
11
+ * Handles bootstrap profile management subcommands (status, use, list, remove, scan, prune, sync-schemas).
12
+ * Delegates back to the BootstrapCommandHandler instance for shared helpers.
13
+ */
14
+ export default class BootstrapProfileManager {
15
+ constructor(handler) {
16
+ this.handler = handler;
17
+ }
18
+
19
+ /**
20
+ * Display active bootstrap profile and resource state.
21
+ * @param {object} [options] - Parsed CLI options (e.g., --verify)
22
+ */
23
+ async _handleStatus(options = {}) {
24
+ const config = this.handler.config.read();
25
+ if (!config) {
26
+ console.log('No bootstrap configuration found.');
27
+ console.log('Run `ml-container-creator bootstrap` to set up shared infrastructure.');
28
+ return;
29
+ }
30
+
31
+ const profile = this.handler.config.getActiveProfile();
32
+ if (!profile) {
33
+ console.log('No active bootstrap profile found.');
34
+ console.log('Run `ml-container-creator bootstrap` to set up shared infrastructure.');
35
+ return;
36
+ }
37
+
38
+ const allProfiles = this.handler.config.listProfiles();
39
+ console.log(`\n📋 Active Profile: ${profile.name} (${allProfiles.length} profile${allProfiles.length === 1 ? '' : 's'} total)`);
40
+ console.log('─'.repeat(40));
41
+
42
+ for (const [key, value] of Object.entries(profile.config)) {
43
+ console.log(` ${key}: ${value}`);
44
+ }
45
+
46
+ console.log('─'.repeat(40));
47
+
48
+ // Validate bootstrap stack
49
+ console.log('\n🔍 Resource Validation:');
50
+
51
+ const stackName = profile.config.stackName || `${STACK_NAME_PREFIX}-${profile.name}`;
52
+
53
+ try {
54
+ const stackInfo = this.handler._execAws(
55
+ `cloudformation describe-stacks --stack-name ${stackName} --region ${profile.config.awsRegion}`,
56
+ profile.config.awsProfile
57
+ );
58
+
59
+ const stack = stackInfo.Stacks && stackInfo.Stacks[0];
60
+ if (stack) {
61
+ const status = stack.StackStatus;
62
+ const statusIcon = status === 'CREATE_COMPLETE' || status === 'UPDATE_COMPLETE' ? '✅' : '⚠️';
63
+ console.log(` ${statusIcon} Bootstrap stack: ${stackName} (${status})`);
64
+
65
+ // Show stack outputs
66
+ const outputs = {};
67
+ for (const output of (stack.Outputs || [])) {
68
+ outputs[output.OutputKey] = output.OutputValue;
69
+ }
70
+
71
+ if (outputs.RoleArn) {
72
+ console.log(` ✅ IAM role: ${outputs.RoleArn.split('/').pop()}`);
73
+ }
74
+ if (outputs.EcrRepositoryName) {
75
+ console.log(` ✅ ECR repository: ${outputs.EcrRepositoryName}`);
76
+ }
77
+ if (outputs.AsyncS3BucketName) {
78
+ console.log(` ✅ S3 bucket (async): ${outputs.AsyncS3BucketName}`);
79
+ }
80
+ if (outputs.BatchS3BucketName) {
81
+ console.log(` ✅ S3 bucket (batch): ${outputs.BatchS3BucketName}`);
82
+ }
83
+ if (outputs.AdapterS3BucketName) {
84
+ console.log(` ✅ S3 bucket (adapters): ${outputs.AdapterS3BucketName}`);
85
+ }
86
+ if (outputs.BenchmarkS3BucketName) {
87
+ console.log(` ✅ S3 bucket (benchmark): ${outputs.BenchmarkS3BucketName}`);
88
+ }
89
+ if (outputs.StackVersion) {
90
+ console.log(` 📋 Stack version: ${outputs.StackVersion}`);
91
+ }
92
+ }
93
+ } catch {
94
+ // Fall back to individual resource checks for profiles created before CloudFormation migration
95
+ console.log(` ⚠️ Bootstrap stack "${stackName}" not found — checking resources individually`);
96
+
97
+ try {
98
+ const defaultRoleName = 'mlcc-sagemaker-execution-role';
99
+ let roleName = defaultRoleName;
100
+ if (profile.config.roleArn) {
101
+ const arnParts = profile.config.roleArn.split('/');
102
+ roleName = arnParts[arnParts.length - 1];
103
+ }
104
+
105
+ const roleExists = this.handler._resourceExists(
106
+ `iam get-role --role-name ${roleName}`,
107
+ profile.config.awsProfile
108
+ );
109
+ if (roleExists) {
110
+ console.log(` ✅ IAM role: ${roleName}`);
111
+ } else {
112
+ console.log(` ⚠️ IAM role: ${roleName} — missing`);
113
+ }
114
+ } catch {
115
+ console.log(' ⚠️ IAM role: could not validate');
116
+ }
117
+
118
+ try {
119
+ const ecrExists = this.handler._resourceExists(
120
+ `ecr describe-repositories --repository-names ml-container-creator --region ${profile.config.awsRegion}`,
121
+ profile.config.awsProfile
122
+ );
123
+ if (ecrExists) {
124
+ console.log(' ✅ ECR repository: ml-container-creator');
125
+ } else {
126
+ console.log(' ⚠️ ECR repository: ml-container-creator — missing');
127
+ }
128
+ } catch {
129
+ console.log(' ⚠️ ECR repository: could not validate');
130
+ }
131
+
132
+ if (profile.config.asyncS3Bucket) {
133
+ try {
134
+ const asyncExists = this.handler._resourceExists(
135
+ `s3api head-bucket --bucket ${profile.config.asyncS3Bucket}`,
136
+ profile.config.awsProfile
137
+ );
138
+ console.log(asyncExists
139
+ ? ` ✅ S3 bucket: ${profile.config.asyncS3Bucket}`
140
+ : ` ⚠️ S3 bucket: ${profile.config.asyncS3Bucket} — missing`);
141
+ } catch {
142
+ console.log(` ⚠️ S3 bucket: ${profile.config.asyncS3Bucket} — could not validate`);
143
+ }
144
+ }
145
+
146
+ if (profile.config.batchS3Bucket) {
147
+ try {
148
+ const batchExists = this.handler._resourceExists(
149
+ `s3api head-bucket --bucket ${profile.config.batchS3Bucket}`,
150
+ profile.config.awsProfile
151
+ );
152
+ console.log(batchExists
153
+ ? ` ✅ S3 bucket: ${profile.config.batchS3Bucket}`
154
+ : ` ⚠️ S3 bucket: ${profile.config.batchS3Bucket} — missing`);
155
+ } catch {
156
+ console.log(` ⚠️ S3 bucket: ${profile.config.batchS3Bucket} — could not validate`);
157
+ }
158
+ }
159
+
160
+ if (profile.config.benchmarkS3Bucket) {
161
+ try {
162
+ const benchmarkExists = this.handler._resourceExists(
163
+ `s3api head-bucket --bucket ${profile.config.benchmarkS3Bucket}`,
164
+ profile.config.awsProfile
165
+ );
166
+ console.log(benchmarkExists
167
+ ? ` ✅ S3 bucket (benchmark): ${profile.config.benchmarkS3Bucket}`
168
+ : ` ⚠️ S3 bucket (benchmark): ${profile.config.benchmarkS3Bucket} — missing`);
169
+ } catch {
170
+ console.log(` ⚠️ S3 bucket (benchmark): ${profile.config.benchmarkS3Bucket} — could not validate`);
171
+ }
172
+ }
173
+ }
174
+
175
+ // Display deployed resources from manifest
176
+ console.log('\n📦 Deployed Resources:');
177
+
178
+ const assetManager = new AssetManager(profile.name);
179
+
180
+ if (!existsSync(assetManager.manifestPath)) {
181
+ console.log(' No deployment tracking data available.');
182
+ console.log(' Resources will be tracked after running deploy, push, or submit scripts.');
183
+ return;
184
+ }
185
+
186
+ const resourcesByProject = assetManager.getResourcesByProject();
187
+
188
+ if (resourcesByProject.size === 0) {
189
+ console.log(' No deployed resources tracked.');
190
+ return;
191
+ }
192
+
193
+ for (const [project, resources] of resourcesByProject) {
194
+ console.log(`\n Project: ${project}`);
195
+ for (const resource of resources) {
196
+ const timestamp = resource.createdAt || resource.lastUpdatedAt;
197
+ console.log(` ${resource.resourceType} ${resource.resourceId} [${resource.status}] ${timestamp}`);
198
+ }
199
+ }
200
+
201
+ const counts = assetManager.getStatusCounts();
202
+ console.log(`\n Summary: ${counts.active} active, ${counts.deleted} deleted, ${counts.unknown} unknown`);
203
+
204
+ // Drift detection if --verify flag is set
205
+ if (options.verify) {
206
+ await this._handleStatusVerify(profile, assetManager);
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Perform drift detection for active resources.
212
+ * @param {object} profile - Active profile object with name and config
213
+ * @param {AssetManager} assetManager - AssetManager instance for the profile
214
+ */
215
+ async _handleStatusVerify(profile, assetManager) {
216
+ console.log('\n🔎 Drift Detection:');
217
+
218
+ const activeResources = assetManager.listResources({ status: 'active' });
219
+
220
+ if (activeResources.length === 0) {
221
+ console.log(' No active resources to verify.');
222
+ return;
223
+ }
224
+
225
+ let verified = 0;
226
+ let drifted = 0;
227
+ let unchecked = 0;
228
+
229
+ for (const resource of activeResources) {
230
+ const checkCommand = this.handler._buildDriftCheckCommand(resource);
231
+
232
+ if (!checkCommand) {
233
+ unchecked++;
234
+ continue;
235
+ }
236
+
237
+ try {
238
+ const exists = this.handler._resourceExists(checkCommand, profile.config.awsProfile);
239
+
240
+ if (exists) {
241
+ verified++;
242
+ console.log(` ✅ ${resource.resourceType}: ${resource.resourceId}`);
243
+ } else {
244
+ drifted++;
245
+ assetManager.updateStatus(resource.resourceId, 'unknown');
246
+ console.log(` ⚠️ ${resource.resourceType}: ${resource.resourceId} — not found (status updated to unknown)`);
247
+ }
248
+ } catch {
249
+ unchecked++;
250
+ console.log(` ⚠️ ${resource.resourceType}: ${resource.resourceId} — could not verify (credentials or API unavailable)`);
251
+ }
252
+ }
253
+
254
+ console.log(`\n Drift Summary: ${verified} verified, ${drifted} drifted, ${unchecked} unchecked`);
255
+ }
256
+
257
+ /**
258
+ * Switch the active bootstrap profile.
259
+ * @param {string} profileName - Profile name to activate
260
+ */
261
+ async _handleUse(profileName) {
262
+ if (!profileName) {
263
+ console.log('Usage: ml-container-creator bootstrap use <profile>');
264
+ console.log(' ml-container-creator bootstrap use none (deactivate)');
265
+ return;
266
+ }
267
+
268
+ if (profileName === 'none') {
269
+ this.handler.config.setActiveProfile(null);
270
+ console.log('Active profile cleared. No bootstrap profile is active.');
271
+ return;
272
+ }
273
+
274
+ const profile = this.handler.config.getProfile(profileName);
275
+ if (!profile) {
276
+ const available = this.handler.config.listProfiles();
277
+ console.log(`Profile "${profileName}" not found.`);
278
+ if (available.length > 0) {
279
+ console.log(`Available profiles: ${available.join(', ')}`);
280
+ } else {
281
+ console.log('No profiles configured. Run `ml-container-creator bootstrap` to create one.');
282
+ }
283
+ return;
284
+ }
285
+
286
+ this.handler.config.setActiveProfile(profileName);
287
+ console.log(`Switched active profile to "${profileName}".`);
288
+ }
289
+
290
+ /**
291
+ * List all bootstrap profiles.
292
+ */
293
+ async _handleList() {
294
+ const profiles = this.handler.config.listProfiles();
295
+
296
+ if (profiles.length === 0) {
297
+ console.log('No bootstrap profiles configured.');
298
+ console.log('Run `ml-container-creator bootstrap` to set up shared infrastructure.');
299
+ return;
300
+ }
301
+
302
+ const config = this.handler.config.read();
303
+ const activeProfileName = config ? config.activeProfile : null;
304
+
305
+ console.log('\nBootstrap Profiles:');
306
+ for (const name of profiles) {
307
+ if (name === activeProfileName) {
308
+ console.log(` * ${name} (active)`);
309
+ } else {
310
+ console.log(` ${name}`);
311
+ }
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Remove a bootstrap profile (metadata-only).
317
+ *
318
+ * Only removes the profile entry from config.json and the local manifest file.
319
+ * AWS resources (CloudFormation stack, S3 buckets, ECR repo, IAM roles) are
320
+ * intentionally retained — they may be shared across profiles or still in use.
321
+ *
322
+ * @param {string} profileName - Profile name to remove
323
+ * @param {object} options - Parsed CLI options (e.g., --force)
324
+ */
325
+ async _handleRemove(profileName, options) {
326
+ if (!profileName) {
327
+ console.log('Usage: ml-container-creator bootstrap remove <profile> [--force]');
328
+ return;
329
+ }
330
+
331
+ const profile = this.handler.config.getProfile(profileName);
332
+ if (!profile) {
333
+ console.log(`Profile "${profileName}" not found.`);
334
+ return;
335
+ }
336
+
337
+ // Check for manifest file with active resources
338
+ const assetManager = new AssetManager(profileName);
339
+ const hasManifest = existsSync(assetManager.manifestPath);
340
+
341
+ if (hasManifest) {
342
+ const counts = assetManager.getStatusCounts();
343
+ if (counts.active > 0 && !options.force) {
344
+ console.log(`⚠️ Profile "${profileName}" has ${counts.active} active resource${counts.active === 1 ? '' : 's'} in the deployment manifest.`);
345
+ }
346
+ }
347
+
348
+ if (!options.force) {
349
+ const { confirm } = await this.handler._promptFn([{
350
+ type: 'confirm',
351
+ name: 'confirm',
352
+ message: `Remove bootstrap profile "${profileName}"?`,
353
+ default: false
354
+ }]);
355
+
356
+ if (!confirm) {
357
+ console.log('Removal cancelled.');
358
+ return;
359
+ }
360
+ }
361
+
362
+ // Delete manifest file if it exists
363
+ if (hasManifest) {
364
+ try {
365
+ unlinkSync(assetManager.manifestPath);
366
+ console.log(`Manifest file for "${profileName}" deleted.`);
367
+ } catch {
368
+ console.log(`⚠️ Could not delete manifest file for "${profileName}".`);
369
+ }
370
+ }
371
+
372
+ this.handler.config.removeProfile(profileName);
373
+ console.log(`Profile "${profileName}" removed.`);
374
+
375
+ // Advisory: AWS resources are retained for safety
376
+ const stackName = profile.stackName || `${STACK_NAME_PREFIX}-${profileName}`;
377
+ console.log('');
378
+ console.log('ℹ️ Profile removed from config. AWS resources (CloudFormation stack, S3 buckets, ECR repo, IAM roles) have been retained.');
379
+ console.log(` To delete AWS resources, manually delete the CloudFormation stack "${stackName}" in the AWS console.`);
380
+ }
381
+
382
+ /**
383
+ * Scan AWS for pre-existing MLCC-managed resources and add them to the manifest.
384
+ */
385
+ async _handleScan() {
386
+ const profile = this.handler.config.getActiveProfile();
387
+ if (!profile) {
388
+ console.log('No active bootstrap profile found.');
389
+ console.log('Run `ml-container-creator bootstrap` to set up shared infrastructure.');
390
+ return;
391
+ }
392
+
393
+ console.log(`\n🔍 Scanning for pre-existing resources in ${profile.config.awsRegion}...`);
394
+
395
+ const assetManager = new AssetManager(profile.name);
396
+ const now = new Date().toISOString();
397
+ let discovered = 0;
398
+ let added = 0;
399
+ let skipped = 0;
400
+
401
+ // 1. Query Resource Groups Tagging API for mlcc:managed-by tagged resources
402
+ try {
403
+ console.log('\n Checking tagged resources...');
404
+ const tagResult = this.handler._execAws(
405
+ `resourcegroupstaggingapi get-resources --tag-filters Key=mlcc:managed-by,Values=ml-container-creator --region ${profile.config.awsRegion}`,
406
+ profile.config.awsProfile
407
+ );
408
+
409
+ const taggedResources = tagResult.ResourceTagMappingList || [];
410
+ for (const tagged of taggedResources) {
411
+ discovered++;
412
+ const arn = tagged.ResourceARN;
413
+ const existing = assetManager.getResource(arn);
414
+ if (existing) {
415
+ skipped++;
416
+ continue;
417
+ }
418
+
419
+ const resourceType = this.handler._inferResourceTypeFromArn(arn);
420
+ if (!resourceType) {
421
+ skipped++;
422
+ continue;
423
+ }
424
+
425
+ const project = this.handler._inferProjectFromTags(tagged.Tags) || 'unknown';
426
+
427
+ try {
428
+ assetManager.addResource({
429
+ resourceId: arn,
430
+ resourceType,
431
+ createdAt: now,
432
+ lastUpdatedAt: now,
433
+ project,
434
+ status: 'active',
435
+ metadata: { discoveredBy: 'scan' }
436
+ });
437
+ added++;
438
+ } catch {
439
+ skipped++;
440
+ }
441
+ }
442
+ } catch {
443
+ console.log(' ⚠️ Could not query tagged resources (credentials or API unavailable)');
444
+ }
445
+
446
+ // 2. Query ECR for images in ml-container-creator repository
447
+ try {
448
+ console.log(' Checking ECR images...');
449
+ const ecrResult = this.handler._execAws(
450
+ `ecr describe-images --repository-name ml-container-creator --region ${profile.config.awsRegion}`,
451
+ profile.config.awsProfile
452
+ );
453
+
454
+ const images = ecrResult.imageDetails || [];
455
+ for (const image of images) {
456
+ const tags = image.imageTags || [];
457
+ for (const tag of tags) {
458
+ discovered++;
459
+ const imageUri = `${profile.config.accountId}.dkr.ecr.${profile.config.awsRegion}.amazonaws.com/ml-container-creator:${tag}`;
460
+ const existing = assetManager.getResource(imageUri);
461
+ if (existing) {
462
+ skipped++;
463
+ continue;
464
+ }
465
+
466
+ try {
467
+ assetManager.addResource({
468
+ resourceId: imageUri,
469
+ resourceType: 'ecr-image',
470
+ createdAt: now,
471
+ lastUpdatedAt: now,
472
+ project: this.handler._inferProjectFromImageTag(tag),
473
+ status: 'active',
474
+ metadata: {
475
+ repositoryName: 'ml-container-creator',
476
+ imageTag: tag,
477
+ region: profile.config.awsRegion,
478
+ discoveredBy: 'scan'
479
+ }
480
+ });
481
+ added++;
482
+ } catch {
483
+ skipped++;
484
+ }
485
+ }
486
+ }
487
+ } catch {
488
+ console.log(' ⚠️ Could not query ECR images (credentials or API unavailable)');
489
+ }
490
+
491
+ // 3. Query CodeBuild for *-build-* projects
492
+ try {
493
+ console.log(' Checking CodeBuild projects...');
494
+ const cbResult = this.handler._execAws(
495
+ `codebuild list-projects --region ${profile.config.awsRegion}`,
496
+ profile.config.awsProfile
497
+ );
498
+
499
+ const projects = (cbResult.projects || []).filter(name => name.includes('-build-'));
500
+ for (const projectName of projects) {
501
+ discovered++;
502
+ const arn = `arn:aws:codebuild:${profile.config.awsRegion}:${profile.config.accountId}:project/${projectName}`;
503
+ const existing = assetManager.getResource(arn);
504
+ if (existing) {
505
+ skipped++;
506
+ continue;
507
+ }
508
+
509
+ try {
510
+ assetManager.addResource({
511
+ resourceId: arn,
512
+ resourceType: 'codebuild-project',
513
+ createdAt: now,
514
+ lastUpdatedAt: now,
515
+ project: this.handler._inferProjectFromCodeBuildName(projectName),
516
+ status: 'active',
517
+ metadata: {
518
+ projectName,
519
+ region: profile.config.awsRegion,
520
+ discoveredBy: 'scan'
521
+ }
522
+ });
523
+ added++;
524
+ } catch {
525
+ skipped++;
526
+ }
527
+ }
528
+ } catch {
529
+ console.log(' ⚠️ Could not query CodeBuild projects (credentials or API unavailable)');
530
+ }
531
+
532
+ // Display summary
533
+ console.log(`\n Scan complete: ${discovered} discovered, ${added} added, ${skipped} skipped (duplicates or unsupported)`);
534
+
535
+ if (discovered === 0) {
536
+ console.log(' No MLCC-managed resources were discovered.');
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Prune stale records from the manifest — removes entries with status
542
+ * 'deleted' or 'unknown' that are no longer useful.
543
+ */
544
+ async _handlePrune() {
545
+ const profile = this.handler.config.getActiveProfile();
546
+ if (!profile) {
547
+ console.log('No active bootstrap profile found.');
548
+ return;
549
+ }
550
+
551
+ const assetManager = new AssetManager(profile.name);
552
+
553
+ if (!existsSync(assetManager.manifestPath)) {
554
+ console.log('No deployment tracking data to prune.');
555
+ return;
556
+ }
557
+
558
+ const before = assetManager.listResources();
559
+ const toRemove = before.filter(r => r.status === 'deleted' || r.status === 'unknown');
560
+
561
+ if (toRemove.length === 0) {
562
+ console.log('Nothing to prune — no deleted or unknown records found.');
563
+ return;
564
+ }
565
+
566
+ console.log(`\n🧹 Pruning ${toRemove.length} stale record${toRemove.length === 1 ? '' : 's'}:\n`);
567
+
568
+ for (const resource of toRemove) {
569
+ assetManager.removeResource(resource.resourceId);
570
+ console.log(` 🗑️ [${resource.status}] ${resource.resourceType}: ${resource.resourceId}`);
571
+ }
572
+
573
+ const after = assetManager.listResources();
574
+ console.log(`\n Done. ${toRemove.length} removed, ${after.length} remaining.`);
575
+ }
576
+
577
+ /**
578
+ * Handle sync-schemas subcommand: download service models and verify AWS CLI.
579
+ */
580
+ async _handleSyncSchemas() {
581
+ console.log('\n📦 Schema Sync — Downloading AWS service models...\n');
582
+
583
+ // Verify AWS CLI is installed
584
+ try {
585
+ const version = execSync('aws --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
586
+ console.log(` AWS CLI: ${version}`);
587
+ } catch {
588
+ console.log(' ⚠️ AWS CLI not found.');
589
+ console.log(' Install: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html');
590
+ console.log(' Continuing without AWS CLI verification...\n');
591
+ }
592
+
593
+ // Dynamic import to avoid circular dependencies
594
+ const { syncSchemas } = await import('./schema-sync.js');
595
+ const result = await syncSchemas();
596
+
597
+ if (result.success) {
598
+ console.log('\n ✅ Schema sync complete.');
599
+ } else {
600
+ console.log('\n ⚠️ Schema sync completed with errors (some services may be unavailable).');
601
+ }
602
+
603
+ console.log(` Manifest written: lastSynced = ${result.manifest.lastSynced}\n`);
604
+ }
605
+
606
+ /**
607
+ * Handle sync-model-families subcommand: discover tune-eligible models from
608
+ * the SageMaker JumpStart Hub and update the tune catalog.
609
+ *
610
+ * Requires AWS credentials with sagemaker:ListHubContents and
611
+ * sagemaker:DescribeHubContent permissions.
612
+ */
613
+ async _handleSyncModelFamilies() {
614
+ console.log('\n📦 Sync Model Families — Discovering supported models...\n');
615
+
616
+ // Determine region from active profile or environment
617
+ const profile = this.handler.config.getActiveProfile();
618
+ const region = profile?.config?.awsRegion || process.env.AWS_REGION || 'us-west-2';
619
+
620
+ try {
621
+ const { syncModelFamilies } = await import('../../scripts/sync-model-families.js');
622
+ const result = await syncModelFamilies({ region });
623
+ console.log(`\n✅ Sync complete: ${result.added} new, ${result.total} total models`);
624
+ } catch (err) {
625
+ if (err.name === 'CredentialsProviderError' || err.message?.includes('credentials') || err.message?.includes('Could not load credentials')) {
626
+ console.log('❌ AWS credentials not available or insufficient permissions.');
627
+ console.log('');
628
+ console.log(' Required permissions:');
629
+ console.log(' • sagemaker:ListHubContents');
630
+ console.log(' • sagemaker:DescribeHubContent');
631
+ console.log('');
632
+ console.log(' Ensure your AWS credentials are configured:');
633
+ console.log(' aws configure');
634
+ console.log(' # or set AWS_PROFILE to a profile with SageMaker AI access');
635
+ } else {
636
+ console.log(`❌ Sync failed: ${err.message}`);
637
+ }
638
+ process.exit(1);
639
+ }
640
+ }
641
+ }