@quiltdata/benchling-webhook 0.5.4 → 0.6.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.
- package/README.md +273 -10
- package/dist/bin/benchling-webhook.d.ts +1 -1
- package/dist/bin/benchling-webhook.d.ts.map +1 -1
- package/dist/bin/benchling-webhook.js +8 -22
- package/dist/bin/benchling-webhook.js.map +1 -1
- package/dist/bin/cdk-dev.js +59 -3
- package/dist/bin/cli.js +16 -6
- package/dist/bin/cli.js.map +1 -1
- package/dist/bin/commands/deploy.d.ts +6 -2
- package/dist/bin/commands/deploy.d.ts.map +1 -1
- package/dist/bin/commands/deploy.js +149 -90
- package/dist/bin/commands/deploy.js.map +1 -1
- package/dist/bin/config-profiles.d.ts +59 -0
- package/dist/bin/config-profiles.d.ts.map +1 -0
- package/dist/bin/config-profiles.js +272 -0
- package/dist/bin/config-profiles.js.map +1 -0
- package/dist/bin/create-secret.d.ts +25 -0
- package/dist/bin/create-secret.d.ts.map +1 -0
- package/dist/bin/create-secret.js +239 -0
- package/dist/bin/create-secret.js.map +1 -0
- package/dist/lib/benchling-auth-validator.d.ts +65 -0
- package/dist/lib/benchling-auth-validator.d.ts.map +1 -0
- package/dist/lib/benchling-auth-validator.js +213 -0
- package/dist/lib/benchling-auth-validator.js.map +1 -0
- package/dist/lib/benchling-webhook-stack.d.ts +13 -10
- package/dist/lib/benchling-webhook-stack.d.ts.map +1 -1
- package/dist/lib/benchling-webhook-stack.js +25 -69
- package/dist/lib/benchling-webhook-stack.js.map +1 -1
- package/dist/lib/config-logger.d.ts +191 -0
- package/dist/lib/config-logger.d.ts.map +1 -0
- package/dist/lib/config-logger.js +372 -0
- package/dist/lib/config-logger.js.map +1 -0
- package/dist/lib/configuration-saver.d.ts +75 -0
- package/dist/lib/configuration-saver.d.ts.map +1 -0
- package/dist/lib/configuration-saver.js +145 -0
- package/dist/lib/configuration-saver.js.map +1 -0
- package/dist/lib/configuration-validator.d.ts +63 -0
- package/dist/lib/configuration-validator.d.ts.map +1 -0
- package/dist/lib/configuration-validator.js +136 -0
- package/dist/lib/configuration-validator.js.map +1 -0
- package/dist/lib/configuration-wizard.d.ts +52 -0
- package/dist/lib/configuration-wizard.d.ts.map +1 -0
- package/dist/lib/configuration-wizard.js +193 -0
- package/dist/lib/configuration-wizard.js.map +1 -0
- package/dist/lib/fargate-service.d.ts +18 -9
- package/dist/lib/fargate-service.d.ts.map +1 -1
- package/dist/lib/fargate-service.js +177 -61
- package/dist/lib/fargate-service.js.map +1 -1
- package/dist/lib/quilt-config-resolver.d.ts +53 -0
- package/dist/lib/quilt-config-resolver.d.ts.map +1 -0
- package/dist/lib/quilt-config-resolver.js +100 -0
- package/dist/lib/quilt-config-resolver.js.map +1 -0
- package/dist/lib/s3-bucket-validator.d.ts +76 -0
- package/dist/lib/s3-bucket-validator.d.ts.map +1 -0
- package/dist/lib/s3-bucket-validator.js +237 -0
- package/dist/lib/s3-bucket-validator.js.map +1 -0
- package/dist/lib/types/config.d.ts +398 -0
- package/dist/lib/types/config.d.ts.map +1 -0
- package/dist/lib/types/config.js +11 -0
- package/dist/lib/types/config.js.map +1 -0
- package/dist/lib/utils/config-loader.d.ts +48 -0
- package/dist/lib/utils/config-loader.d.ts.map +1 -0
- package/dist/lib/utils/config-loader.js +109 -0
- package/dist/lib/utils/config-loader.js.map +1 -0
- package/dist/lib/utils/config-resolver.d.ts +138 -0
- package/dist/lib/utils/config-resolver.d.ts.map +1 -0
- package/dist/lib/utils/config-resolver.js +272 -0
- package/dist/lib/utils/config-resolver.js.map +1 -0
- package/dist/lib/utils/config.d.ts +50 -0
- package/dist/lib/utils/config.d.ts.map +1 -1
- package/dist/lib/utils/config.js +86 -0
- package/dist/lib/utils/config.js.map +1 -1
- package/dist/lib/utils/secrets.d.ts +174 -0
- package/dist/lib/utils/secrets.d.ts.map +1 -0
- package/dist/lib/utils/secrets.js +351 -0
- package/dist/lib/utils/secrets.js.map +1 -0
- package/dist/lib/xdg-cli-wrapper.d.ts +113 -0
- package/dist/lib/xdg-cli-wrapper.d.ts.map +1 -0
- package/dist/lib/xdg-cli-wrapper.js +288 -0
- package/dist/lib/xdg-cli-wrapper.js.map +1 -0
- package/dist/lib/xdg-config.d.ts +187 -0
- package/dist/lib/xdg-config.d.ts.map +1 -0
- package/dist/lib/xdg-config.js +562 -0
- package/dist/lib/xdg-config.js.map +1 -0
- package/dist/package.json +33 -25
- package/dist/scripts/config-health-check.d.ts +78 -0
- package/dist/scripts/config-health-check.d.ts.map +1 -0
- package/dist/scripts/config-health-check.js +559 -0
- package/dist/scripts/config-health-check.js.map +1 -0
- package/dist/scripts/infer-quilt-config.d.ts +50 -0
- package/dist/scripts/infer-quilt-config.d.ts.map +1 -0
- package/dist/scripts/infer-quilt-config.js +353 -0
- package/dist/scripts/infer-quilt-config.js.map +1 -0
- package/dist/scripts/install-wizard.d.ts +34 -0
- package/dist/scripts/install-wizard.d.ts.map +1 -0
- package/dist/scripts/install-wizard.js +719 -0
- package/dist/scripts/install-wizard.js.map +1 -0
- package/dist/scripts/sync-secrets.d.ts +63 -0
- package/dist/scripts/sync-secrets.d.ts.map +1 -0
- package/dist/scripts/sync-secrets.js +424 -0
- package/dist/scripts/sync-secrets.js.map +1 -0
- package/env.template +60 -47
- package/package.json +33 -25
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Interactive Configuration Wizard
|
|
5
|
+
*
|
|
6
|
+
* Guided configuration setup with comprehensive validation:
|
|
7
|
+
* - Benchling tenant and OAuth credentials
|
|
8
|
+
* - S3 bucket access verification
|
|
9
|
+
* - Quilt API connectivity testing
|
|
10
|
+
* - AWS Secrets Manager integration
|
|
11
|
+
*
|
|
12
|
+
* Supports both interactive and non-interactive (CI/CD) modes.
|
|
13
|
+
*
|
|
14
|
+
* @module scripts/install-wizard
|
|
15
|
+
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
50
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
51
|
+
};
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.runInstallWizard = runInstallWizard;
|
|
54
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
55
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
56
|
+
const client_sts_1 = require("@aws-sdk/client-sts");
|
|
57
|
+
const xdg_config_1 = require("../lib/xdg-config");
|
|
58
|
+
const infer_quilt_config_1 = require("./infer-quilt-config");
|
|
59
|
+
const sync_secrets_1 = require("./sync-secrets");
|
|
60
|
+
const config_resolver_1 = require("../lib/utils/config-resolver");
|
|
61
|
+
const client_cloudformation_1 = require("@aws-sdk/client-cloudformation");
|
|
62
|
+
const https = __importStar(require("https"));
|
|
63
|
+
/**
|
|
64
|
+
* Validates Benchling tenant accessibility
|
|
65
|
+
*
|
|
66
|
+
* @param tenant - Benchling tenant name
|
|
67
|
+
* @returns Validation result
|
|
68
|
+
*/
|
|
69
|
+
async function validateBenchlingTenant(tenant) {
|
|
70
|
+
const result = {
|
|
71
|
+
isValid: false,
|
|
72
|
+
errors: [],
|
|
73
|
+
warnings: [],
|
|
74
|
+
};
|
|
75
|
+
if (!tenant || tenant.trim().length === 0) {
|
|
76
|
+
result.errors.push("Tenant name cannot be empty");
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
// Basic format validation
|
|
80
|
+
if (!/^[a-zA-Z0-9-_]+$/.test(tenant)) {
|
|
81
|
+
result.errors.push("Tenant name contains invalid characters");
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
// Test tenant URL accessibility
|
|
85
|
+
const tenantUrl = `https://${tenant}.benchling.com`;
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
https
|
|
88
|
+
.get(tenantUrl, { timeout: 5000 }, (res) => {
|
|
89
|
+
if (res.statusCode === 200 || res.statusCode === 302 || res.statusCode === 301) {
|
|
90
|
+
result.isValid = true;
|
|
91
|
+
console.log(` ✓ Tenant URL accessible: ${tenantUrl}`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
result.warnings.push(`Tenant URL returned status ${res.statusCode}`);
|
|
95
|
+
result.isValid = true; // Consider this a warning, not an error
|
|
96
|
+
}
|
|
97
|
+
resolve(result);
|
|
98
|
+
})
|
|
99
|
+
.on("error", (error) => {
|
|
100
|
+
result.warnings.push(`Could not verify tenant URL: ${error.message}`);
|
|
101
|
+
result.isValid = true; // Allow proceeding with warning
|
|
102
|
+
resolve(result);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Validates Benchling OAuth credentials
|
|
108
|
+
*
|
|
109
|
+
* @param tenant - Benchling tenant
|
|
110
|
+
* @param clientId - OAuth client ID
|
|
111
|
+
* @param clientSecret - OAuth client secret
|
|
112
|
+
* @returns Validation result
|
|
113
|
+
*/
|
|
114
|
+
async function validateBenchlingCredentials(tenant, clientId, clientSecret) {
|
|
115
|
+
const result = {
|
|
116
|
+
isValid: false,
|
|
117
|
+
errors: [],
|
|
118
|
+
warnings: [],
|
|
119
|
+
};
|
|
120
|
+
if (!clientId || clientId.trim().length === 0) {
|
|
121
|
+
result.errors.push("Client ID cannot be empty");
|
|
122
|
+
}
|
|
123
|
+
if (!clientSecret || clientSecret.trim().length === 0) {
|
|
124
|
+
result.errors.push("Client secret cannot be empty");
|
|
125
|
+
}
|
|
126
|
+
if (result.errors.length > 0) {
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
// Test OAuth token endpoint
|
|
130
|
+
const tokenUrl = `https://${tenant}.benchling.com/api/v2/token`;
|
|
131
|
+
const authString = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
|
|
132
|
+
return new Promise((resolve) => {
|
|
133
|
+
const postData = "grant_type=client_credentials";
|
|
134
|
+
const options = {
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers: {
|
|
137
|
+
"Authorization": `Basic ${authString}`,
|
|
138
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
139
|
+
"Content-Length": postData.length,
|
|
140
|
+
},
|
|
141
|
+
timeout: 10000,
|
|
142
|
+
};
|
|
143
|
+
const req = https.request(tokenUrl, options, (res) => {
|
|
144
|
+
let data = "";
|
|
145
|
+
res.on("data", (chunk) => {
|
|
146
|
+
data += chunk;
|
|
147
|
+
});
|
|
148
|
+
res.on("end", () => {
|
|
149
|
+
if (res.statusCode === 200) {
|
|
150
|
+
result.isValid = true;
|
|
151
|
+
console.log(" ✓ OAuth credentials validated successfully");
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
result.errors.push(`OAuth validation failed with status ${res.statusCode}: ${data.substring(0, 100)}`);
|
|
155
|
+
}
|
|
156
|
+
resolve(result);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
req.on("error", (error) => {
|
|
160
|
+
result.warnings.push(`Could not validate OAuth credentials: ${error.message}`);
|
|
161
|
+
result.isValid = true; // Allow proceeding with warning
|
|
162
|
+
resolve(result);
|
|
163
|
+
});
|
|
164
|
+
req.write(postData);
|
|
165
|
+
req.end();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Validates S3 bucket access
|
|
170
|
+
*
|
|
171
|
+
* @param bucketName - S3 bucket name
|
|
172
|
+
* @param region - AWS region
|
|
173
|
+
* @param awsProfile - AWS profile to use
|
|
174
|
+
* @returns Validation result
|
|
175
|
+
*/
|
|
176
|
+
async function validateS3BucketAccess(bucketName, region, awsProfile) {
|
|
177
|
+
const result = {
|
|
178
|
+
isValid: false,
|
|
179
|
+
errors: [],
|
|
180
|
+
warnings: [],
|
|
181
|
+
};
|
|
182
|
+
if (!bucketName || bucketName.trim().length === 0) {
|
|
183
|
+
result.errors.push("Bucket name cannot be empty");
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const clientConfig = { region };
|
|
188
|
+
if (awsProfile) {
|
|
189
|
+
const { fromIni } = await Promise.resolve().then(() => __importStar(require("@aws-sdk/credential-providers")));
|
|
190
|
+
clientConfig.credentials = fromIni({ profile: awsProfile });
|
|
191
|
+
}
|
|
192
|
+
const s3Client = new client_s3_1.S3Client(clientConfig);
|
|
193
|
+
// Test HeadBucket (verify bucket exists and we have access)
|
|
194
|
+
const headCommand = new client_s3_1.HeadBucketCommand({ Bucket: bucketName });
|
|
195
|
+
await s3Client.send(headCommand);
|
|
196
|
+
console.log(` ✓ S3 bucket accessible: ${bucketName}`);
|
|
197
|
+
// Test ListObjects (verify we can list objects)
|
|
198
|
+
const listCommand = new client_s3_1.ListObjectsV2Command({
|
|
199
|
+
Bucket: bucketName,
|
|
200
|
+
MaxKeys: 1,
|
|
201
|
+
});
|
|
202
|
+
await s3Client.send(listCommand);
|
|
203
|
+
console.log(" ✓ S3 bucket list permission confirmed");
|
|
204
|
+
result.isValid = true;
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
const err = error;
|
|
208
|
+
result.errors.push(`S3 bucket validation failed: ${err.message}`);
|
|
209
|
+
}
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Verifies CDK deployment account using AWS STS
|
|
214
|
+
*
|
|
215
|
+
* @param region - AWS region for STS client
|
|
216
|
+
* @param awsProfile - AWS profile to use (optional)
|
|
217
|
+
* @returns AWS account ID
|
|
218
|
+
*/
|
|
219
|
+
async function verifyCDKDeploymentAccount(region, awsProfile) {
|
|
220
|
+
try {
|
|
221
|
+
const clientConfig = { region };
|
|
222
|
+
if (awsProfile) {
|
|
223
|
+
const { fromIni } = await Promise.resolve().then(() => __importStar(require("@aws-sdk/credential-providers")));
|
|
224
|
+
clientConfig.credentials = fromIni({ profile: awsProfile });
|
|
225
|
+
}
|
|
226
|
+
const stsClient = new client_sts_1.STSClient(clientConfig);
|
|
227
|
+
const response = await stsClient.send(new client_sts_1.GetCallerIdentityCommand({}));
|
|
228
|
+
const accountId = response.Account;
|
|
229
|
+
console.log(` ✓ CDK deployment account verified: ${accountId}`);
|
|
230
|
+
return accountId;
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
throw new Error(`Failed to verify AWS account: ${error.message}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Finds catalog region by fetching config.json from QuiltWebHost
|
|
238
|
+
*
|
|
239
|
+
* @param catalogUrl - Quilt catalog URL (QuiltWebHost)
|
|
240
|
+
* @returns AWS region string or null if unable to determine
|
|
241
|
+
*/
|
|
242
|
+
async function findCatalogRegion(catalogUrl) {
|
|
243
|
+
if (!catalogUrl || catalogUrl.trim().length === 0) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
// Validate URL format
|
|
247
|
+
try {
|
|
248
|
+
new URL(catalogUrl);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
console.warn(` ⚠ Invalid catalog URL format: ${catalogUrl}`);
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
// Fetch config.json from QuiltWebHost
|
|
255
|
+
const configUrl = `${catalogUrl}/config.json`;
|
|
256
|
+
return new Promise((resolve) => {
|
|
257
|
+
https
|
|
258
|
+
.get(configUrl, { timeout: 10000 }, (res) => {
|
|
259
|
+
let data = "";
|
|
260
|
+
res.on("data", (chunk) => {
|
|
261
|
+
data += chunk;
|
|
262
|
+
});
|
|
263
|
+
res.on("end", () => {
|
|
264
|
+
if (res.statusCode === 200) {
|
|
265
|
+
try {
|
|
266
|
+
const config = JSON.parse(data);
|
|
267
|
+
// Quilt config.json has direct "region" field
|
|
268
|
+
const region = config.region;
|
|
269
|
+
if (region && typeof region === "string") {
|
|
270
|
+
console.log(` ✓ Found catalog region: ${region}`);
|
|
271
|
+
resolve(region);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
console.warn(" ⚠ No region field in catalog config.json");
|
|
275
|
+
resolve(null);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
console.warn(` ⚠ Failed to parse catalog config.json: ${error.message}`);
|
|
280
|
+
resolve(null);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
console.warn(` ⚠ Catalog config.json returned status ${res.statusCode}`);
|
|
285
|
+
resolve(null);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
})
|
|
289
|
+
.on("error", (error) => {
|
|
290
|
+
console.warn(` ⚠ Could not fetch catalog config: ${error.message}`);
|
|
291
|
+
resolve(null);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Runs the interactive configuration wizard
|
|
297
|
+
*
|
|
298
|
+
* @param options - Wizard options
|
|
299
|
+
* @returns Completed user configuration
|
|
300
|
+
*/
|
|
301
|
+
async function runInstallWizard(options = {}) {
|
|
302
|
+
const { profile = "default", nonInteractive = false, skipValidation = false, awsProfile, awsRegion = "us-east-1" } = options;
|
|
303
|
+
console.log("╔═══════════════════════════════════════════════════════════╗");
|
|
304
|
+
console.log("║ Benchling Webhook Configuration Wizard ║");
|
|
305
|
+
console.log("╚═══════════════════════════════════════════════════════════╝\n");
|
|
306
|
+
// Load existing configuration if available
|
|
307
|
+
const xdgConfig = new xdg_config_1.XDGConfig();
|
|
308
|
+
let existingConfig = {};
|
|
309
|
+
try {
|
|
310
|
+
existingConfig = xdgConfig.readProfileConfig("user", profile);
|
|
311
|
+
console.log("✓ Loaded existing configuration\n");
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
console.log("No existing configuration found, starting fresh\n");
|
|
315
|
+
}
|
|
316
|
+
const config = {
|
|
317
|
+
...existingConfig, // Merge existing config as defaults
|
|
318
|
+
_metadata: {
|
|
319
|
+
source: "install-wizard",
|
|
320
|
+
savedAt: new Date().toISOString(),
|
|
321
|
+
version: "0.6.0",
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
// Step 1: Infer Quilt configuration
|
|
325
|
+
console.log("Step 1: Inferring Quilt configuration...\n");
|
|
326
|
+
// Step 1a: Try to get catalog URL from quilt3 CLI first
|
|
327
|
+
let catalogRegion = awsRegion;
|
|
328
|
+
try {
|
|
329
|
+
const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
330
|
+
const catalogUrl = execSync("quilt3 config", { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }).trim();
|
|
331
|
+
if (catalogUrl && catalogUrl.startsWith("http")) {
|
|
332
|
+
console.log(`Found quilt3 CLI catalog: ${catalogUrl}`);
|
|
333
|
+
// Fetch region from catalog config.json
|
|
334
|
+
const detectedRegion = await findCatalogRegion(catalogUrl);
|
|
335
|
+
if (detectedRegion) {
|
|
336
|
+
catalogRegion = detectedRegion;
|
|
337
|
+
console.log(`Using catalog region: ${catalogRegion}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
// quilt3 not available or failed, continue with default region
|
|
343
|
+
console.log(`No quilt3 CLI found, using default region: ${catalogRegion}`);
|
|
344
|
+
}
|
|
345
|
+
const inferenceResult = await (0, infer_quilt_config_1.inferQuiltConfig)({
|
|
346
|
+
region: catalogRegion,
|
|
347
|
+
profile: awsProfile,
|
|
348
|
+
interactive: !nonInteractive,
|
|
349
|
+
});
|
|
350
|
+
const derivedConfig = (0, infer_quilt_config_1.inferenceResultToDerivedConfig)(inferenceResult);
|
|
351
|
+
// Merge inferred config
|
|
352
|
+
Object.assign(config, derivedConfig);
|
|
353
|
+
console.log("\n✓ Quilt configuration inferred");
|
|
354
|
+
// Step 2: Display and confirm Quilt stack configuration
|
|
355
|
+
if (!nonInteractive) {
|
|
356
|
+
console.log("\nStep 2: Verify Quilt Stack Configuration\n");
|
|
357
|
+
console.log("Detected Quilt stack:");
|
|
358
|
+
// Use parseStackArn and extractStackOutputs to get complete stack info
|
|
359
|
+
if (config.quiltStackArn) {
|
|
360
|
+
try {
|
|
361
|
+
const parsedArn = (0, config_resolver_1.parseStackArn)(config.quiltStackArn);
|
|
362
|
+
console.log(` Stack Name: ${parsedArn.stackName}`);
|
|
363
|
+
console.log(` Stack ARN: ${config.quiltStackArn}`);
|
|
364
|
+
console.log(` Region: ${parsedArn.region}`);
|
|
365
|
+
console.log(` Account: ${parsedArn.account}`);
|
|
366
|
+
// Fetch stack outputs using the config-resolver module
|
|
367
|
+
try {
|
|
368
|
+
const clientConfig = {
|
|
369
|
+
region: parsedArn.region,
|
|
370
|
+
};
|
|
371
|
+
if (config.awsProfile || awsProfile) {
|
|
372
|
+
const { fromIni } = await Promise.resolve().then(() => __importStar(require("@aws-sdk/credential-providers")));
|
|
373
|
+
clientConfig.credentials = fromIni({ profile: config.awsProfile || awsProfile });
|
|
374
|
+
}
|
|
375
|
+
const cfClient = new client_cloudformation_1.CloudFormationClient(clientConfig);
|
|
376
|
+
const outputs = await (0, config_resolver_1.extractStackOutputs)(cfClient, parsedArn.stackName);
|
|
377
|
+
console.log(` Catalog URL: ${outputs.QuiltWebHost || "Not found"}`);
|
|
378
|
+
console.log(` User Database: ${outputs.UserAthenaDatabaseName || outputs.AthenaDatabase || "Not found"}`);
|
|
379
|
+
console.log(` Queue ARN: ${outputs.PackagerQueueArn || outputs.QueueArn || "Not found"}`);
|
|
380
|
+
}
|
|
381
|
+
catch (outputError) {
|
|
382
|
+
console.warn(` ⚠ Could not fetch stack outputs: ${outputError.message}`);
|
|
383
|
+
console.log(` Catalog URL: ${config.quiltCatalog || "Not found"}`);
|
|
384
|
+
console.log(` Queue ARN: ${config.queueArn || "Not found"}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
// Fall back to simple display if parsing fails
|
|
389
|
+
console.log(` Stack ARN: ${config.quiltStackArn}`);
|
|
390
|
+
console.log(` Region: ${config.quiltRegion || awsRegion}`);
|
|
391
|
+
console.log(` Catalog URL: ${config.quiltCatalog || "Not found"}`);
|
|
392
|
+
console.log(` Queue ARN: ${config.queueArn || "Not found"}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
console.log(" Stack ARN: Not found");
|
|
397
|
+
console.log(` Region: ${config.quiltRegion || awsRegion}`);
|
|
398
|
+
}
|
|
399
|
+
const { confirmStack } = await inquirer_1.default.prompt([
|
|
400
|
+
{
|
|
401
|
+
type: "confirm",
|
|
402
|
+
name: "confirmStack",
|
|
403
|
+
message: "Is this the correct Quilt stack?",
|
|
404
|
+
default: true,
|
|
405
|
+
},
|
|
406
|
+
]);
|
|
407
|
+
if (!confirmStack) {
|
|
408
|
+
console.log("\nPlease run the wizard again and select the correct stack.");
|
|
409
|
+
process.exit(0);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// Step 3: Benchling configuration
|
|
413
|
+
console.log("\nStep 3: Benchling configuration\n");
|
|
414
|
+
if (!nonInteractive) {
|
|
415
|
+
const benchlingAnswers = await inquirer_1.default.prompt([
|
|
416
|
+
{
|
|
417
|
+
type: "input",
|
|
418
|
+
name: "benchlingTenant",
|
|
419
|
+
message: "Benchling Tenant:",
|
|
420
|
+
default: config.benchlingTenant,
|
|
421
|
+
validate: (input) => input.trim().length > 0 || "Tenant is required",
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
type: "input",
|
|
425
|
+
name: "benchlingClientId",
|
|
426
|
+
message: "Benchling OAuth Client ID:",
|
|
427
|
+
default: config.benchlingClientId,
|
|
428
|
+
validate: (input) => input.trim().length > 0 || "Client ID is required",
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
type: "password",
|
|
432
|
+
name: "benchlingClientSecret",
|
|
433
|
+
message: config.benchlingClientSecret
|
|
434
|
+
? "Benchling OAuth Client Secret (press Enter to keep existing):"
|
|
435
|
+
: "Benchling OAuth Client Secret:",
|
|
436
|
+
validate: (input) => {
|
|
437
|
+
// If there's an existing secret and input is empty, we'll keep the existing one
|
|
438
|
+
if (config.benchlingClientSecret && input.trim().length === 0) {
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
return input.trim().length > 0 || "Client secret is required";
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
type: "input",
|
|
446
|
+
name: "benchlingAppDefinitionId",
|
|
447
|
+
message: "Benchling App Definition ID:",
|
|
448
|
+
default: config.benchlingAppDefinitionId,
|
|
449
|
+
validate: (input) => input.trim().length > 0 || "App definition ID is required",
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
type: "input",
|
|
453
|
+
name: "benchlingPkgBucket",
|
|
454
|
+
message: "Benchling Package S3 Bucket:",
|
|
455
|
+
default: config.benchlingPkgBucket || config.quiltUserBucket,
|
|
456
|
+
validate: (input) => input.trim().length > 0 || "Bucket name is required",
|
|
457
|
+
},
|
|
458
|
+
]);
|
|
459
|
+
// Handle empty password input - keep existing secret if user pressed Enter
|
|
460
|
+
if (benchlingAnswers.benchlingClientSecret.trim().length === 0 && config.benchlingClientSecret) {
|
|
461
|
+
benchlingAnswers.benchlingClientSecret = config.benchlingClientSecret;
|
|
462
|
+
}
|
|
463
|
+
Object.assign(config, benchlingAnswers);
|
|
464
|
+
// Validate Benchling configuration
|
|
465
|
+
if (!skipValidation && config.benchlingTenant) {
|
|
466
|
+
const tenantValidation = await validateBenchlingTenant(config.benchlingTenant);
|
|
467
|
+
if (!tenantValidation.isValid) {
|
|
468
|
+
console.error("\n❌ Benchling tenant validation failed:");
|
|
469
|
+
tenantValidation.errors.forEach((err) => console.error(` - ${err}`));
|
|
470
|
+
const { proceed } = await inquirer_1.default.prompt([
|
|
471
|
+
{
|
|
472
|
+
type: "confirm",
|
|
473
|
+
name: "proceed",
|
|
474
|
+
message: "Continue anyway?",
|
|
475
|
+
default: false,
|
|
476
|
+
},
|
|
477
|
+
]);
|
|
478
|
+
if (!proceed) {
|
|
479
|
+
throw new Error("Configuration aborted by user");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (tenantValidation.warnings.length > 0) {
|
|
483
|
+
console.warn("\n⚠ Warnings:");
|
|
484
|
+
tenantValidation.warnings.forEach((warn) => console.warn(` - ${warn}`));
|
|
485
|
+
}
|
|
486
|
+
// Validate OAuth credentials
|
|
487
|
+
if (config.benchlingClientId && config.benchlingClientSecret) {
|
|
488
|
+
const credValidation = await validateBenchlingCredentials(config.benchlingTenant, config.benchlingClientId, config.benchlingClientSecret);
|
|
489
|
+
if (!credValidation.isValid) {
|
|
490
|
+
console.error("\n❌ Benchling OAuth credential validation failed:");
|
|
491
|
+
credValidation.errors.forEach((err) => console.error(` - ${err}`));
|
|
492
|
+
const { proceed } = await inquirer_1.default.prompt([
|
|
493
|
+
{
|
|
494
|
+
type: "confirm",
|
|
495
|
+
name: "proceed",
|
|
496
|
+
message: "Continue anyway?",
|
|
497
|
+
default: false,
|
|
498
|
+
},
|
|
499
|
+
]);
|
|
500
|
+
if (!proceed) {
|
|
501
|
+
throw new Error("Configuration aborted by user");
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (credValidation.warnings.length > 0) {
|
|
505
|
+
console.warn("\n⚠ Warnings:");
|
|
506
|
+
credValidation.warnings.forEach((warn) => console.warn(` - ${warn}`));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Validate Benchling package bucket
|
|
510
|
+
if (config.benchlingPkgBucket) {
|
|
511
|
+
const bucketValidation = await validateS3BucketAccess(config.benchlingPkgBucket, config.quiltRegion || awsRegion, awsProfile);
|
|
512
|
+
if (!bucketValidation.isValid) {
|
|
513
|
+
console.error("\n❌ Benchling package bucket validation failed:");
|
|
514
|
+
bucketValidation.errors.forEach((err) => console.error(` - ${err}`));
|
|
515
|
+
const { proceed } = await inquirer_1.default.prompt([
|
|
516
|
+
{
|
|
517
|
+
type: "confirm",
|
|
518
|
+
name: "proceed",
|
|
519
|
+
message: "Continue anyway?",
|
|
520
|
+
default: false,
|
|
521
|
+
},
|
|
522
|
+
]);
|
|
523
|
+
if (!proceed) {
|
|
524
|
+
throw new Error("Configuration aborted by user");
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
// Non-interactive mode: use existing config values
|
|
532
|
+
// Required fields must already be set in XDG config
|
|
533
|
+
if (!config.benchlingTenant || !config.benchlingClientId || !config.benchlingClientSecret) {
|
|
534
|
+
throw new Error("Non-interactive mode requires benchlingTenant, benchlingClientId, and benchlingClientSecret to be already configured in XDG config. Run 'npm run setup' interactively first.");
|
|
535
|
+
}
|
|
536
|
+
// Set default bucket if not specified
|
|
537
|
+
if (!config.benchlingPkgBucket) {
|
|
538
|
+
config.benchlingPkgBucket = config.quiltUserBucket;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// Step 4: AWS configuration
|
|
542
|
+
console.log("\nStep 4: AWS configuration\n");
|
|
543
|
+
if (!nonInteractive) {
|
|
544
|
+
const awsAnswers = await inquirer_1.default.prompt([
|
|
545
|
+
{
|
|
546
|
+
type: "input",
|
|
547
|
+
name: "awsProfile",
|
|
548
|
+
message: "AWS Profile (optional, leave empty for default):",
|
|
549
|
+
default: awsProfile || "",
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
type: "input",
|
|
553
|
+
name: "cdkRegion",
|
|
554
|
+
message: "CDK Deployment Region:",
|
|
555
|
+
default: config.quiltRegion || awsRegion,
|
|
556
|
+
},
|
|
557
|
+
]);
|
|
558
|
+
if (awsAnswers.awsProfile) {
|
|
559
|
+
config.awsProfile = awsAnswers.awsProfile;
|
|
560
|
+
}
|
|
561
|
+
config.cdkRegion = awsAnswers.cdkRegion;
|
|
562
|
+
// Verify CDK deployment account
|
|
563
|
+
console.log("\nVerifying CDK deployment account...");
|
|
564
|
+
try {
|
|
565
|
+
const accountId = await verifyCDKDeploymentAccount(awsAnswers.cdkRegion, config.awsProfile);
|
|
566
|
+
config.cdkAccount = accountId;
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
console.error(`\n❌ Failed to verify AWS account: ${error.message}`);
|
|
570
|
+
const { proceed } = await inquirer_1.default.prompt([
|
|
571
|
+
{
|
|
572
|
+
type: "confirm",
|
|
573
|
+
name: "proceed",
|
|
574
|
+
message: "Continue anyway?",
|
|
575
|
+
default: false,
|
|
576
|
+
},
|
|
577
|
+
]);
|
|
578
|
+
if (!proceed) {
|
|
579
|
+
throw new Error("Configuration aborted by user");
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
// Non-interactive mode: use existing config or default region
|
|
585
|
+
if (!config.cdkRegion) {
|
|
586
|
+
config.cdkRegion = config.quiltRegion || awsRegion;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// Step 5: Optional configuration
|
|
590
|
+
console.log("\nStep 5: Optional configuration\n");
|
|
591
|
+
if (!nonInteractive) {
|
|
592
|
+
const optionalAnswers = await inquirer_1.default.prompt([
|
|
593
|
+
{
|
|
594
|
+
type: "input",
|
|
595
|
+
name: "pkgPrefix",
|
|
596
|
+
message: "Package S3 prefix (default: benchling):",
|
|
597
|
+
default: "benchling",
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
type: "input",
|
|
601
|
+
name: "pkgKey",
|
|
602
|
+
message: "Package metadata key (default: experiment_id):",
|
|
603
|
+
default: "experiment_id",
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
type: "list",
|
|
607
|
+
name: "logLevel",
|
|
608
|
+
message: "Log level:",
|
|
609
|
+
choices: ["DEBUG", "INFO", "WARNING", "ERROR"],
|
|
610
|
+
default: "INFO",
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
type: "input",
|
|
614
|
+
name: "benchlingTestEntry",
|
|
615
|
+
message: "Benchling Test Entry ID (optional, for validation):",
|
|
616
|
+
default: config.benchlingTestEntry || "",
|
|
617
|
+
},
|
|
618
|
+
]);
|
|
619
|
+
Object.assign(config, optionalAnswers);
|
|
620
|
+
// Remove empty benchlingTestEntry if not provided
|
|
621
|
+
if (!config.benchlingTestEntry || config.benchlingTestEntry.trim() === "") {
|
|
622
|
+
delete config.benchlingTestEntry;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
// Non-interactive mode: use existing config or defaults
|
|
627
|
+
if (!config.pkgPrefix) {
|
|
628
|
+
config.pkgPrefix = "benchling";
|
|
629
|
+
}
|
|
630
|
+
if (!config.pkgKey) {
|
|
631
|
+
config.pkgKey = "experiment_id";
|
|
632
|
+
}
|
|
633
|
+
if (!config.logLevel) {
|
|
634
|
+
config.logLevel = "INFO";
|
|
635
|
+
}
|
|
636
|
+
// benchlingTestEntry is optional, keep existing value if present
|
|
637
|
+
}
|
|
638
|
+
// Step 6: Save configuration
|
|
639
|
+
console.log("\nStep 6: Saving configuration...\n");
|
|
640
|
+
xdgConfig.ensureProfileDirectories(profile);
|
|
641
|
+
xdgConfig.writeProfileConfig("user", config, profile);
|
|
642
|
+
console.log(`✓ Configuration saved to profile: ${profile}`);
|
|
643
|
+
// Step 7: Sync secrets to AWS Secrets Manager
|
|
644
|
+
if (!nonInteractive) {
|
|
645
|
+
const { syncSecrets } = await inquirer_1.default.prompt([
|
|
646
|
+
{
|
|
647
|
+
type: "confirm",
|
|
648
|
+
name: "syncSecrets",
|
|
649
|
+
message: "Sync secrets to AWS Secrets Manager?",
|
|
650
|
+
default: true,
|
|
651
|
+
},
|
|
652
|
+
]);
|
|
653
|
+
if (syncSecrets) {
|
|
654
|
+
console.log("\nSyncing secrets to AWS Secrets Manager...\n");
|
|
655
|
+
await (0, sync_secrets_1.syncSecretsToAWS)({
|
|
656
|
+
profile,
|
|
657
|
+
awsProfile: config.awsProfile,
|
|
658
|
+
region: config.cdkRegion || awsRegion,
|
|
659
|
+
});
|
|
660
|
+
console.log("\n✓ Secrets synced successfully");
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
console.log("\n╔═══════════════════════════════════════════════════════════╗");
|
|
664
|
+
console.log("║ Configuration Complete! ║");
|
|
665
|
+
console.log("╚═══════════════════════════════════════════════════════════╝\n");
|
|
666
|
+
return config;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Main execution for CLI usage
|
|
670
|
+
*/
|
|
671
|
+
async function main() {
|
|
672
|
+
const args = process.argv.slice(2);
|
|
673
|
+
const options = {};
|
|
674
|
+
// Parse command line arguments
|
|
675
|
+
for (let i = 0; i < args.length; i++) {
|
|
676
|
+
if (args[i] === "--profile" && i + 1 < args.length) {
|
|
677
|
+
options.profile = args[i + 1];
|
|
678
|
+
i++;
|
|
679
|
+
}
|
|
680
|
+
else if (args[i] === "--non-interactive") {
|
|
681
|
+
options.nonInteractive = true;
|
|
682
|
+
}
|
|
683
|
+
else if (args[i] === "--skip-validation") {
|
|
684
|
+
options.skipValidation = true;
|
|
685
|
+
}
|
|
686
|
+
else if (args[i] === "--aws-profile" && i + 1 < args.length) {
|
|
687
|
+
options.awsProfile = args[i + 1];
|
|
688
|
+
i++;
|
|
689
|
+
}
|
|
690
|
+
else if (args[i] === "--aws-region" && i + 1 < args.length) {
|
|
691
|
+
options.awsRegion = args[i + 1];
|
|
692
|
+
i++;
|
|
693
|
+
}
|
|
694
|
+
else if (args[i] === "--help") {
|
|
695
|
+
console.log("Usage: install-wizard [options]");
|
|
696
|
+
console.log("\nOptions:");
|
|
697
|
+
console.log(" --profile <name> Configuration profile name (default: default)");
|
|
698
|
+
console.log(" --non-interactive Run in non-interactive mode (CI/CD)");
|
|
699
|
+
console.log(" --skip-validation Skip validation checks");
|
|
700
|
+
console.log(" --aws-profile <profile> AWS profile to use");
|
|
701
|
+
console.log(" --aws-region <region> AWS region (default: us-east-1)");
|
|
702
|
+
console.log(" --help Show this help message");
|
|
703
|
+
process.exit(0);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
await runInstallWizard(options);
|
|
708
|
+
process.exit(0);
|
|
709
|
+
}
|
|
710
|
+
catch (error) {
|
|
711
|
+
console.error("\n❌ Configuration failed:", error.message);
|
|
712
|
+
process.exit(1);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// Run main if executed directly
|
|
716
|
+
if (require.main === module) {
|
|
717
|
+
main();
|
|
718
|
+
}
|
|
719
|
+
//# sourceMappingURL=install-wizard.js.map
|