@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,172 @@
1
+ /**
2
+ * HuggingFace Hub API Client
3
+ *
4
+ * Fetches model metadata from HuggingFace Hub API with graceful error handling.
5
+ * Uses Node.js built-in fetch API (available in Node 18+).
6
+ *
7
+ * Requirements: 11.1, 11.2, 11.3, 11.4, 11.8, 11.10, 11.12
8
+ */
9
+
10
+ export default class HuggingFaceClient {
11
+ constructor(options = {}) {
12
+ this.baseUrl = options.baseUrl || 'https://huggingface.co';
13
+ this.timeout = options.timeout || 5000;
14
+ this.offline = options.offline || false;
15
+ }
16
+
17
+ /**
18
+ * Fetch model metadata from HuggingFace Hub API
19
+ * @param {string} modelId - Model ID (e.g., "meta-llama/Llama-2-7b-chat-hf")
20
+ * @returns {Promise<Object|null>} Model metadata or null on failure
21
+ */
22
+ async fetchModelMetadata(modelId) {
23
+ if (this.offline) {
24
+ return null;
25
+ }
26
+
27
+ try {
28
+ const controller = new AbortController();
29
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
30
+
31
+ const response = await fetch(
32
+ `${this.baseUrl}/api/models/${modelId}`,
33
+ { signal: controller.signal }
34
+ );
35
+
36
+ clearTimeout(timeoutId);
37
+
38
+ if (!response.ok) {
39
+ // Handle rate limits
40
+ if (response.status === 429) {
41
+ console.warn('HuggingFace API rate limit reached');
42
+ return null;
43
+ }
44
+ // Handle not found
45
+ if (response.status === 404) {
46
+ return null;
47
+ }
48
+ // Other errors
49
+ return null;
50
+ }
51
+
52
+ return await response.json();
53
+ } catch (error) {
54
+ // Handle timeout
55
+ if (error.name === 'AbortError') {
56
+ console.warn(`HuggingFace API timeout after ${this.timeout}ms`);
57
+ return null;
58
+ }
59
+ // Handle network errors
60
+ if (error.name === 'TypeError' && error.message.includes('fetch')) {
61
+ console.warn('HuggingFace API network error');
62
+ return null;
63
+ }
64
+ // Other errors - graceful fallback
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Fetch tokenizer config for chat template detection
71
+ * @param {string} modelId - Model ID
72
+ * @returns {Promise<Object|null>} Tokenizer config or null on failure
73
+ */
74
+ async fetchTokenizerConfig(modelId) {
75
+ if (this.offline) {
76
+ return null;
77
+ }
78
+
79
+ try {
80
+ const controller = new AbortController();
81
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
82
+
83
+ const response = await fetch(
84
+ `${this.baseUrl}/${modelId}/resolve/main/tokenizer_config.json`,
85
+ { signal: controller.signal }
86
+ );
87
+
88
+ clearTimeout(timeoutId);
89
+
90
+ if (!response.ok) {
91
+ // Handle rate limits
92
+ if (response.status === 429) {
93
+ console.warn('HuggingFace API rate limit reached');
94
+ return null;
95
+ }
96
+ // Handle not found
97
+ if (response.status === 404) {
98
+ return null;
99
+ }
100
+ // Other errors
101
+ return null;
102
+ }
103
+
104
+ return await response.json();
105
+ } catch (error) {
106
+ // Handle timeout
107
+ if (error.name === 'AbortError') {
108
+ console.warn(`HuggingFace API timeout after ${this.timeout}ms`);
109
+ return null;
110
+ }
111
+ // Handle network errors
112
+ if (error.name === 'TypeError' && error.message.includes('fetch')) {
113
+ console.warn('HuggingFace API network error');
114
+ return null;
115
+ }
116
+ // Other errors - graceful fallback
117
+ return null;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Fetch model config for model architecture
123
+ * @param {string} modelId - Model ID
124
+ * @returns {Promise<Object|null>} Model config or null on failure
125
+ */
126
+ async fetchModelConfig(modelId) {
127
+ if (this.offline) {
128
+ return null;
129
+ }
130
+
131
+ try {
132
+ const controller = new AbortController();
133
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
134
+
135
+ const response = await fetch(
136
+ `${this.baseUrl}/${modelId}/resolve/main/config.json`,
137
+ { signal: controller.signal }
138
+ );
139
+
140
+ clearTimeout(timeoutId);
141
+
142
+ if (!response.ok) {
143
+ // Handle rate limits
144
+ if (response.status === 429) {
145
+ console.warn('HuggingFace API rate limit reached');
146
+ return null;
147
+ }
148
+ // Handle not found
149
+ if (response.status === 404) {
150
+ return null;
151
+ }
152
+ // Other errors
153
+ return null;
154
+ }
155
+
156
+ return await response.json();
157
+ } catch (error) {
158
+ // Handle timeout
159
+ if (error.name === 'AbortError') {
160
+ console.warn(`HuggingFace API timeout after ${this.timeout}ms`);
161
+ return null;
162
+ }
163
+ // Handle network errors
164
+ if (error.name === 'TypeError' && error.message.includes('fetch')) {
165
+ console.warn('HuggingFace API network error');
166
+ return null;
167
+ }
168
+ // Other errors - graceful fallback
169
+ return null;
170
+ }
171
+ }
172
+ }
@@ -0,0 +1,37 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * KEY=VALUE Parser Utility
6
+ *
7
+ * Parses KEY=VALUE strings for --model-env and --server-env CLI flags.
8
+ * Splits only on the first '=' character, allowing values to contain
9
+ * additional '=' characters.
10
+ *
11
+ * Requirements: 3.4, 3.5, 4.4, 4.5
12
+ */
13
+
14
+ import { ValidationError } from './config-manager.js'
15
+
16
+ /**
17
+ * Parse a KEY=VALUE string, splitting only on the first '=' character.
18
+ * @param {string} input - Raw CLI value (e.g., "TENSOR_PARALLEL_SIZE=4")
19
+ * @returns {{ key: string, value: string }}
20
+ * @throws {ValidationError} if no '=' is present
21
+ */
22
+ export function parseKeyValue(input) {
23
+ const idx = input.indexOf('=')
24
+
25
+ if (idx === -1) {
26
+ throw new ValidationError(
27
+ `Invalid format for env var: expected KEY=VALUE, got '${input}'`,
28
+ 'env',
29
+ input
30
+ )
31
+ }
32
+
33
+ const key = input.substring(0, idx)
34
+ const value = input.substring(idx + 1)
35
+
36
+ return { key, value }
37
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Known Flags Validator Strategy
3
+ *
4
+ * Validates environment variables against a registry of known flags for each framework.
5
+ * Checks variable names, types, and range constraints.
6
+ *
7
+ * Requirements: 13.9, 13.13, 13.14, 13.15
8
+ */
9
+ export default class KnownFlagsValidator {
10
+ /**
11
+ * Create a new KnownFlagsValidator.
12
+ *
13
+ * @param {Object} frameworkFlags - Framework flags registry
14
+ */
15
+ constructor(frameworkFlags = {}) {
16
+ this.frameworkFlags = frameworkFlags;
17
+ this.name = 'known-flags-registry';
18
+ }
19
+
20
+ /**
21
+ * Validate environment variables against known flags registry.
22
+ *
23
+ * @param {string} framework - Framework name
24
+ * @param {string} version - Framework version
25
+ * @param {Object} envVars - Environment variables to validate
26
+ * @returns {Object} ValidationResult
27
+ * @returns {Array<Object>} ValidationResult.warnings - Warning messages
28
+ * @returns {Array<Object>} ValidationResult.errors - Error messages
29
+ */
30
+ async validate(framework, version, envVars) {
31
+ const warnings = [];
32
+ const errors = [];
33
+
34
+ // Get known flags for this framework version
35
+ const knownFlags = this.getKnownFlags(framework, version);
36
+
37
+ if (!knownFlags || Object.keys(knownFlags).length === 0) {
38
+ // No known flags data available
39
+ return { warnings, errors };
40
+ }
41
+
42
+ // Validate each environment variable
43
+ for (const [key, value] of Object.entries(envVars)) {
44
+ const flagSpec = knownFlags[key];
45
+
46
+ if (!flagSpec) {
47
+ // Unknown flag - might be valid but not in our registry
48
+ warnings.push({
49
+ key,
50
+ message: `Unknown environment variable '${key}' for ${framework} ${version}`
51
+ });
52
+ continue;
53
+ }
54
+
55
+ // Check if flag is deprecated
56
+ if (flagSpec.deprecated) {
57
+ warnings.push({
58
+ key,
59
+ message: `Environment variable '${key}' is deprecated. ${flagSpec.deprecationMessage || ''}`
60
+ });
61
+
62
+ if (flagSpec.replacement) {
63
+ warnings.push({
64
+ key,
65
+ message: `Consider using '${flagSpec.replacement}' instead of '${key}'`
66
+ });
67
+ }
68
+ }
69
+
70
+ // Validate type
71
+ const typeError = this.validateType(key, value, flagSpec.type);
72
+ if (typeError) {
73
+ errors.push(typeError);
74
+ continue; // Skip range validation if type is wrong
75
+ }
76
+
77
+ // Validate range constraints
78
+ const rangeError = this.validateRange(key, value, flagSpec);
79
+ if (rangeError) {
80
+ errors.push(rangeError);
81
+ }
82
+ }
83
+
84
+ return { warnings, errors };
85
+ }
86
+
87
+ /**
88
+ * Get known flags for a framework version.
89
+ *
90
+ * @param {string} framework - Framework name
91
+ * @param {string} version - Framework version
92
+ * @returns {Object|null} Known flags specification
93
+ * @private
94
+ */
95
+ getKnownFlags(framework, version) {
96
+ if (!this.frameworkFlags[framework]) {
97
+ return null;
98
+ }
99
+
100
+ // Try exact version match first
101
+ if (this.frameworkFlags[framework][version]) {
102
+ return this.frameworkFlags[framework][version];
103
+ }
104
+
105
+ // Try to find closest version (simplified - just use 'default' if available)
106
+ if (this.frameworkFlags[framework].default) {
107
+ return this.frameworkFlags[framework].default;
108
+ }
109
+
110
+ return null;
111
+ }
112
+
113
+ /**
114
+ * Validate environment variable type.
115
+ *
116
+ * @param {string} key - Variable name
117
+ * @param {string} value - Variable value
118
+ * @param {string} expectedType - Expected type (integer, float, string, boolean)
119
+ * @returns {Object|null} Error object or null if valid
120
+ * @private
121
+ */
122
+ validateType(key, value, expectedType) {
123
+ if (!expectedType) {
124
+ return null; // No type constraint
125
+ }
126
+
127
+ switch (expectedType) {
128
+ case 'integer':
129
+ if (!/^-?\d+$/.test(value)) {
130
+ return {
131
+ key,
132
+ message: `Environment variable '${key}' must be an integer, got '${value}'`
133
+ };
134
+ }
135
+ break;
136
+
137
+ case 'float':
138
+ if (!/^-?\d+(\.\d+)?$/.test(value)) {
139
+ return {
140
+ key,
141
+ message: `Environment variable '${key}' must be a float, got '${value}'`
142
+ };
143
+ }
144
+ break;
145
+
146
+ case 'boolean':
147
+ if (!['true', 'false', '0', '1', 'yes', 'no'].includes(value.toLowerCase())) {
148
+ return {
149
+ key,
150
+ message: `Environment variable '${key}' must be a boolean (true/false, 0/1, yes/no), got '${value}'`
151
+ };
152
+ }
153
+ break;
154
+
155
+ case 'string':
156
+ // String is always valid
157
+ break;
158
+
159
+ default:
160
+ // Unknown type - skip validation
161
+ break;
162
+ }
163
+
164
+ return null;
165
+ }
166
+
167
+ /**
168
+ * Validate environment variable range constraints.
169
+ *
170
+ * @param {string} key - Variable name
171
+ * @param {string} value - Variable value
172
+ * @param {Object} flagSpec - Flag specification with min/max constraints
173
+ * @returns {Object|null} Error object or null if valid
174
+ * @private
175
+ */
176
+ validateRange(key, value, flagSpec) {
177
+ // Only validate range for numeric types
178
+ if (flagSpec.type !== 'integer' && flagSpec.type !== 'float') {
179
+ return null;
180
+ }
181
+
182
+ const numValue = parseFloat(value);
183
+
184
+ if (flagSpec.min !== undefined && numValue < flagSpec.min) {
185
+ return {
186
+ key,
187
+ message: `Environment variable '${key}' must be >= ${flagSpec.min}, got ${value}`
188
+ };
189
+ }
190
+
191
+ if (flagSpec.max !== undefined && numValue > flagSpec.max) {
192
+ return {
193
+ key,
194
+ message: `Environment variable '${key}' must be <= ${flagSpec.max}, got ${value}`
195
+ };
196
+ }
197
+
198
+ return null;
199
+ }
200
+ }
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4
+ // SPDX-License-Identifier: Apache-2.0
5
+
6
+ /**
7
+ * Manifest CLI Wrapper
8
+ *
9
+ * A standalone Node.js script invoked by the `do/manifest` shell helper.
10
+ * Reads the bootstrap config to resolve the active profile, instantiates
11
+ * AssetManager, and dispatches commands.
12
+ *
13
+ * Usage:
14
+ * node do/lib/manifest-cli.js add --type <type> --id <id> --project <project> [--meta <json>]
15
+ * node do/lib/manifest-cli.js delete --id <id>
16
+ * node do/lib/manifest-cli.js list [--project <project>] [--status <status>] [--type <type>]
17
+ *
18
+ * Validates: Requirements 9.1–9.7
19
+ */
20
+
21
+ import AssetManager, { VALID_RESOURCE_TYPES, VALID_STATUSES } from './asset-manager.js'
22
+ import BootstrapConfig from './bootstrap-config.js'
23
+
24
+ /**
25
+ * Parse command-line arguments into a map of flag → value pairs.
26
+ * Supports --flag value syntax. Positional args are ignored.
27
+ *
28
+ * @param {string[]} argv - The raw process.argv array
29
+ * @returns {{ subcommand: string|null, flags: Object }}
30
+ */
31
+ function parseArgs(argv) {
32
+ // argv[0] = node, argv[1] = script path, argv[2] = subcommand, argv[3..] = flags
33
+ const args = argv.slice(2)
34
+ const subcommand = args.length > 0 && !args[0].startsWith('--') ? args[0] : null
35
+ const flags = {}
36
+
37
+ for (let i = subcommand ? 1 : 0; i < args.length; i++) {
38
+ if (args[i].startsWith('--') && i + 1 < args.length) {
39
+ const key = args[i].slice(2)
40
+ flags[key] = args[i + 1]
41
+ i++
42
+ }
43
+ }
44
+
45
+ return { subcommand, flags }
46
+ }
47
+
48
+ /**
49
+ * Print usage information and exit.
50
+ */
51
+ function printUsage() {
52
+ console.log('Usage:')
53
+ console.log(' manifest add --type <resourceType> --id <resourceId> --project <projectName> [--meta <json>]')
54
+ console.log(' manifest delete --id <resourceId>')
55
+ console.log(' manifest list [--project <project>] [--status <status>] [--type <type>]')
56
+ console.log(' manifest prune')
57
+ console.log('')
58
+ console.log('Valid resource types:')
59
+ console.log(` ${VALID_RESOURCE_TYPES.join(', ')}`)
60
+ console.log('')
61
+ console.log('Valid statuses:')
62
+ console.log(` ${VALID_STATUSES.join(', ')}`)
63
+ }
64
+
65
+ /**
66
+ * Format resources as a table for console output.
67
+ *
68
+ * @param {Array<Object>} resources - Array of Asset_Records
69
+ */
70
+ function printResourceTable(resources) {
71
+ if (resources.length === 0) {
72
+ console.log('No resources found.')
73
+ return
74
+ }
75
+
76
+ // Header
77
+ const header = ['Type', 'Resource ID', 'Project', 'Status', 'Created At']
78
+ const widths = header.map(h => h.length)
79
+
80
+ // Calculate column widths
81
+ for (const r of resources) {
82
+ widths[0] = Math.max(widths[0], (r.resourceType || '').length)
83
+ widths[1] = Math.max(widths[1], Math.min((r.resourceId || '').length, 60))
84
+ widths[2] = Math.max(widths[2], (r.project || '').length)
85
+ widths[3] = Math.max(widths[3], (r.status || '').length)
86
+ widths[4] = Math.max(widths[4], (r.createdAt || '').length)
87
+ }
88
+
89
+ const pad = (str, width) => String(str).padEnd(width)
90
+ const separator = widths.map(w => '-'.repeat(w)).join(' ')
91
+
92
+ console.log(header.map((h, i) => pad(h, widths[i])).join(' '))
93
+ console.log(separator)
94
+
95
+ for (const r of resources) {
96
+ const id = (r.resourceId || '').length > 60
97
+ ? r.resourceId.slice(0, 57) + '...'
98
+ : r.resourceId || ''
99
+ console.log([
100
+ pad(r.resourceType || '', widths[0]),
101
+ pad(id, widths[1]),
102
+ pad(r.project || '', widths[2]),
103
+ pad(r.status || '', widths[3]),
104
+ pad(r.createdAt || '', widths[4])
105
+ ].join(' '))
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Handle the `add` subcommand.
111
+ *
112
+ * @param {Object} flags - Parsed flags
113
+ * @param {AssetManager} assetManager - The AssetManager instance
114
+ */
115
+ function handleAdd(flags, assetManager) {
116
+ const { type, id, project, meta } = flags
117
+
118
+ if (!type || !id || !project) {
119
+ console.error('Error: --type, --id, and --project are required for the add command.')
120
+ console.log('')
121
+ printUsage()
122
+ process.exitCode = 1
123
+ return
124
+ }
125
+
126
+ if (!VALID_RESOURCE_TYPES.includes(type)) {
127
+ console.error(`Error: Invalid resource type "${type}".`)
128
+ console.error(`Valid types: ${VALID_RESOURCE_TYPES.join(', ')}`)
129
+ process.exitCode = 1
130
+ return
131
+ }
132
+
133
+ let metadata = {}
134
+ if (meta) {
135
+ try {
136
+ metadata = JSON.parse(meta)
137
+ } catch (err) {
138
+ console.error(`Error: Invalid JSON for --meta: ${err.message}`)
139
+ process.exitCode = 1
140
+ return
141
+ }
142
+ }
143
+
144
+ const now = new Date().toISOString()
145
+ const record = {
146
+ resourceId: id,
147
+ resourceType: type,
148
+ createdAt: now,
149
+ lastUpdatedAt: now,
150
+ project,
151
+ status: 'active',
152
+ metadata
153
+ }
154
+
155
+ assetManager.addResource(record)
156
+ console.log(`Added ${type}: ${id}`)
157
+ }
158
+
159
+ /**
160
+ * Handle the `delete` subcommand.
161
+ *
162
+ * @param {Object} flags - Parsed flags
163
+ * @param {AssetManager} assetManager - The AssetManager instance
164
+ */
165
+ function handleDelete(flags, assetManager) {
166
+ const { id } = flags
167
+
168
+ if (!id) {
169
+ console.error('Error: --id is required for the delete command.')
170
+ console.log('')
171
+ printUsage()
172
+ process.exitCode = 1
173
+ return
174
+ }
175
+
176
+ const updated = assetManager.updateStatus(id, 'deleted')
177
+ if (updated) {
178
+ console.log(`Marked as deleted: ${id}`)
179
+ } else {
180
+ console.log(`Resource not found in manifest: ${id}`)
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Handle the `list` subcommand.
186
+ *
187
+ * @param {Object} flags - Parsed flags
188
+ * @param {AssetManager} assetManager - The AssetManager instance
189
+ */
190
+ function handleList(flags, assetManager) {
191
+ const filters = {}
192
+ if (flags.project) filters.project = flags.project
193
+ if (flags.status) filters.status = flags.status
194
+ if (flags.type) filters.resourceType = flags.type
195
+
196
+ const resources = assetManager.listResources(filters)
197
+ printResourceTable(resources)
198
+ }
199
+
200
+ /**
201
+ * Handle the `prune` subcommand — remove deleted and unknown records.
202
+ *
203
+ * @param {AssetManager} assetManager - The AssetManager instance
204
+ */
205
+ function handlePrune(assetManager) {
206
+ const all = assetManager.listResources()
207
+ const stale = all.filter(r => r.status === 'deleted' || r.status === 'unknown')
208
+
209
+ if (stale.length === 0) {
210
+ console.log('Nothing to prune — no deleted or unknown records found.')
211
+ return
212
+ }
213
+
214
+ for (const resource of stale) {
215
+ assetManager.removeResource(resource.resourceId)
216
+ console.log(` 🗑️ [${resource.status}] ${resource.resourceType}: ${resource.resourceId}`)
217
+ }
218
+
219
+ const remaining = assetManager.listResources()
220
+ console.log(`\nPruned ${stale.length} record${stale.length === 1 ? '' : 's'}. ${remaining.length} remaining.`)
221
+ }
222
+
223
+ /**
224
+ * Main entry point. Resolves the active bootstrap profile,
225
+ * instantiates AssetManager, and dispatches the subcommand.
226
+ *
227
+ * @param {string[]} argv - The raw process.argv array
228
+ */
229
+ export function main(argv) {
230
+ const { subcommand, flags } = parseArgs(argv)
231
+
232
+ if (!subcommand) {
233
+ printUsage()
234
+ process.exitCode = 1
235
+ return
236
+ }
237
+
238
+ // Resolve active bootstrap profile
239
+ const bootstrapConfig = new BootstrapConfig()
240
+ const activeProfile = bootstrapConfig.getActiveProfile()
241
+
242
+ if (!activeProfile) {
243
+ console.warn('Warning: No active bootstrap profile configured. Skipping manifest operation.')
244
+ console.warn('Run "ml-container-creator bootstrap" to configure a profile.')
245
+ return
246
+ }
247
+
248
+ const assetManager = new AssetManager(activeProfile.name)
249
+
250
+ switch (subcommand) {
251
+ case 'add':
252
+ handleAdd(flags, assetManager)
253
+ break
254
+ case 'delete':
255
+ handleDelete(flags, assetManager)
256
+ break
257
+ case 'list':
258
+ handleList(flags, assetManager)
259
+ break
260
+ case 'prune':
261
+ handlePrune(assetManager)
262
+ break
263
+ default:
264
+ console.error(`Unknown subcommand: ${subcommand}`)
265
+ console.log('')
266
+ printUsage()
267
+ process.exitCode = 1
268
+ break
269
+ }
270
+ }
271
+
272
+ // Run when executed directly
273
+ const isDirectExecution = process.argv[1] && (
274
+ process.argv[1].endsWith('manifest-cli.js') ||
275
+ process.argv[1].endsWith('do/lib/manifest-cli.js')
276
+ )
277
+
278
+ if (isDirectExecution) {
279
+ main(process.argv)
280
+ }