@aws/ml-container-creator 0.2.1 → 0.2.2
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.
- package/bin/cli.js +88 -86
- package/config/bootstrap-stack.json +211 -0
- package/config/parameter-schema.json +88 -0
- package/infra/ci-harness/bin/ci-harness.ts +26 -0
- package/infra/ci-harness/buildspec.yml +352 -0
- package/infra/ci-harness/cdk.json +27 -0
- package/infra/ci-harness/lambda/scanner/index.ts +199 -0
- package/infra/ci-harness/lib/ci-harness-stack.ts +609 -0
- package/infra/ci-harness/package-lock.json +3979 -0
- package/infra/ci-harness/package.json +32 -0
- package/infra/ci-harness/tsconfig.json +38 -0
- package/package.json +13 -3
- package/src/app.js +318 -318
- package/src/copy-tpl.js +19 -19
- package/src/lib/asset-manager.js +74 -74
- package/src/lib/aws-profile-parser.js +45 -45
- package/src/lib/bootstrap-command-handler.js +560 -547
- package/src/lib/bootstrap-config.js +45 -45
- package/src/lib/ci-register-helpers.js +19 -19
- package/src/lib/ci-report-helpers.js +37 -37
- package/src/lib/ci-stage-helpers.js +49 -49
- package/src/lib/comment-generator.js +4 -4
- package/src/lib/config-manager.js +105 -105
- package/src/lib/deployment-config-resolver.js +10 -10
- package/src/lib/deployment-registry.js +153 -153
- package/src/lib/engine-prefix-resolver.js +8 -8
- package/src/lib/key-value-parser.js +6 -6
- package/src/lib/manifest-cli.js +108 -108
- package/src/lib/prompt-runner.js +224 -224
- package/src/lib/prompts.js +121 -121
- package/src/lib/registry-command-handler.js +174 -174
- package/src/lib/registry-loader.js +52 -52
- package/src/lib/sensitive-redactor.js +9 -9
- package/src/lib/template-engine.js +1 -1
- package/src/lib/template-manager.js +62 -62
- package/src/prompt-adapter.js +18 -18
package/src/copy-tpl.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import ejs from 'ejs'
|
|
5
|
-
import { globSync } from 'tinyglobby'
|
|
6
|
-
import fs from 'fs'
|
|
7
|
-
import path from 'path'
|
|
4
|
+
import ejs from 'ejs';
|
|
5
|
+
import { globSync } from 'tinyglobby';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Binary file extensions that should be copied without EJS rendering.
|
|
@@ -16,7 +16,7 @@ const BINARY_EXTENSIONS = new Set([
|
|
|
16
16
|
'.pdf',
|
|
17
17
|
'.exe', '.dll', '.so', '.dylib',
|
|
18
18
|
'.pyc', '.pyo', '.class', '.jar', '.war', '.ear'
|
|
19
|
-
])
|
|
19
|
+
]);
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Determines whether a file is binary based on its extension.
|
|
@@ -25,8 +25,8 @@ const BINARY_EXTENSIONS = new Set([
|
|
|
25
25
|
* @returns {boolean} True if the file has a known binary extension
|
|
26
26
|
*/
|
|
27
27
|
function isBinaryFile(filePath) {
|
|
28
|
-
const ext = path.extname(filePath).toLowerCase()
|
|
29
|
-
return BINARY_EXTENSIONS.has(ext)
|
|
28
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
29
|
+
return BINARY_EXTENSIONS.has(ext);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
@@ -47,31 +47,31 @@ export function copyTpl(templateDir, destDir, vars, ignorePatterns = []) {
|
|
|
47
47
|
ignore: ignorePatterns,
|
|
48
48
|
dot: true,
|
|
49
49
|
onlyFiles: true
|
|
50
|
-
})
|
|
50
|
+
});
|
|
51
51
|
|
|
52
52
|
for (const file of files) {
|
|
53
|
-
const src = path.join(templateDir, file)
|
|
54
|
-
const dest = path.join(destDir, file)
|
|
53
|
+
const src = path.join(templateDir, file);
|
|
54
|
+
const dest = path.join(destDir, file);
|
|
55
55
|
|
|
56
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
56
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
57
57
|
|
|
58
58
|
if (isBinaryFile(file)) {
|
|
59
|
-
fs.copyFileSync(src, dest)
|
|
60
|
-
continue
|
|
59
|
+
fs.copyFileSync(src, dest);
|
|
60
|
+
continue;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
const content = fs.readFileSync(src, 'utf8')
|
|
63
|
+
const content = fs.readFileSync(src, 'utf8');
|
|
64
64
|
|
|
65
|
-
let rendered
|
|
65
|
+
let rendered;
|
|
66
66
|
try {
|
|
67
|
-
rendered = ejs.render(content, vars, { filename: src })
|
|
67
|
+
rendered = ejs.render(content, vars, { filename: src });
|
|
68
68
|
} catch (err) {
|
|
69
|
-
const line = err.line ? ` (line ${err.line})` : ''
|
|
69
|
+
const line = err.line ? ` (line ${err.line})` : '';
|
|
70
70
|
throw new Error(
|
|
71
71
|
`EJS rendering failed for "${file}"${line}: ${err.message}`
|
|
72
|
-
)
|
|
72
|
+
);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
fs.writeFileSync(dest, rendered)
|
|
75
|
+
fs.writeFileSync(dest, rendered);
|
|
76
76
|
}
|
|
77
77
|
}
|
package/src/lib/asset-manager.js
CHANGED
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
* }
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
|
|
33
|
-
import { join, dirname } from 'node:path'
|
|
34
|
-
import { homedir } from 'node:os'
|
|
32
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
33
|
+
import { join, dirname } from 'node:path';
|
|
34
|
+
import { homedir } from 'node:os';
|
|
35
35
|
|
|
36
|
-
const SCHEMA_VERSION = '2026-05-04'
|
|
36
|
+
const SCHEMA_VERSION = '2026-05-04';
|
|
37
37
|
|
|
38
38
|
const VALID_RESOURCE_TYPES = [
|
|
39
39
|
'sagemaker-endpoint',
|
|
@@ -48,11 +48,11 @@ const VALID_RESOURCE_TYPES = [
|
|
|
48
48
|
'sns-topic',
|
|
49
49
|
'k8s-deployment',
|
|
50
50
|
'k8s-service'
|
|
51
|
-
]
|
|
51
|
+
];
|
|
52
52
|
|
|
53
|
-
const VALID_STATUSES = ['active', 'deleted', 'unknown']
|
|
53
|
+
const VALID_STATUSES = ['active', 'deleted', 'unknown'];
|
|
54
54
|
|
|
55
|
-
export { SCHEMA_VERSION, VALID_RESOURCE_TYPES, VALID_STATUSES }
|
|
55
|
+
export { SCHEMA_VERSION, VALID_RESOURCE_TYPES, VALID_STATUSES };
|
|
56
56
|
|
|
57
57
|
export default class AssetManager {
|
|
58
58
|
/**
|
|
@@ -62,8 +62,8 @@ export default class AssetManager {
|
|
|
62
62
|
* Defaults to ~/.ml-container-creator
|
|
63
63
|
*/
|
|
64
64
|
constructor(profileName, options = {}) {
|
|
65
|
-
this.profileName = profileName
|
|
66
|
-
this.configDir = options.configDir || join(homedir(), '.ml-container-creator')
|
|
65
|
+
this.profileName = profileName;
|
|
66
|
+
this.configDir = options.configDir || join(homedir(), '.ml-container-creator');
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
@@ -72,7 +72,7 @@ export default class AssetManager {
|
|
|
72
72
|
* @returns {string} Absolute path to the manifest JSON file
|
|
73
73
|
*/
|
|
74
74
|
get manifestPath() {
|
|
75
|
-
return join(this.configDir, 'manifests', `${this.profileName}.json`)
|
|
75
|
+
return join(this.configDir, 'manifests', `${this.profileName}.json`);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
@@ -87,34 +87,34 @@ export default class AssetManager {
|
|
|
87
87
|
*/
|
|
88
88
|
_readManifest() {
|
|
89
89
|
if (!existsSync(this.manifestPath)) {
|
|
90
|
-
return { schemaVersion: SCHEMA_VERSION, resources: [] }
|
|
90
|
+
return { schemaVersion: SCHEMA_VERSION, resources: [] };
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
const raw = readFileSync(this.manifestPath, 'utf8')
|
|
93
|
+
const raw = readFileSync(this.manifestPath, 'utf8');
|
|
94
94
|
|
|
95
|
-
let data
|
|
95
|
+
let data;
|
|
96
96
|
try {
|
|
97
|
-
data = JSON.parse(raw)
|
|
97
|
+
data = JSON.parse(raw);
|
|
98
98
|
} catch (err) {
|
|
99
99
|
throw new Error(
|
|
100
100
|
`Invalid JSON in manifest file ${this.manifestPath}: ${err.message}`
|
|
101
|
-
)
|
|
101
|
+
);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
if (!data.schemaVersion) {
|
|
105
105
|
console.warn(
|
|
106
106
|
`Warning: Manifest file ${this.manifestPath} has no schemaVersion. Attempting best-effort read.`
|
|
107
|
-
)
|
|
107
|
+
);
|
|
108
108
|
} else if (data.schemaVersion !== SCHEMA_VERSION) {
|
|
109
109
|
console.warn(
|
|
110
110
|
`Warning: Manifest file ${this.manifestPath} has unrecognized schemaVersion "${data.schemaVersion}". Attempting best-effort read.`
|
|
111
|
-
)
|
|
111
|
+
);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
return {
|
|
115
115
|
schemaVersion: data.schemaVersion || SCHEMA_VERSION,
|
|
116
116
|
resources: Array.isArray(data.resources) ? data.resources : []
|
|
117
|
-
}
|
|
117
|
+
};
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/**
|
|
@@ -125,15 +125,15 @@ export default class AssetManager {
|
|
|
125
125
|
* @param {{ schemaVersion: string, resources: Array<Object> }} manifest
|
|
126
126
|
*/
|
|
127
127
|
_writeManifest(manifest) {
|
|
128
|
-
const dir = dirname(this.manifestPath)
|
|
128
|
+
const dir = dirname(this.manifestPath);
|
|
129
129
|
if (!existsSync(dir)) {
|
|
130
|
-
mkdirSync(dir, { recursive: true })
|
|
130
|
+
mkdirSync(dir, { recursive: true });
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
writeFileSync(
|
|
134
134
|
this.manifestPath,
|
|
135
135
|
`${JSON.stringify(manifest, null, 2)}\n`
|
|
136
|
-
)
|
|
136
|
+
);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/**
|
|
@@ -150,7 +150,7 @@ export default class AssetManager {
|
|
|
150
150
|
* @returns {{ valid: boolean, errors: string[] }}
|
|
151
151
|
*/
|
|
152
152
|
_validateRecord(record) {
|
|
153
|
-
const errors = []
|
|
153
|
+
const errors = [];
|
|
154
154
|
|
|
155
155
|
const requiredFields = [
|
|
156
156
|
'resourceId',
|
|
@@ -160,11 +160,11 @@ export default class AssetManager {
|
|
|
160
160
|
'project',
|
|
161
161
|
'status',
|
|
162
162
|
'metadata'
|
|
163
|
-
]
|
|
163
|
+
];
|
|
164
164
|
|
|
165
165
|
for (const field of requiredFields) {
|
|
166
166
|
if (record[field] === undefined || record[field] === null) {
|
|
167
|
-
errors.push(`Missing required field: ${field}`)
|
|
167
|
+
errors.push(`Missing required field: ${field}`);
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
@@ -172,7 +172,7 @@ export default class AssetManager {
|
|
|
172
172
|
if (!VALID_RESOURCE_TYPES.includes(record.resourceType)) {
|
|
173
173
|
errors.push(
|
|
174
174
|
`Invalid resourceType: "${record.resourceType}". Must be one of: ${VALID_RESOURCE_TYPES.join(', ')}`
|
|
175
|
-
)
|
|
175
|
+
);
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
@@ -180,7 +180,7 @@ export default class AssetManager {
|
|
|
180
180
|
if (!VALID_STATUSES.includes(record.status)) {
|
|
181
181
|
errors.push(
|
|
182
182
|
`Invalid status: "${record.status}". Must be one of: ${VALID_STATUSES.join(', ')}`
|
|
183
|
-
)
|
|
183
|
+
);
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
|
|
@@ -188,7 +188,7 @@ export default class AssetManager {
|
|
|
188
188
|
if (!_isValidISO8601(record.createdAt)) {
|
|
189
189
|
errors.push(
|
|
190
190
|
`Invalid createdAt: "${record.createdAt}". Must be a valid ISO 8601 timestamp.`
|
|
191
|
-
)
|
|
191
|
+
);
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
|
|
@@ -196,17 +196,17 @@ export default class AssetManager {
|
|
|
196
196
|
if (!_isValidISO8601(record.lastUpdatedAt)) {
|
|
197
197
|
errors.push(
|
|
198
198
|
`Invalid lastUpdatedAt: "${record.lastUpdatedAt}". Must be a valid ISO 8601 timestamp.`
|
|
199
|
-
)
|
|
199
|
+
);
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
if (record.metadata !== undefined && record.metadata !== null) {
|
|
204
204
|
if (typeof record.metadata !== 'object' || Array.isArray(record.metadata)) {
|
|
205
|
-
errors.push('Invalid metadata: must be a non-null object.')
|
|
205
|
+
errors.push('Invalid metadata: must be a non-null object.');
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
return { valid: errors.length === 0, errors }
|
|
209
|
+
return { valid: errors.length === 0, errors };
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
/**
|
|
@@ -219,24 +219,24 @@ export default class AssetManager {
|
|
|
219
219
|
* @throws {Error} If the record fails validation
|
|
220
220
|
*/
|
|
221
221
|
addResource(record) {
|
|
222
|
-
const { valid, errors } = this._validateRecord(record)
|
|
222
|
+
const { valid, errors } = this._validateRecord(record);
|
|
223
223
|
if (!valid) {
|
|
224
|
-
throw new Error(`Invalid asset record: ${errors.join('; ')}`)
|
|
224
|
+
throw new Error(`Invalid asset record: ${errors.join('; ')}`);
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
const manifest = this._readManifest()
|
|
227
|
+
const manifest = this._readManifest();
|
|
228
228
|
const existingIndex = manifest.resources.findIndex(
|
|
229
229
|
r => r.resourceId === record.resourceId
|
|
230
|
-
)
|
|
230
|
+
);
|
|
231
231
|
|
|
232
232
|
if (existingIndex !== -1) {
|
|
233
|
-
manifest.resources[existingIndex].lastUpdatedAt = record.lastUpdatedAt
|
|
234
|
-
manifest.resources[existingIndex].status = record.status
|
|
233
|
+
manifest.resources[existingIndex].lastUpdatedAt = record.lastUpdatedAt;
|
|
234
|
+
manifest.resources[existingIndex].status = record.status;
|
|
235
235
|
} else {
|
|
236
|
-
manifest.resources.push(record)
|
|
236
|
+
manifest.resources.push(record);
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
this._writeManifest(manifest)
|
|
239
|
+
this._writeManifest(manifest);
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
/**
|
|
@@ -247,17 +247,17 @@ export default class AssetManager {
|
|
|
247
247
|
* @returns {boolean} true if the resource was found and updated, false otherwise
|
|
248
248
|
*/
|
|
249
249
|
updateStatus(resourceId, newStatus) {
|
|
250
|
-
const manifest = this._readManifest()
|
|
251
|
-
const resource = manifest.resources.find(r => r.resourceId === resourceId)
|
|
250
|
+
const manifest = this._readManifest();
|
|
251
|
+
const resource = manifest.resources.find(r => r.resourceId === resourceId);
|
|
252
252
|
|
|
253
253
|
if (!resource) {
|
|
254
|
-
return false
|
|
254
|
+
return false;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
resource.status = newStatus
|
|
258
|
-
resource.lastUpdatedAt = new Date().toISOString()
|
|
259
|
-
this._writeManifest(manifest)
|
|
260
|
-
return true
|
|
257
|
+
resource.status = newStatus;
|
|
258
|
+
resource.lastUpdatedAt = new Date().toISOString();
|
|
259
|
+
this._writeManifest(manifest);
|
|
260
|
+
return true;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
/**
|
|
@@ -267,8 +267,8 @@ export default class AssetManager {
|
|
|
267
267
|
* @returns {Object|null} The matching Asset_Record, or null if not found
|
|
268
268
|
*/
|
|
269
269
|
getResource(resourceId) {
|
|
270
|
-
const manifest = this._readManifest()
|
|
271
|
-
return manifest.resources.find(r => r.resourceId === resourceId) || null
|
|
270
|
+
const manifest = this._readManifest();
|
|
271
|
+
return manifest.resources.find(r => r.resourceId === resourceId) || null;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
/**
|
|
@@ -278,18 +278,18 @@ export default class AssetManager {
|
|
|
278
278
|
* @returns {boolean} true if the resource was found and removed, false otherwise
|
|
279
279
|
*/
|
|
280
280
|
removeResource(resourceId) {
|
|
281
|
-
const manifest = this._readManifest()
|
|
282
|
-
const originalLength = manifest.resources.length
|
|
281
|
+
const manifest = this._readManifest();
|
|
282
|
+
const originalLength = manifest.resources.length;
|
|
283
283
|
manifest.resources = manifest.resources.filter(
|
|
284
284
|
r => r.resourceId !== resourceId
|
|
285
|
-
)
|
|
285
|
+
);
|
|
286
286
|
|
|
287
287
|
if (manifest.resources.length === originalLength) {
|
|
288
|
-
return false
|
|
288
|
+
return false;
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
this._writeManifest(manifest)
|
|
292
|
-
return true
|
|
291
|
+
this._writeManifest(manifest);
|
|
292
|
+
return true;
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
/**
|
|
@@ -302,24 +302,24 @@ export default class AssetManager {
|
|
|
302
302
|
* @returns {Array<Object>} Matching Asset_Records
|
|
303
303
|
*/
|
|
304
304
|
listResources(filters = {}) {
|
|
305
|
-
const manifest = this._readManifest()
|
|
305
|
+
const manifest = this._readManifest();
|
|
306
306
|
|
|
307
307
|
if (!filters || Object.keys(filters).length === 0) {
|
|
308
|
-
return manifest.resources
|
|
308
|
+
return manifest.resources;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
311
|
return manifest.resources.filter(resource => {
|
|
312
312
|
if (filters.resourceType && resource.resourceType !== filters.resourceType) {
|
|
313
|
-
return false
|
|
313
|
+
return false;
|
|
314
314
|
}
|
|
315
315
|
if (filters.project && resource.project !== filters.project) {
|
|
316
|
-
return false
|
|
316
|
+
return false;
|
|
317
317
|
}
|
|
318
318
|
if (filters.status && resource.status !== filters.status) {
|
|
319
|
-
return false
|
|
319
|
+
return false;
|
|
320
320
|
}
|
|
321
|
-
return true
|
|
322
|
-
})
|
|
321
|
+
return true;
|
|
322
|
+
});
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
/**
|
|
@@ -328,18 +328,18 @@ export default class AssetManager {
|
|
|
328
328
|
* @returns {Map<string, Array<Object>>} Map of project name → Asset_Record array
|
|
329
329
|
*/
|
|
330
330
|
getResourcesByProject() {
|
|
331
|
-
const manifest = this._readManifest()
|
|
332
|
-
const grouped = new Map()
|
|
331
|
+
const manifest = this._readManifest();
|
|
332
|
+
const grouped = new Map();
|
|
333
333
|
|
|
334
334
|
for (const resource of manifest.resources) {
|
|
335
|
-
const project = resource.project
|
|
335
|
+
const project = resource.project;
|
|
336
336
|
if (!grouped.has(project)) {
|
|
337
|
-
grouped.set(project, [])
|
|
337
|
+
grouped.set(project, []);
|
|
338
338
|
}
|
|
339
|
-
grouped.get(project).push(resource)
|
|
339
|
+
grouped.get(project).push(resource);
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
return grouped
|
|
342
|
+
return grouped;
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
/**
|
|
@@ -348,16 +348,16 @@ export default class AssetManager {
|
|
|
348
348
|
* @returns {{ active: number, deleted: number, unknown: number }}
|
|
349
349
|
*/
|
|
350
350
|
getStatusCounts() {
|
|
351
|
-
const manifest = this._readManifest()
|
|
352
|
-
const counts = { active: 0, deleted: 0, unknown: 0 }
|
|
351
|
+
const manifest = this._readManifest();
|
|
352
|
+
const counts = { active: 0, deleted: 0, unknown: 0 };
|
|
353
353
|
|
|
354
354
|
for (const resource of manifest.resources) {
|
|
355
355
|
if (resource.status in counts) {
|
|
356
|
-
counts[resource.status]
|
|
356
|
+
counts[resource.status]++;
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
return counts
|
|
360
|
+
return counts;
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
363
|
|
|
@@ -374,12 +374,12 @@ export default class AssetManager {
|
|
|
374
374
|
*/
|
|
375
375
|
function _isValidISO8601(str) {
|
|
376
376
|
if (typeof str !== 'string' || str.length === 0) {
|
|
377
|
-
return false
|
|
377
|
+
return false;
|
|
378
378
|
}
|
|
379
|
-
const date = new Date(str)
|
|
379
|
+
const date = new Date(str);
|
|
380
380
|
if (isNaN(date.getTime())) {
|
|
381
|
-
return false
|
|
381
|
+
return false;
|
|
382
382
|
}
|
|
383
383
|
// Ensure the string looks like an ISO 8601 timestamp (not just any parseable date string)
|
|
384
|
-
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(str)
|
|
384
|
+
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(str);
|
|
385
385
|
}
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
* - 'default' is always sorted first if present
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { readFileSync, existsSync } from 'node:fs'
|
|
19
|
-
import { join } from 'node:path'
|
|
20
|
-
import { homedir } from 'node:os'
|
|
18
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { homedir } from 'node:os';
|
|
21
21
|
|
|
22
22
|
export default class AwsProfileParser {
|
|
23
23
|
/**
|
|
@@ -26,8 +26,8 @@ export default class AwsProfileParser {
|
|
|
26
26
|
* @param {string} [options.credentialsPath] - Override path to ~/.aws/credentials
|
|
27
27
|
*/
|
|
28
28
|
constructor(options = {}) {
|
|
29
|
-
this._configPath = options.configPath || null
|
|
30
|
-
this._credentialsPath = options.credentialsPath || null
|
|
29
|
+
this._configPath = options.configPath || null;
|
|
30
|
+
this._credentialsPath = options.credentialsPath || null;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -37,18 +37,18 @@ export default class AwsProfileParser {
|
|
|
37
37
|
* @returns {string[]} Profile names, with 'default' first if it exists
|
|
38
38
|
*/
|
|
39
39
|
getProfiles() {
|
|
40
|
-
const configProfiles = this._getProfilesFromConfig()
|
|
41
|
-
const credentialsProfiles = this._getProfilesFromCredentials()
|
|
40
|
+
const configProfiles = this._getProfilesFromConfig();
|
|
41
|
+
const credentialsProfiles = this._getProfilesFromCredentials();
|
|
42
42
|
|
|
43
|
-
const allNames = new Set([...configProfiles, ...credentialsProfiles])
|
|
43
|
+
const allNames = new Set([...configProfiles, ...credentialsProfiles]);
|
|
44
44
|
|
|
45
45
|
const sorted = [...allNames].sort((a, b) => {
|
|
46
|
-
if (a === 'default') return -1
|
|
47
|
-
if (b === 'default') return 1
|
|
48
|
-
return a.localeCompare(b)
|
|
49
|
-
})
|
|
46
|
+
if (a === 'default') return -1;
|
|
47
|
+
if (b === 'default') return 1;
|
|
48
|
+
return a.localeCompare(b);
|
|
49
|
+
});
|
|
50
50
|
|
|
51
|
-
return sorted
|
|
51
|
+
return sorted;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
@@ -58,52 +58,52 @@ export default class AwsProfileParser {
|
|
|
58
58
|
* @returns {Map<string, Object>} Map of section names to parsed key-value pairs
|
|
59
59
|
*/
|
|
60
60
|
_parseIniFile(filePath) {
|
|
61
|
-
const sections = new Map()
|
|
61
|
+
const sections = new Map();
|
|
62
62
|
|
|
63
63
|
if (!existsSync(filePath)) {
|
|
64
|
-
return sections
|
|
64
|
+
return sections;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
let content
|
|
67
|
+
let content;
|
|
68
68
|
try {
|
|
69
|
-
content = readFileSync(filePath, 'utf8')
|
|
69
|
+
content = readFileSync(filePath, 'utf8');
|
|
70
70
|
} catch {
|
|
71
|
-
return sections
|
|
71
|
+
return sections;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
let currentSection = null
|
|
75
|
-
const lines = content.split(/\r?\n/)
|
|
74
|
+
let currentSection = null;
|
|
75
|
+
const lines = content.split(/\r?\n/);
|
|
76
76
|
|
|
77
77
|
for (const rawLine of lines) {
|
|
78
|
-
const line = rawLine.trim()
|
|
78
|
+
const line = rawLine.trim();
|
|
79
79
|
|
|
80
80
|
// Skip empty lines and comments
|
|
81
81
|
if (!line || line.startsWith('#') || line.startsWith(';')) {
|
|
82
|
-
continue
|
|
82
|
+
continue;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// Check for section header
|
|
86
|
-
const sectionMatch = line.match(/^\[([^\]]+)\]$/)
|
|
86
|
+
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
|
|
87
87
|
if (sectionMatch) {
|
|
88
|
-
currentSection = sectionMatch[1].trim()
|
|
88
|
+
currentSection = sectionMatch[1].trim();
|
|
89
89
|
if (!sections.has(currentSection)) {
|
|
90
|
-
sections.set(currentSection, {})
|
|
90
|
+
sections.set(currentSection, {});
|
|
91
91
|
}
|
|
92
|
-
continue
|
|
92
|
+
continue;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
// Parse key = value pairs
|
|
96
96
|
if (currentSection) {
|
|
97
|
-
const kvMatch = line.match(/^([^=]+?)=(.*)$/)
|
|
97
|
+
const kvMatch = line.match(/^([^=]+?)=(.*)$/);
|
|
98
98
|
if (kvMatch) {
|
|
99
|
-
const key = kvMatch[1].trim()
|
|
100
|
-
const value = kvMatch[2].trim()
|
|
101
|
-
sections.get(currentSection)[key] = value
|
|
99
|
+
const key = kvMatch[1].trim();
|
|
100
|
+
const value = kvMatch[2].trim();
|
|
101
|
+
sections.get(currentSection)[key] = value;
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
return sections
|
|
106
|
+
return sections;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/**
|
|
@@ -115,26 +115,26 @@ export default class AwsProfileParser {
|
|
|
115
115
|
* @returns {string[]} Array of profile names
|
|
116
116
|
*/
|
|
117
117
|
_extractProfileNames(parsed, isConfig = false) {
|
|
118
|
-
const names = []
|
|
118
|
+
const names = [];
|
|
119
119
|
|
|
120
120
|
for (const sectionName of parsed.keys()) {
|
|
121
121
|
if (isConfig) {
|
|
122
122
|
// ~/.aws/config uses [profile name] except for [default]
|
|
123
123
|
if (sectionName === 'default') {
|
|
124
|
-
names.push('default')
|
|
124
|
+
names.push('default');
|
|
125
125
|
} else if (sectionName.startsWith('profile ')) {
|
|
126
|
-
const name = sectionName.slice('profile '.length).trim()
|
|
126
|
+
const name = sectionName.slice('profile '.length).trim();
|
|
127
127
|
if (name) {
|
|
128
|
-
names.push(name)
|
|
128
|
+
names.push(name);
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
} else {
|
|
132
132
|
// ~/.aws/credentials uses [name] directly
|
|
133
|
-
names.push(sectionName)
|
|
133
|
+
names.push(sectionName);
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
return names
|
|
137
|
+
return names;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
/**
|
|
@@ -143,7 +143,7 @@ export default class AwsProfileParser {
|
|
|
143
143
|
* @returns {string} Absolute path to ~/.aws/config
|
|
144
144
|
*/
|
|
145
145
|
_getConfigPath() {
|
|
146
|
-
return this._configPath || join(homedir(), '.aws', 'config')
|
|
146
|
+
return this._configPath || join(homedir(), '.aws', 'config');
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/**
|
|
@@ -152,7 +152,7 @@ export default class AwsProfileParser {
|
|
|
152
152
|
* @returns {string} Absolute path to ~/.aws/credentials
|
|
153
153
|
*/
|
|
154
154
|
_getCredentialsPath() {
|
|
155
|
-
return this._credentialsPath || join(homedir(), '.aws', 'credentials')
|
|
155
|
+
return this._credentialsPath || join(homedir(), '.aws', 'credentials');
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
/**
|
|
@@ -162,9 +162,9 @@ export default class AwsProfileParser {
|
|
|
162
162
|
* @private
|
|
163
163
|
*/
|
|
164
164
|
_getProfilesFromConfig() {
|
|
165
|
-
const configPath = this._getConfigPath()
|
|
166
|
-
const parsed = this._parseIniFile(configPath)
|
|
167
|
-
return this._extractProfileNames(parsed, true)
|
|
165
|
+
const configPath = this._getConfigPath();
|
|
166
|
+
const parsed = this._parseIniFile(configPath);
|
|
167
|
+
return this._extractProfileNames(parsed, true);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
/**
|
|
@@ -174,8 +174,8 @@ export default class AwsProfileParser {
|
|
|
174
174
|
* @private
|
|
175
175
|
*/
|
|
176
176
|
_getProfilesFromCredentials() {
|
|
177
|
-
const credentialsPath = this._getCredentialsPath()
|
|
178
|
-
const parsed = this._parseIniFile(credentialsPath)
|
|
179
|
-
return this._extractProfileNames(parsed, false)
|
|
177
|
+
const credentialsPath = this._getCredentialsPath();
|
|
178
|
+
const parsed = this._parseIniFile(credentialsPath);
|
|
179
|
+
return this._extractProfileNames(parsed, false);
|
|
180
180
|
}
|
|
181
181
|
}
|