@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,519 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Registry Command Handler
6
+ *
7
+ * Handles the `registry` CLI subcommand tree for managing
8
+ * deployment entries in the deployment registry.
9
+ *
10
+ * Subcommands:
11
+ * log Internal: called by do/register
12
+ * list [--backend, --architecture, --model, --instance-type, --status]
13
+ * get <id> Show full entry details
14
+ * remove <id> Remove an entry
15
+ * replay <id> [overrides] Replay a deployment
16
+ * export [id] [--status] Export entries as JSON
17
+ * import <file> [--merge|--replace] Import entries from JSON
18
+ * search [--model, --architecture, --backend, --instance-type]
19
+ */
20
+
21
+ import os from 'node:os'
22
+ import path from 'node:path'
23
+ import { readFileSync } from 'node:fs'
24
+ import { execSync } from 'node:child_process'
25
+ import { fileURLToPath } from 'node:url'
26
+ import DeploymentRegistry, { reconstructReplayFlags } from './deployment-registry.js'
27
+
28
+ const PERSONAL_REGISTRY_PATH = path.join(os.homedir(), '.ml-container-creator', 'registry.json')
29
+ const PROJECT_REGISTRY_PATH = path.join(process.cwd(), '.ml-container-creator', 'registry.json')
30
+
31
+ export default class RegistryCommandHandler {
32
+ constructor() {
33
+ // No external dependencies required
34
+ }
35
+
36
+ /**
37
+ * Dispatch registry subcommands.
38
+ * @param {string[]} args - Remaining positional args after 'registry'
39
+ * @param {object} options - Parsed CLI options
40
+ */
41
+ async handle(args, options) {
42
+ if (args.length === 0) {
43
+ this._showRegistryHelp()
44
+ return
45
+ }
46
+
47
+ const subcommand = args[0].toLowerCase()
48
+
49
+ switch (subcommand) {
50
+ case 'log':
51
+ await this._handleLog(options)
52
+ break
53
+ case 'list':
54
+ this._handleList(options)
55
+ break
56
+ case 'get':
57
+ this._handleGet(args[1])
58
+ break
59
+ case 'remove':
60
+ this._handleRemove(args[1])
61
+ break
62
+ case 'replay':
63
+ await this._handleReplay(args[1], options)
64
+ break
65
+ case 'export':
66
+ this._handleExport(args[1], options)
67
+ break
68
+ case 'import':
69
+ await this._handleImport(args[1], options)
70
+ break
71
+ case 'search':
72
+ this._handleSearch(options)
73
+ break
74
+ default:
75
+ console.log(`Unknown registry subcommand: ${subcommand}`)
76
+ this._showRegistryHelp()
77
+ break
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Internal: log a deployment entry (called by do/register).
83
+ *
84
+ * Parses CLI flags into a Deployment_Entry structure and adds it
85
+ * to the appropriate registry (personal or project-level).
86
+ *
87
+ * @param {object} options - Parsed CLI options from do/register
88
+ */
89
+ async _handleLog(options) {
90
+ const registryPath = options.project ? PROJECT_REGISTRY_PATH : PERSONAL_REGISTRY_PATH
91
+ const registry = new DeploymentRegistry(registryPath)
92
+
93
+ const deploymentConfig = options.deploymentConfig || options['deployment-config'] || ''
94
+ const architecture = options.architecture || ''
95
+ const backend = options.backend || ''
96
+
97
+ const entry = {
98
+ timestamp: new Date().toISOString(),
99
+ status: options.status || 'success',
100
+ deployment: {
101
+ deploymentConfig,
102
+ architecture,
103
+ backend,
104
+ baseImage: options.baseImage || options['base-image'] || null,
105
+ deploymentTarget: options.deploymentTarget || options['deployment-target'] || null,
106
+ buildTarget: options.buildTarget || options['build-target'] || null
107
+ },
108
+ model: {
109
+ modelName: options.modelName || options['model-name'] || null,
110
+ modelFormat: options.modelFormat || options['model-format'] || null
111
+ },
112
+ infrastructure: {
113
+ instanceType: options.instanceType || options['instance-type'] || null,
114
+ region: options.region || null,
115
+ roleArn: options.roleArn || options['role-arn'] || null
116
+ },
117
+ configuration: {
118
+ parameters: {}
119
+ },
120
+ outcome: {
121
+ notes: options.notes || null
122
+ },
123
+ metadata: {
124
+ generatorVersion: options.generatorVersion || options['generator-version'] || 'unknown',
125
+ source: 'local',
126
+ importedFrom: null
127
+ }
128
+ }
129
+
130
+ // Parse parameters from JSON string if provided
131
+ if (options.parameters) {
132
+ try {
133
+ entry.configuration.parameters = typeof options.parameters === 'string'
134
+ ? JSON.parse(options.parameters)
135
+ : options.parameters
136
+ } catch (err) {
137
+ console.log(`Warning: Could not parse parameters JSON: ${err.message}`)
138
+ entry.configuration.parameters = {}
139
+ }
140
+ }
141
+
142
+ try {
143
+ const id = registry.add(entry)
144
+ console.log(`✅ Deployment entry logged successfully.`)
145
+ console.log(` Entry ID: ${id}`)
146
+ console.log(` View details: ml-container-creator registry get ${id}`)
147
+ } catch (err) {
148
+ console.log(`Error logging deployment entry: ${err.message}`)
149
+ }
150
+ }
151
+
152
+ /**
153
+ * registry list [--backend, --architecture, --model, --instance-type, --status]
154
+ *
155
+ * Displays entries from both personal and project-level registries.
156
+ * Supports filtering by backend, architecture, model, instance-type, and status.
157
+ *
158
+ * @param {object} options - Parsed CLI options
159
+ */
160
+ _handleList(options) {
161
+ const filters = this._extractFilters(options)
162
+
163
+ const personalRegistry = new DeploymentRegistry(PERSONAL_REGISTRY_PATH)
164
+ const projectRegistry = new DeploymentRegistry(PROJECT_REGISTRY_PATH)
165
+
166
+ const personalEntries = personalRegistry.list(filters).map(e => ({ ...e, _source: 'personal' }))
167
+ const projectEntries = projectRegistry.list(filters).map(e => ({ ...e, _source: 'project' }))
168
+
169
+ const allEntries = [...personalEntries, ...projectEntries]
170
+
171
+ if (allEntries.length === 0) {
172
+ console.log('No deployment entries found.')
173
+ console.log('Use "./do/register" after a successful deployment to add an entry.')
174
+ return
175
+ }
176
+
177
+ console.log('\nDeployment Registry Entries:\n')
178
+ for (const entry of allEntries) {
179
+ const id = entry.id || '(no id)'
180
+ const ts = entry.timestamp ? entry.timestamp.slice(0, 19) : '(no timestamp)'
181
+ const dc = entry.deployment?.deploymentConfig || '(none)'
182
+ const mn = entry.model?.modelName || '(none)'
183
+ const it = entry.infrastructure?.instanceType || '(none)'
184
+ const st = entry.status || '(none)'
185
+ const src = entry._source === 'project' ? ' [project]' : ''
186
+ console.log(` ${id} ${ts} ${dc} ${mn} ${it} ${st}${src}`)
187
+ }
188
+ console.log('')
189
+ }
190
+
191
+ /**
192
+ * registry get <id>
193
+ *
194
+ * Displays the full entry as formatted JSON.
195
+ *
196
+ * @param {string} id - Entry ID
197
+ */
198
+ _handleGet(id) {
199
+ if (!id) {
200
+ console.log('Usage: ml-container-creator registry get <id>')
201
+ return
202
+ }
203
+
204
+ const entry = this._findEntry(id)
205
+
206
+ if (!entry) {
207
+ console.log(`Error: Entry "${id}" not found.`)
208
+ return
209
+ }
210
+
211
+ console.log(`\nDeployment Entry: ${id}\n`)
212
+ console.log(JSON.stringify(entry, null, 2))
213
+ console.log('')
214
+ }
215
+
216
+ /**
217
+ * registry remove <id>
218
+ *
219
+ * Removes an entry from the registry.
220
+ *
221
+ * @param {string} id - Entry ID
222
+ */
223
+ _handleRemove(id) {
224
+ if (!id) {
225
+ console.log('Usage: ml-container-creator registry remove <id>')
226
+ return
227
+ }
228
+
229
+ const personalRegistry = new DeploymentRegistry(PERSONAL_REGISTRY_PATH)
230
+ if (personalRegistry.remove(id)) {
231
+ console.log(`✅ Entry "${id}" removed from personal registry.`)
232
+ return
233
+ }
234
+
235
+ const projectRegistry = new DeploymentRegistry(PROJECT_REGISTRY_PATH)
236
+ if (projectRegistry.remove(id)) {
237
+ console.log(`✅ Entry "${id}" removed from project registry.`)
238
+ return
239
+ }
240
+
241
+ console.log(`Error: Entry "${id}" not found.`)
242
+ }
243
+
244
+ /**
245
+ * registry replay <id> [overrides]
246
+ *
247
+ * Looks up an entry, reconstructs CLI flags, applies overrides,
248
+ * and invokes the generator with the reconstructed flags.
249
+ *
250
+ * @param {string} id - Entry ID
251
+ * @param {object} options - Parsed CLI options (overrides)
252
+ */
253
+ async _handleReplay(id, options) {
254
+ if (!id) {
255
+ console.log('Usage: ml-container-creator registry replay <id> [--model-name <name>] [--instance-type <type>] ...')
256
+ return
257
+ }
258
+
259
+ const entry = this._findEntry(id)
260
+
261
+ if (!entry) {
262
+ console.log(`Error: Entry "${id}" not found.`)
263
+ return
264
+ }
265
+
266
+ // Build overrides from user-provided CLI options
267
+ const overrides = {}
268
+ const overrideMap = {
269
+ 'deployment-config': '--deployment-config',
270
+ 'deploymentConfig': '--deployment-config',
271
+ 'model-name': '--model-name',
272
+ 'modelName': '--model-name',
273
+ 'instance-type': '--instance-type',
274
+ 'instanceType': '--instance-type',
275
+ 'region': '--region',
276
+ 'model-format': '--model-format',
277
+ 'modelFormat': '--model-format'
278
+ }
279
+
280
+ for (const [optKey, flagKey] of Object.entries(overrideMap)) {
281
+ if (options[optKey] != null) {
282
+ overrides[flagKey] = options[optKey]
283
+ }
284
+ }
285
+
286
+ const flags = reconstructReplayFlags(entry, overrides)
287
+
288
+ console.log(`\nReplaying deployment entry: ${id}`)
289
+ console.log(` Deployment config: ${flags['--deployment-config'] || '(will prompt)'}`)
290
+ console.log(` Model name: ${flags['--model-name'] || '(will prompt)'}`)
291
+ console.log(` Instance type: ${flags['--instance-type'] || '(will prompt)'}`)
292
+ console.log(` Region: ${flags['--region'] || '(will prompt)'}`)
293
+ console.log('')
294
+
295
+ const flagArgs = []
296
+ for (const [flag, value] of Object.entries(flags)) {
297
+ flagArgs.push(flag, value)
298
+ }
299
+
300
+ // Resolve the CLI script path relative to this module
301
+ const __filename = fileURLToPath(import.meta.url)
302
+ const cliPath = path.resolve(path.dirname(__filename), '../../bin/cli.js')
303
+
304
+ try {
305
+ execSync(`ml-container-creator ${flagArgs.join(' ')}`, { stdio: 'inherit' })
306
+ } catch {
307
+ // Fallback: invoke via node + script path if binary is not on PATH
308
+ execSync(`${process.execPath} ${cliPath} ${flagArgs.join(' ')}`, { stdio: 'inherit' })
309
+ }
310
+ }
311
+
312
+ /**
313
+ * registry export [id] [--status]
314
+ *
315
+ * Exports entries as JSON to stdout.
316
+ *
317
+ * @param {string} [id] - Optional entry ID to export a single entry
318
+ * @param {object} options - Parsed CLI options
319
+ */
320
+ _handleExport(id, options) {
321
+ const registryPath = options.project ? PROJECT_REGISTRY_PATH : PERSONAL_REGISTRY_PATH
322
+ const registry = new DeploymentRegistry(registryPath)
323
+
324
+ const exportOptions = {}
325
+ if (options.status) {
326
+ exportOptions.status = options.status
327
+ }
328
+
329
+ const result = registry.exportEntries(id || null, exportOptions)
330
+
331
+ if (result.entries.length === 0) {
332
+ console.log('No entries to export.')
333
+ return
334
+ }
335
+
336
+ console.log(JSON.stringify(result, null, 2))
337
+ }
338
+
339
+ /**
340
+ * registry import <file> [--merge|--replace]
341
+ *
342
+ * Reads a JSON file, validates it, and imports entries into the registry.
343
+ *
344
+ * @param {string} filePath - Path to the import file
345
+ * @param {object} options - Parsed CLI options
346
+ */
347
+ async _handleImport(filePath, options) {
348
+ if (!filePath) {
349
+ console.log('Usage: ml-container-creator registry import <file> [--merge|--replace]')
350
+ return
351
+ }
352
+
353
+ let raw
354
+ try {
355
+ raw = readFileSync(filePath, 'utf8')
356
+ } catch (err) {
357
+ console.log(`Error: File not found: ${filePath}`)
358
+ return
359
+ }
360
+
361
+ let json
362
+ try {
363
+ json = JSON.parse(raw)
364
+ } catch (err) {
365
+ console.log(`Error: Invalid JSON in ${filePath}: ${err.message}`)
366
+ return
367
+ }
368
+
369
+ if (!json.version || !Array.isArray(json.entries)) {
370
+ console.log('Error: Invalid export format — missing required "version" or "entries" fields.')
371
+ return
372
+ }
373
+
374
+ let strategy = 'skip'
375
+ if (options.merge) {
376
+ strategy = 'merge'
377
+ } else if (options.replace) {
378
+ strategy = 'replace'
379
+ }
380
+
381
+ const registryPath = options.project ? PROJECT_REGISTRY_PATH : PERSONAL_REGISTRY_PATH
382
+ const registry = new DeploymentRegistry(registryPath)
383
+
384
+ try {
385
+ const result = registry.importEntries(json, strategy, path.basename(filePath))
386
+ console.log(`\nImport complete:`)
387
+ console.log(` Added: ${result.added}`)
388
+ console.log(` Skipped: ${result.skipped}`)
389
+ console.log(` Conflicts: ${result.conflicts}`)
390
+ console.log('')
391
+ } catch (err) {
392
+ console.log(`Error importing entries: ${err.message}`)
393
+ }
394
+ }
395
+
396
+ /**
397
+ * registry search [--model, --architecture, --backend, --instance-type, --status]
398
+ *
399
+ * Searches across both personal and project-level registries.
400
+ * Uses glob matching for model names.
401
+ *
402
+ * @param {object} options - Parsed CLI options
403
+ */
404
+ _handleSearch(options) {
405
+ const query = this._extractFilters(options)
406
+
407
+ const personalRegistry = new DeploymentRegistry(PERSONAL_REGISTRY_PATH)
408
+ const projectRegistry = new DeploymentRegistry(PROJECT_REGISTRY_PATH)
409
+
410
+ const personalResults = personalRegistry.search(query).map(e => ({ ...e, _source: 'personal' }))
411
+ const projectResults = projectRegistry.search(query).map(e => ({ ...e, _source: 'project' }))
412
+
413
+ const allResults = [...personalResults, ...projectResults]
414
+
415
+ if (allResults.length === 0) {
416
+ console.log('No matching entries found.')
417
+ return
418
+ }
419
+
420
+ console.log(`\nSearch Results (${allResults.length} match${allResults.length === 1 ? '' : 'es'}):\n`)
421
+ for (const entry of allResults) {
422
+ const id = entry.id || '(no id)'
423
+ const ts = entry.timestamp ? entry.timestamp.slice(0, 19) : '(no timestamp)'
424
+ const dc = entry.deployment?.deploymentConfig || '(none)'
425
+ const mn = entry.model?.modelName || '(none)'
426
+ const it = entry.infrastructure?.instanceType || '(none)'
427
+ const st = entry.status || '(none)'
428
+ const src = entry._source === 'project' ? ' [project]' : ''
429
+ console.log(` ${id} ${ts} ${dc} ${mn} ${it} ${st}${src}`)
430
+ }
431
+ console.log('')
432
+ }
433
+
434
+ /**
435
+ * Show registry usage help.
436
+ */
437
+ _showRegistryHelp() {
438
+ console.log(`
439
+ Deployment Registry Management
440
+
441
+ USAGE:
442
+ ml-container-creator registry <subcommand> [options]
443
+
444
+ SUBCOMMANDS:
445
+ list List deployment entries
446
+ get <id> Show full entry details
447
+ remove <id> Remove an entry
448
+ replay <id> [overrides] Replay a deployment configuration
449
+ export [id] [--status <status>] Export entries as JSON
450
+ import <file> [--merge|--replace] Import entries from JSON
451
+ search [filters] Search entries with glob matching
452
+
453
+ FILTER OPTIONS (for list and search):
454
+ --backend <backend> Filter by backend (e.g., vllm, flask)
455
+ --architecture <arch> Filter by architecture (e.g., transformers, http)
456
+ --model <name> Filter by model name (search supports glob patterns)
457
+ --instance-type <type> Filter by instance type
458
+ --status <status> Filter by status (success, partial, failed)
459
+
460
+ REPLAY OPTIONS:
461
+ --deployment-config <config> Override deployment config
462
+ --model-name <name> Override model name
463
+ --instance-type <type> Override instance type
464
+ --region <region> Override region
465
+
466
+ IMPORT OPTIONS:
467
+ --merge Keep both existing and imported on conflict
468
+ --replace Overwrite existing with imported on conflict
469
+
470
+ OTHER OPTIONS:
471
+ --project Use project-level registry instead of personal
472
+
473
+ EXAMPLES:
474
+ ml-container-creator registry list
475
+ ml-container-creator registry list --backend vllm --status success
476
+ ml-container-creator registry get a1b2c3d4
477
+ ml-container-creator registry remove a1b2c3d4
478
+ ml-container-creator registry replay a1b2c3d4
479
+ ml-container-creator registry replay a1b2c3d4 --instance-type ml.g5.2xlarge
480
+ ml-container-creator registry export > my-deployments.json
481
+ ml-container-creator registry export a1b2c3d4
482
+ ml-container-creator registry import team-deployments.json --merge
483
+ ml-container-creator registry search --model "meta-llama/*" --backend vllm
484
+ `)
485
+ }
486
+
487
+ // ── Helper methods ──────────────────────────────────────────────
488
+
489
+ /**
490
+ * Extract filter options from CLI options into a filters object.
491
+ * @param {object} options - Parsed CLI options
492
+ * @returns {object} Filter key-value pairs
493
+ */
494
+ _extractFilters(options) {
495
+ const filters = {}
496
+ if (options.backend) filters.backend = options.backend
497
+ if (options.architecture) filters.architecture = options.architecture
498
+ if (options.model) filters.model = options.model
499
+ if (options['instance-type'] || options.instanceType) {
500
+ filters['instance-type'] = options['instance-type'] || options.instanceType
501
+ }
502
+ if (options.status) filters.status = options.status
503
+ return filters
504
+ }
505
+
506
+ /**
507
+ * Find an entry by ID across both personal and project registries.
508
+ * @param {string} id - Entry ID
509
+ * @returns {object|null} The matching entry, or null
510
+ */
511
+ _findEntry(id) {
512
+ const personalRegistry = new DeploymentRegistry(PERSONAL_REGISTRY_PATH)
513
+ const entry = personalRegistry.get(id)
514
+ if (entry) return entry
515
+
516
+ const projectRegistry = new DeploymentRegistry(PROJECT_REGISTRY_PATH)
517
+ return projectRegistry.get(id)
518
+ }
519
+ }