@quiltdata/benchling-webhook 0.6.3-20251104T170954Z → 0.7.2-20251106T003353Z
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 +166 -5
- package/dist/bin/benchling-webhook.d.ts +2 -16
- package/dist/bin/benchling-webhook.d.ts.map +1 -1
- package/dist/bin/benchling-webhook.js +98 -158
- package/dist/bin/benchling-webhook.js.map +1 -1
- package/dist/bin/cli.js +96 -8
- package/dist/bin/cli.js.map +1 -1
- package/dist/bin/{config-profiles.d.ts → commands/config-profiles.d.ts} +10 -9
- package/dist/bin/commands/config-profiles.d.ts.map +1 -0
- package/dist/bin/{config-profiles.js → commands/config-profiles.js} +110 -102
- package/dist/bin/commands/config-profiles.js.map +1 -0
- package/dist/bin/commands/create-secret.d.ts.map +1 -0
- package/dist/bin/commands/create-secret.js.map +1 -0
- package/dist/bin/commands/deploy.d.ts +11 -0
- package/dist/bin/commands/deploy.d.ts.map +1 -1
- package/dist/bin/commands/deploy.js +65 -110
- package/dist/bin/commands/deploy.js.map +1 -1
- package/dist/bin/{get-env.d.ts → commands/get-env.d.ts} +1 -1
- package/dist/bin/commands/get-env.d.ts.map +1 -0
- package/dist/bin/{get-env.js → commands/get-env.js} +2 -2
- package/dist/bin/commands/get-env.js.map +1 -0
- package/dist/bin/commands/health-check.d.ts +47 -0
- package/dist/bin/commands/health-check.d.ts.map +1 -0
- package/dist/bin/commands/health-check.js +357 -0
- package/dist/bin/commands/health-check.js.map +1 -0
- package/dist/bin/commands/infer-quilt-config.d.ts +50 -0
- package/dist/bin/commands/infer-quilt-config.d.ts.map +1 -0
- package/dist/bin/commands/infer-quilt-config.js +356 -0
- package/dist/bin/commands/infer-quilt-config.js.map +1 -0
- package/dist/bin/commands/init.d.ts.map +1 -1
- package/dist/bin/commands/init.js +2 -32
- package/dist/bin/commands/init.js.map +1 -1
- package/dist/bin/commands/manifest.d.ts +11 -0
- package/dist/bin/commands/manifest.d.ts.map +1 -1
- package/dist/bin/commands/manifest.js +22 -8
- package/dist/bin/commands/manifest.js.map +1 -1
- package/dist/bin/commands/publish.d.ts.map +1 -0
- package/dist/bin/{publish.js → commands/publish.js} +2 -2
- package/dist/bin/commands/publish.js.map +1 -0
- package/dist/bin/commands/setup-profile.d.ts +29 -0
- package/dist/bin/commands/setup-profile.d.ts.map +1 -0
- package/dist/bin/commands/setup-profile.js +220 -0
- package/dist/bin/commands/setup-profile.js.map +1 -0
- package/dist/bin/commands/setup-wizard.d.ts +26 -11
- package/dist/bin/commands/setup-wizard.d.ts.map +1 -1
- package/dist/bin/commands/setup-wizard.js +844 -46
- package/dist/bin/commands/setup-wizard.js.map +1 -1
- package/dist/{scripts → bin/commands}/sync-secrets.d.ts +6 -1
- package/dist/bin/commands/sync-secrets.d.ts.map +1 -0
- package/dist/{scripts → bin/commands}/sync-secrets.js +159 -55
- package/dist/bin/commands/sync-secrets.js.map +1 -0
- package/dist/bin/commands/validate.d.ts.map +1 -1
- package/dist/bin/commands/validate.js +2 -12
- package/dist/bin/commands/validate.js.map +1 -1
- package/dist/lib/alb-api-gateway.d.ts +7 -1
- package/dist/lib/alb-api-gateway.d.ts.map +1 -1
- package/dist/lib/alb-api-gateway.js +9 -6
- package/dist/lib/alb-api-gateway.js.map +1 -1
- package/dist/lib/benchling-webhook-stack.d.ts +13 -12
- package/dist/lib/benchling-webhook-stack.d.ts.map +1 -1
- package/dist/lib/benchling-webhook-stack.js +43 -30
- package/dist/lib/benchling-webhook-stack.js.map +1 -1
- package/dist/lib/configuration-saver.d.ts +4 -16
- package/dist/lib/configuration-saver.d.ts.map +1 -1
- package/dist/lib/configuration-saver.js +14 -54
- package/dist/lib/configuration-saver.js.map +1 -1
- package/dist/lib/fargate-service.d.ts +11 -21
- package/dist/lib/fargate-service.d.ts.map +1 -1
- package/dist/lib/fargate-service.js +79 -176
- package/dist/lib/fargate-service.js.map +1 -1
- package/dist/lib/types/config.d.ts +591 -224
- package/dist/lib/types/config.d.ts.map +1 -1
- package/dist/lib/types/config.js +134 -3
- package/dist/lib/types/config.js.map +1 -1
- package/dist/lib/utils/config.d.ts.map +1 -1
- package/dist/lib/utils/config.js.map +1 -1
- package/dist/lib/xdg-config.d.ts +222 -106
- package/dist/lib/xdg-config.d.ts.map +1 -1
- package/dist/lib/xdg-config.js +448 -387
- package/dist/lib/xdg-config.js.map +1 -1
- package/dist/package.json +16 -13
- package/dist/scripts/check-logs.d.ts +12 -0
- package/dist/scripts/check-logs.d.ts.map +1 -0
- package/dist/{bin → scripts}/check-logs.js +65 -15
- package/dist/scripts/check-logs.js.map +1 -0
- package/dist/scripts/check-webhook-verification.d.ts +3 -0
- package/dist/scripts/check-webhook-verification.d.ts.map +1 -0
- package/dist/{bin/test-invalid-signature.js → scripts/check-webhook-verification.js} +1 -1
- package/dist/scripts/check-webhook-verification.js.map +1 -0
- package/dist/scripts/infer-quilt-config.d.ts +23 -26
- package/dist/scripts/infer-quilt-config.d.ts.map +1 -1
- package/dist/scripts/infer-quilt-config.js +58 -96
- package/dist/scripts/infer-quilt-config.js.map +1 -1
- package/dist/scripts/send-event.d.ts.map +1 -0
- package/dist/scripts/send-event.js.map +1 -0
- package/dist/{bin → scripts}/version.d.ts +3 -1
- package/dist/scripts/version.d.ts.map +1 -0
- package/dist/{bin → scripts}/version.js +95 -9
- package/dist/scripts/version.js.map +1 -0
- package/package.json +16 -13
- package/dist/bin/check-logs.d.ts +0 -7
- package/dist/bin/check-logs.d.ts.map +0 -1
- package/dist/bin/check-logs.js.map +0 -1
- package/dist/bin/config-profiles.d.ts.map +0 -1
- package/dist/bin/config-profiles.js.map +0 -1
- package/dist/bin/create-secret.d.ts.map +0 -1
- package/dist/bin/create-secret.js.map +0 -1
- package/dist/bin/dev-deploy.d.ts +0 -20
- package/dist/bin/dev-deploy.d.ts.map +0 -1
- package/dist/bin/dev-deploy.js +0 -289
- package/dist/bin/dev-deploy.js.map +0 -1
- package/dist/bin/get-env.d.ts.map +0 -1
- package/dist/bin/get-env.js.map +0 -1
- package/dist/bin/publish.d.ts.map +0 -1
- package/dist/bin/publish.js.map +0 -1
- package/dist/bin/release.d.ts +0 -11
- package/dist/bin/release.d.ts.map +0 -1
- package/dist/bin/release.js +0 -141
- package/dist/bin/release.js.map +0 -1
- package/dist/bin/send-event.d.ts.map +0 -1
- package/dist/bin/send-event.js.map +0 -1
- package/dist/bin/test-invalid-signature.d.ts +0 -3
- package/dist/bin/test-invalid-signature.d.ts.map +0 -1
- package/dist/bin/test-invalid-signature.js.map +0 -1
- package/dist/bin/version.d.ts.map +0 -1
- package/dist/bin/version.js.map +0 -1
- package/dist/scripts/config-health-check.d.ts +0 -84
- package/dist/scripts/config-health-check.d.ts.map +0 -1
- package/dist/scripts/config-health-check.js +0 -659
- package/dist/scripts/config-health-check.js.map +0 -1
- package/dist/scripts/install-wizard.d.ts +0 -34
- package/dist/scripts/install-wizard.d.ts.map +0 -1
- package/dist/scripts/install-wizard.js +0 -719
- package/dist/scripts/install-wizard.js.map +0 -1
- package/dist/scripts/sync-secrets.d.ts.map +0 -1
- package/dist/scripts/sync-secrets.js.map +0 -1
- /package/dist/bin/{create-secret.d.ts → commands/create-secret.d.ts} +0 -0
- /package/dist/bin/{create-secret.js → commands/create-secret.js} +0 -0
- /package/dist/bin/{publish.d.ts → commands/publish.d.ts} +0 -0
- /package/dist/{bin → scripts}/send-event.d.ts +0 -0
- /package/dist/{bin → scripts}/send-event.js +0 -0
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
"use strict";
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
+
* Interactive Configuration Wizard (v0.7.0)
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Complete setup wizard that orchestrates:
|
|
7
|
+
* 1. Quilt configuration inference
|
|
8
|
+
* 2. Interactive configuration prompts
|
|
9
|
+
* 3. Configuration validation
|
|
10
|
+
* 4. Profile persistence via XDGConfig
|
|
11
|
+
*
|
|
12
|
+
* Consolidated from scripts/install-wizard.ts, scripts/config/wizard.ts,
|
|
13
|
+
* and scripts/config/validator.ts.
|
|
8
14
|
*
|
|
9
15
|
* @module commands/setup-wizard
|
|
10
16
|
*/
|
|
@@ -46,58 +52,850 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
46
52
|
};
|
|
47
53
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
54
|
exports.setupWizardCommand = setupWizardCommand;
|
|
55
|
+
const https = __importStar(require("https"));
|
|
56
|
+
const fs_1 = require("fs");
|
|
57
|
+
const path_1 = require("path");
|
|
58
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
49
59
|
const chalk_1 = __importDefault(require("chalk"));
|
|
50
|
-
const
|
|
60
|
+
const boxen_1 = __importDefault(require("boxen"));
|
|
61
|
+
const ora_1 = __importDefault(require("ora"));
|
|
62
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
63
|
+
const xdg_config_1 = require("../../lib/xdg-config");
|
|
64
|
+
const infer_quilt_config_1 = require("../commands/infer-quilt-config");
|
|
65
|
+
const manifest_1 = require("./manifest");
|
|
66
|
+
const sync_secrets_1 = require("./sync-secrets");
|
|
67
|
+
const deploy_1 = require("./deploy");
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
69
|
+
const pkg = require("../../package.json");
|
|
70
|
+
const MANIFEST_FILENAME = "benchling-app-manifest.yaml";
|
|
71
|
+
function getAccountFromStackArn(stackArn) {
|
|
72
|
+
if (!stackArn) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const parts = stackArn.split(":");
|
|
76
|
+
if (parts.length < 5) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const account = parts[4];
|
|
80
|
+
return /^[0-9]{12}$/.test(account) ? account : undefined;
|
|
81
|
+
}
|
|
82
|
+
// =============================================================================
|
|
83
|
+
// VALIDATION FUNCTIONS (from scripts/config/validator.ts)
|
|
84
|
+
// =============================================================================
|
|
51
85
|
/**
|
|
52
|
-
*
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
86
|
+
* Validates Benchling tenant accessibility
|
|
87
|
+
*/
|
|
88
|
+
async function validateBenchlingTenant(tenant) {
|
|
89
|
+
const result = {
|
|
90
|
+
isValid: false,
|
|
91
|
+
errors: [],
|
|
92
|
+
warnings: [],
|
|
93
|
+
};
|
|
94
|
+
if (!tenant || tenant.trim().length === 0) {
|
|
95
|
+
result.errors.push("Tenant name cannot be empty");
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
// Basic format validation
|
|
99
|
+
if (!/^[a-zA-Z0-9-_]+$/.test(tenant)) {
|
|
100
|
+
result.errors.push("Tenant name contains invalid characters (only alphanumeric, dash, underscore allowed)");
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
// Test tenant URL accessibility
|
|
104
|
+
const tenantUrl = `https://${tenant}.benchling.com`;
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
https
|
|
107
|
+
.get(tenantUrl, { timeout: 5000 }, (res) => {
|
|
108
|
+
if (res.statusCode === 200 || res.statusCode === 302 || res.statusCode === 301) {
|
|
109
|
+
result.isValid = true;
|
|
110
|
+
console.log(` ✓ Tenant URL accessible: ${tenantUrl}`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
if (!result.warnings)
|
|
114
|
+
result.warnings = [];
|
|
115
|
+
result.warnings.push(`Tenant URL returned status ${res.statusCode}`);
|
|
116
|
+
result.isValid = true; // Consider this a warning, not an error
|
|
117
|
+
}
|
|
118
|
+
resolve(result);
|
|
119
|
+
})
|
|
120
|
+
.on("error", (error) => {
|
|
121
|
+
if (!result.warnings)
|
|
122
|
+
result.warnings = [];
|
|
123
|
+
result.warnings.push(`Could not verify tenant URL: ${error.message}`);
|
|
124
|
+
result.isValid = true; // Allow proceeding with warning
|
|
125
|
+
resolve(result);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Validates Benchling OAuth credentials
|
|
61
131
|
*/
|
|
62
|
-
async function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
132
|
+
async function validateBenchlingCredentials(tenant, clientId, clientSecret) {
|
|
133
|
+
const result = {
|
|
134
|
+
isValid: false,
|
|
135
|
+
errors: [],
|
|
136
|
+
warnings: [],
|
|
137
|
+
};
|
|
138
|
+
if (!clientId || clientId.trim().length === 0) {
|
|
139
|
+
result.errors.push("Client ID cannot be empty");
|
|
140
|
+
}
|
|
141
|
+
if (!clientSecret || clientSecret.trim().length === 0) {
|
|
142
|
+
result.errors.push("Client secret cannot be empty");
|
|
143
|
+
}
|
|
144
|
+
if (result.errors.length > 0) {
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
// Test OAuth token endpoint
|
|
148
|
+
const tokenUrl = `https://${tenant}.benchling.com/api/v2/token`;
|
|
149
|
+
const authString = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
const postData = "grant_type=client_credentials";
|
|
152
|
+
const options = {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: {
|
|
155
|
+
"Authorization": `Basic ${authString}`,
|
|
156
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
157
|
+
"Content-Length": postData.length,
|
|
158
|
+
},
|
|
159
|
+
timeout: 10000,
|
|
160
|
+
};
|
|
161
|
+
const req = https.request(tokenUrl, options, (res) => {
|
|
162
|
+
let data = "";
|
|
163
|
+
res.on("data", (chunk) => {
|
|
164
|
+
data += chunk;
|
|
165
|
+
});
|
|
166
|
+
res.on("end", () => {
|
|
167
|
+
if (res.statusCode === 200) {
|
|
168
|
+
result.isValid = true;
|
|
169
|
+
console.log(" ✓ OAuth credentials validated successfully");
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
result.errors.push(`OAuth validation failed with status ${res.statusCode}: ${data.substring(0, 100)}`);
|
|
173
|
+
}
|
|
174
|
+
resolve(result);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
req.on("error", (error) => {
|
|
178
|
+
if (!result.warnings)
|
|
179
|
+
result.warnings = [];
|
|
180
|
+
result.warnings.push(`Could not validate OAuth credentials: ${error.message}`);
|
|
181
|
+
result.isValid = true; // Allow proceeding with warning
|
|
182
|
+
resolve(result);
|
|
183
|
+
});
|
|
184
|
+
req.write(postData);
|
|
185
|
+
req.end();
|
|
74
186
|
});
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Validates S3 bucket access
|
|
190
|
+
*/
|
|
191
|
+
async function validateS3BucketAccess(bucketName, region, awsProfile) {
|
|
192
|
+
const result = {
|
|
193
|
+
isValid: false,
|
|
194
|
+
errors: [],
|
|
195
|
+
warnings: [],
|
|
196
|
+
};
|
|
197
|
+
if (!bucketName || bucketName.trim().length === 0) {
|
|
198
|
+
result.errors.push("Bucket name cannot be empty");
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const clientConfig = { region };
|
|
203
|
+
if (awsProfile) {
|
|
204
|
+
const { fromIni } = await Promise.resolve().then(() => __importStar(require("@aws-sdk/credential-providers")));
|
|
205
|
+
clientConfig.credentials = fromIni({ profile: awsProfile });
|
|
206
|
+
}
|
|
207
|
+
const s3Client = new client_s3_1.S3Client(clientConfig);
|
|
208
|
+
// Test HeadBucket (verify bucket exists and we have access)
|
|
209
|
+
const headCommand = new client_s3_1.HeadBucketCommand({ Bucket: bucketName });
|
|
210
|
+
await s3Client.send(headCommand);
|
|
211
|
+
console.log(` ✓ S3 bucket accessible: ${bucketName}`);
|
|
212
|
+
// Test ListObjects (verify we can list objects)
|
|
213
|
+
const listCommand = new client_s3_1.ListObjectsV2Command({
|
|
214
|
+
Bucket: bucketName,
|
|
215
|
+
MaxKeys: 1,
|
|
216
|
+
});
|
|
217
|
+
await s3Client.send(listCommand);
|
|
218
|
+
console.log(" ✓ S3 bucket list permission confirmed");
|
|
219
|
+
result.isValid = true;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
const err = error;
|
|
223
|
+
result.errors.push(`S3 bucket validation failed: ${err.message}`);
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Validates complete ProfileConfig
|
|
229
|
+
*/
|
|
230
|
+
async function validateConfig(config, options = {}) {
|
|
231
|
+
const result = {
|
|
232
|
+
isValid: true,
|
|
233
|
+
errors: [],
|
|
234
|
+
warnings: [],
|
|
235
|
+
};
|
|
236
|
+
if (options.skipValidation) {
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
// Validate Benchling tenant
|
|
240
|
+
const tenantValidation = await validateBenchlingTenant(config.benchling.tenant);
|
|
241
|
+
if (!tenantValidation.isValid) {
|
|
242
|
+
result.isValid = false;
|
|
243
|
+
result.errors.push(...tenantValidation.errors);
|
|
244
|
+
}
|
|
245
|
+
if (tenantValidation.warnings && tenantValidation.warnings.length > 0) {
|
|
246
|
+
if (!result.warnings)
|
|
247
|
+
result.warnings = [];
|
|
248
|
+
result.warnings.push(...tenantValidation.warnings);
|
|
249
|
+
}
|
|
250
|
+
// Validate OAuth credentials (if secret is provided)
|
|
251
|
+
if (config.benchling.clientSecret) {
|
|
252
|
+
const credValidation = await validateBenchlingCredentials(config.benchling.tenant, config.benchling.clientId, config.benchling.clientSecret);
|
|
253
|
+
if (!credValidation.isValid) {
|
|
254
|
+
result.isValid = false;
|
|
255
|
+
result.errors.push(...credValidation.errors);
|
|
256
|
+
}
|
|
257
|
+
if (credValidation.warnings && credValidation.warnings.length > 0) {
|
|
258
|
+
if (!result.warnings)
|
|
259
|
+
result.warnings = [];
|
|
260
|
+
result.warnings.push(...credValidation.warnings);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Validate S3 bucket access
|
|
264
|
+
const bucketValidation = await validateS3BucketAccess(config.packages.bucket, config.deployment.region, options.awsProfile);
|
|
265
|
+
if (!bucketValidation.isValid) {
|
|
266
|
+
result.isValid = false;
|
|
267
|
+
result.errors.push(...bucketValidation.errors);
|
|
268
|
+
}
|
|
269
|
+
if (bucketValidation.warnings && bucketValidation.warnings.length > 0) {
|
|
270
|
+
if (!result.warnings)
|
|
271
|
+
result.warnings = [];
|
|
272
|
+
result.warnings.push(...bucketValidation.warnings);
|
|
273
|
+
}
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Runs interactive configuration wizard
|
|
278
|
+
*/
|
|
279
|
+
async function runConfigWizard(options = {}) {
|
|
280
|
+
const { existingConfig = {}, nonInteractive = false, inheritFrom } = options;
|
|
281
|
+
console.log("╔═══════════════════════════════════════════════════════════╗");
|
|
282
|
+
console.log("║ Benchling Webhook Configuration Wizard ║");
|
|
283
|
+
console.log("╚═══════════════════════════════════════════════════════════╝\n");
|
|
284
|
+
if (inheritFrom) {
|
|
285
|
+
console.log(`Creating profile inheriting from: ${inheritFrom}\n`);
|
|
286
|
+
}
|
|
287
|
+
const config = { ...existingConfig };
|
|
288
|
+
// If non-interactive, validate that all required fields are present
|
|
289
|
+
if (nonInteractive) {
|
|
290
|
+
if (!config.benchling?.tenant || !config.benchling?.clientId || !config.benchling?.clientSecret) {
|
|
291
|
+
throw new Error("Non-interactive mode requires benchlingTenant, benchlingClientId, and benchlingClientSecret to be already configured");
|
|
292
|
+
}
|
|
293
|
+
return config;
|
|
294
|
+
}
|
|
295
|
+
// Prompt for Quilt configuration (if not inherited)
|
|
296
|
+
if (!inheritFrom) {
|
|
297
|
+
// Check if all required Quilt fields are already present (from inference)
|
|
298
|
+
// Note: bucket is optional and can be inferred from other sources if missing
|
|
299
|
+
const hasAllQuiltFields = config.quilt?.stackArn &&
|
|
300
|
+
config.quilt?.catalog &&
|
|
301
|
+
config.quilt?.database &&
|
|
302
|
+
config.quilt?.queueArn;
|
|
303
|
+
let shouldPromptForQuilt = !hasAllQuiltFields;
|
|
304
|
+
if (hasAllQuiltFields) {
|
|
305
|
+
console.log("Step 1: Quilt Configuration (inferred from AWS)\n");
|
|
306
|
+
console.log(` Stack ARN: ${config.quilt.stackArn}`);
|
|
307
|
+
console.log(` Catalog: ${config.quilt.catalog}`);
|
|
308
|
+
console.log(` Database: ${config.quilt.database}`);
|
|
309
|
+
console.log(` Queue ARN: ${config.quilt.queueArn}`);
|
|
310
|
+
if (config.quilt.bucket) {
|
|
311
|
+
console.log(` Bucket: ${config.quilt.bucket}`);
|
|
312
|
+
}
|
|
313
|
+
console.log("");
|
|
314
|
+
const { confirmQuilt } = await inquirer_1.default.prompt([
|
|
315
|
+
{
|
|
316
|
+
type: "confirm",
|
|
317
|
+
name: "confirmQuilt",
|
|
318
|
+
message: "Use these inferred Quilt settings?",
|
|
319
|
+
default: true,
|
|
320
|
+
},
|
|
321
|
+
]);
|
|
322
|
+
if (!confirmQuilt) {
|
|
323
|
+
console.log("\nPlease enter Quilt configuration manually:\n");
|
|
324
|
+
shouldPromptForQuilt = true;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// Only prompt for Quilt fields if not all are present OR user chose to enter manually
|
|
328
|
+
if (shouldPromptForQuilt) {
|
|
329
|
+
if (!hasAllQuiltFields) {
|
|
330
|
+
console.log("Step 1: Quilt Configuration\n");
|
|
331
|
+
console.log("Note: Run 'npm run setup:infer' first to auto-detect Quilt stack\n");
|
|
332
|
+
}
|
|
333
|
+
const quiltAnswers = await inquirer_1.default.prompt([
|
|
334
|
+
{
|
|
335
|
+
type: "input",
|
|
336
|
+
name: "stackArn",
|
|
337
|
+
message: "Quilt Stack ARN:",
|
|
338
|
+
default: config.quilt?.stackArn,
|
|
339
|
+
validate: (input) => input.trim().length > 0 && input.startsWith("arn:aws:cloudformation:") ||
|
|
340
|
+
"Stack ARN is required and must start with arn:aws:cloudformation:",
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
type: "input",
|
|
344
|
+
name: "catalog",
|
|
345
|
+
message: "Quilt Catalog URL (domain or full URL):",
|
|
346
|
+
default: config.quilt?.catalog,
|
|
347
|
+
validate: (input) => {
|
|
348
|
+
const trimmed = input.trim();
|
|
349
|
+
if (trimmed.length === 0) {
|
|
350
|
+
return "Catalog URL is required";
|
|
351
|
+
}
|
|
352
|
+
return true;
|
|
353
|
+
},
|
|
354
|
+
filter: (input) => {
|
|
355
|
+
// Strip protocol if present, store only domain
|
|
356
|
+
return input.trim().replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
type: "input",
|
|
361
|
+
name: "bucket",
|
|
362
|
+
message: "Quilt S3 Bucket:",
|
|
363
|
+
default: config.quilt?.bucket,
|
|
364
|
+
validate: (input) => input.trim().length > 0 || "Bucket name is required",
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
type: "input",
|
|
368
|
+
name: "database",
|
|
369
|
+
message: "Quilt Athena Database:",
|
|
370
|
+
default: config.quilt?.database || "quilt_catalog",
|
|
371
|
+
validate: (input) => input.trim().length > 0 || "Database name is required",
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
type: "input",
|
|
375
|
+
name: "queueArn",
|
|
376
|
+
message: "SQS Queue ARN:",
|
|
377
|
+
default: config.quilt?.queueArn,
|
|
378
|
+
validate: (input) => input.trim().length > 0 && input.startsWith("arn:aws:sqs:") ||
|
|
379
|
+
"Queue ARN is required and must start with arn:aws:sqs:",
|
|
380
|
+
},
|
|
381
|
+
]);
|
|
382
|
+
// Extract region from stack ARN
|
|
383
|
+
const arnMatch = quiltAnswers.stackArn.match(/^arn:aws:cloudformation:([^:]+):/);
|
|
384
|
+
const quiltRegion = arnMatch ? arnMatch[1] : "us-east-1";
|
|
385
|
+
config.quilt = {
|
|
386
|
+
stackArn: quiltAnswers.stackArn,
|
|
387
|
+
catalog: quiltAnswers.catalog,
|
|
388
|
+
bucket: quiltAnswers.bucket,
|
|
389
|
+
database: quiltAnswers.database,
|
|
390
|
+
queueArn: quiltAnswers.queueArn,
|
|
391
|
+
region: quiltRegion,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// Prompt for Benchling configuration & guide app setup
|
|
396
|
+
console.log("\nStep 2: Create Benchling App\n");
|
|
397
|
+
let benchlingTenant = config.benchling?.tenant?.trim();
|
|
398
|
+
benchlingTenant = benchlingTenant && benchlingTenant.length > 0 ? benchlingTenant : undefined;
|
|
399
|
+
if (nonInteractive) {
|
|
400
|
+
if (!benchlingTenant) {
|
|
401
|
+
throw new Error("Benchling tenant must be provided in non-interactive mode");
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
while (!benchlingTenant) {
|
|
406
|
+
const tenantAnswer = await inquirer_1.default.prompt([
|
|
407
|
+
{
|
|
408
|
+
type: "input",
|
|
409
|
+
name: "tenant",
|
|
410
|
+
message: "Benchling tenant (e.g., 'acme' for acme.benchling.com):",
|
|
411
|
+
default: config.benchling?.tenant || "",
|
|
412
|
+
filter: (value) => value.trim(),
|
|
413
|
+
validate: (input) => input.trim().length > 0 || "Tenant is required",
|
|
414
|
+
},
|
|
415
|
+
]);
|
|
416
|
+
const candidateTenant = tenantAnswer.tenant.trim();
|
|
417
|
+
const tenantValidation = await validateBenchlingTenant(candidateTenant);
|
|
418
|
+
if (tenantValidation.isValid) {
|
|
419
|
+
benchlingTenant = candidateTenant;
|
|
420
|
+
if (tenantValidation.warnings && tenantValidation.warnings.length > 0) {
|
|
421
|
+
console.warn("");
|
|
422
|
+
console.warn("⚠ Tenant validation warnings:");
|
|
423
|
+
tenantValidation.warnings.forEach((warning) => console.warn(` - ${warning}`));
|
|
424
|
+
console.warn("");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
console.error("\n❌ Benchling tenant validation failed:");
|
|
429
|
+
tenantValidation.errors.forEach((err) => console.error(` - ${err}`));
|
|
430
|
+
console.log("");
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
let manifestPath = (0, path_1.join)(process.cwd(), MANIFEST_FILENAME);
|
|
435
|
+
if (!nonInteractive) {
|
|
436
|
+
const manifestContent = (0, manifest_1.generateBenchlingManifest)({
|
|
437
|
+
catalogDomain: config.quilt?.catalog,
|
|
438
|
+
version: pkg.version,
|
|
439
|
+
});
|
|
440
|
+
let shouldWriteManifest = true;
|
|
441
|
+
if ((0, fs_1.existsSync)(manifestPath)) {
|
|
442
|
+
const { overwrite } = await inquirer_1.default.prompt([
|
|
443
|
+
{
|
|
444
|
+
type: "confirm",
|
|
445
|
+
name: "overwrite",
|
|
446
|
+
message: `A manifest already exists at ${manifestPath}. Overwrite it?`,
|
|
447
|
+
default: false,
|
|
448
|
+
},
|
|
449
|
+
]);
|
|
450
|
+
shouldWriteManifest = overwrite;
|
|
451
|
+
if (!overwrite) {
|
|
452
|
+
console.log(`\nUsing existing manifest: ${manifestPath}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (shouldWriteManifest) {
|
|
456
|
+
(0, fs_1.writeFileSync)(manifestPath, manifestContent, "utf-8");
|
|
457
|
+
console.log(`\n✓ Generated app manifest: ${manifestPath}`);
|
|
458
|
+
}
|
|
459
|
+
const manifestAppName = config.quilt?.catalog && config.quilt.catalog.length > 0
|
|
460
|
+
? config.quilt.catalog.replace(/[.:]/g, "-")
|
|
461
|
+
: "Quilt Integration";
|
|
462
|
+
const instructions = chalk_1.default.bold("Create your Benchling app:\n\n") +
|
|
463
|
+
`1. Open ${chalk_1.default.cyan(`https://${benchlingTenant}.benchling.com/admin/apps`)}\n` +
|
|
464
|
+
"2. Click 'Create New App'\n" +
|
|
465
|
+
`3. Upload the manifest: ${chalk_1.default.cyan(manifestPath)}\n` +
|
|
466
|
+
"4. Create OAuth credentials and copy the Client ID / Secret\n" +
|
|
467
|
+
"5. Install the app (leave webhook URL blank for now)\n" +
|
|
468
|
+
"6. Copy the App Definition ID from the overview page\n";
|
|
469
|
+
console.log();
|
|
470
|
+
console.log((0, boxen_1.default)(instructions, {
|
|
471
|
+
padding: 1,
|
|
472
|
+
borderColor: "blue",
|
|
473
|
+
borderStyle: "round",
|
|
474
|
+
}));
|
|
475
|
+
console.log();
|
|
476
|
+
const { ready } = await inquirer_1.default.prompt([
|
|
477
|
+
{
|
|
478
|
+
type: "confirm",
|
|
479
|
+
name: "ready",
|
|
480
|
+
message: "Have you created and installed the Benchling app?",
|
|
481
|
+
default: true,
|
|
482
|
+
},
|
|
483
|
+
]);
|
|
484
|
+
if (!ready) {
|
|
485
|
+
console.log("\n⏸ Setup paused. Re-run when ready:\n");
|
|
486
|
+
console.log(` ${chalk_1.default.cyan("npx @quiltdata/benchling-webhook setup")}\n`);
|
|
487
|
+
process.exit(0);
|
|
488
|
+
}
|
|
489
|
+
console.log(`Using manifest app name: ${manifestAppName}\n`);
|
|
490
|
+
}
|
|
491
|
+
let benchlingClientId = config.benchling?.clientId?.trim();
|
|
492
|
+
let benchlingClientSecret = config.benchling?.clientSecret?.trim();
|
|
493
|
+
let benchlingAppDefinitionId = config.benchling?.appDefinitionId?.trim();
|
|
494
|
+
let benchlingTestEntryId = config.benchling?.testEntryId?.trim();
|
|
495
|
+
if (nonInteractive) {
|
|
496
|
+
if (!benchlingClientId || !benchlingClientSecret || !benchlingAppDefinitionId) {
|
|
497
|
+
throw new Error("Non-interactive mode requires Benchling clientId, clientSecret, and appDefinitionId to be configured");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
let credentialsValid = false;
|
|
502
|
+
while (!credentialsValid) {
|
|
503
|
+
const credentialAnswers = await inquirer_1.default.prompt([
|
|
504
|
+
{
|
|
505
|
+
type: "input",
|
|
506
|
+
name: "clientId",
|
|
507
|
+
message: "Benchling OAuth Client ID:",
|
|
508
|
+
default: benchlingClientId || "",
|
|
509
|
+
filter: (value) => value.trim(),
|
|
510
|
+
validate: (input) => input.trim().length > 0 || "Client ID is required",
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
type: "password",
|
|
514
|
+
name: "clientSecret",
|
|
515
|
+
message: benchlingClientSecret
|
|
516
|
+
? "Benchling OAuth Client Secret (press Enter to keep existing):"
|
|
517
|
+
: "Benchling OAuth Client Secret:",
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
type: "input",
|
|
521
|
+
name: "appDefinitionId",
|
|
522
|
+
message: "Benchling App Definition ID:",
|
|
523
|
+
default: benchlingAppDefinitionId || "",
|
|
524
|
+
filter: (value) => value.trim(),
|
|
525
|
+
validate: (input) => input.trim().length > 0 || "App Definition ID is required",
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
type: "input",
|
|
529
|
+
name: "testEntryId",
|
|
530
|
+
message: "Benchling Test Entry ID (optional):",
|
|
531
|
+
default: benchlingTestEntryId || "",
|
|
532
|
+
filter: (value) => value.trim(),
|
|
533
|
+
},
|
|
534
|
+
]);
|
|
535
|
+
const candidateSecret = credentialAnswers.clientSecret.trim().length === 0 && benchlingClientSecret
|
|
536
|
+
? benchlingClientSecret
|
|
537
|
+
: credentialAnswers.clientSecret.trim();
|
|
538
|
+
if (!candidateSecret) {
|
|
539
|
+
console.error("\n❌ Benchling OAuth client secret is required\n");
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
const spinner = (0, ora_1.default)("Validating Benchling credentials...").start();
|
|
543
|
+
const validation = await validateBenchlingCredentials(benchlingTenant, credentialAnswers.clientId.trim(), candidateSecret);
|
|
544
|
+
if (validation.isValid) {
|
|
545
|
+
spinner.succeed("Benchling credentials validated");
|
|
546
|
+
if (validation.warnings && validation.warnings.length > 0) {
|
|
547
|
+
console.warn("\n⚠ Credential validation warnings:");
|
|
548
|
+
validation.warnings.forEach((warn) => console.warn(` - ${warn}`));
|
|
549
|
+
console.warn("");
|
|
550
|
+
}
|
|
551
|
+
benchlingClientId = credentialAnswers.clientId.trim();
|
|
552
|
+
benchlingClientSecret = candidateSecret;
|
|
553
|
+
benchlingAppDefinitionId = credentialAnswers.appDefinitionId.trim();
|
|
554
|
+
benchlingTestEntryId = credentialAnswers.testEntryId || undefined;
|
|
555
|
+
credentialsValid = true;
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
spinner.fail("Benchling credential validation failed");
|
|
559
|
+
console.error("");
|
|
560
|
+
validation.errors.forEach((err) => console.error(` - ${err}`));
|
|
561
|
+
console.error("");
|
|
562
|
+
const { retry } = await inquirer_1.default.prompt([
|
|
563
|
+
{
|
|
564
|
+
type: "confirm",
|
|
565
|
+
name: "retry",
|
|
566
|
+
message: "Credentials invalid. Try again?",
|
|
567
|
+
default: true,
|
|
568
|
+
},
|
|
569
|
+
]);
|
|
570
|
+
if (!retry) {
|
|
571
|
+
throw new Error("Setup aborted due to invalid Benchling credentials");
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
config.benchling = {
|
|
577
|
+
tenant: benchlingTenant,
|
|
578
|
+
clientId: benchlingClientId,
|
|
579
|
+
clientSecret: benchlingClientSecret,
|
|
580
|
+
appDefinitionId: benchlingAppDefinitionId,
|
|
581
|
+
};
|
|
582
|
+
if (benchlingTestEntryId && benchlingTestEntryId.length > 0) {
|
|
583
|
+
config.benchling.testEntryId = benchlingTestEntryId;
|
|
584
|
+
}
|
|
585
|
+
// Prompt for package configuration
|
|
586
|
+
console.log("\nPackage Storage Configuration\n");
|
|
587
|
+
const packageAnswers = await inquirer_1.default.prompt([
|
|
588
|
+
{
|
|
589
|
+
type: "input",
|
|
590
|
+
name: "bucket",
|
|
591
|
+
message: "Package S3 Bucket:",
|
|
592
|
+
default: config.packages?.bucket || config.quilt?.bucket,
|
|
593
|
+
validate: (input) => input.trim().length > 0 || "Bucket name is required",
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
type: "input",
|
|
597
|
+
name: "prefix",
|
|
598
|
+
message: "Package S3 prefix:",
|
|
599
|
+
default: config.packages?.prefix || "benchling",
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
type: "input",
|
|
603
|
+
name: "metadataKey",
|
|
604
|
+
message: "Package metadata key:",
|
|
605
|
+
default: config.packages?.metadataKey || "experiment_id",
|
|
606
|
+
},
|
|
607
|
+
]);
|
|
608
|
+
config.packages = {
|
|
609
|
+
bucket: packageAnswers.bucket,
|
|
610
|
+
prefix: packageAnswers.prefix,
|
|
611
|
+
metadataKey: packageAnswers.metadataKey,
|
|
612
|
+
};
|
|
613
|
+
// Prompt for deployment configuration
|
|
614
|
+
console.log("\nDeployment Defaults\n");
|
|
615
|
+
const defaultAccount = config.deployment?.account || getAccountFromStackArn(config.quilt?.stackArn);
|
|
616
|
+
const deploymentAnswers = await inquirer_1.default.prompt([
|
|
617
|
+
{
|
|
618
|
+
type: "input",
|
|
619
|
+
name: "account",
|
|
620
|
+
message: "AWS Account ID:",
|
|
621
|
+
default: defaultAccount || "",
|
|
622
|
+
filter: (value) => value.trim(),
|
|
623
|
+
validate: (input) => input.trim().length === 0 || /^[0-9]{12}$/.test(input.trim()) || "Account ID must be a 12 digit number",
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
type: "input",
|
|
627
|
+
name: "region",
|
|
628
|
+
message: "AWS Deployment Region:",
|
|
629
|
+
default: config.deployment?.region || config.quilt?.region || "us-east-1",
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
type: "input",
|
|
633
|
+
name: "imageTag",
|
|
634
|
+
message: "Docker image tag:",
|
|
635
|
+
default: config.deployment?.imageTag || "latest",
|
|
636
|
+
},
|
|
637
|
+
]);
|
|
638
|
+
config.deployment = {
|
|
639
|
+
...(deploymentAnswers.account ? { account: deploymentAnswers.account } : {}),
|
|
640
|
+
region: deploymentAnswers.region,
|
|
641
|
+
imageTag: deploymentAnswers.imageTag,
|
|
642
|
+
};
|
|
643
|
+
// Optional: Logging configuration
|
|
644
|
+
console.log("\nSecurity and Logging Options\n");
|
|
645
|
+
const optionalAnswers = await inquirer_1.default.prompt([
|
|
646
|
+
{
|
|
647
|
+
type: "list",
|
|
648
|
+
name: "logLevel",
|
|
649
|
+
message: "Log level:",
|
|
650
|
+
choices: ["DEBUG", "INFO", "WARNING", "ERROR"],
|
|
651
|
+
default: config.logging?.level || "INFO",
|
|
652
|
+
},
|
|
79
653
|
{
|
|
80
654
|
type: "confirm",
|
|
81
|
-
name: "
|
|
82
|
-
message: "
|
|
83
|
-
default:
|
|
655
|
+
name: "enableVerification",
|
|
656
|
+
message: "Enable webhook signature verification:",
|
|
657
|
+
default: config.security?.enableVerification !== false,
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
type: "input",
|
|
661
|
+
name: "webhookAllowList",
|
|
662
|
+
message: "Webhook IP allowlist (comma-separated, empty for none):",
|
|
663
|
+
default: config.security?.webhookAllowList || "",
|
|
84
664
|
},
|
|
85
665
|
]);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
666
|
+
config.logging = {
|
|
667
|
+
level: optionalAnswers.logLevel,
|
|
668
|
+
};
|
|
669
|
+
config.security = {
|
|
670
|
+
enableVerification: optionalAnswers.enableVerification,
|
|
671
|
+
webhookAllowList: optionalAnswers.webhookAllowList,
|
|
672
|
+
};
|
|
673
|
+
// Add metadata
|
|
674
|
+
const now = new Date().toISOString();
|
|
675
|
+
config._metadata = {
|
|
676
|
+
version: pkg.version,
|
|
677
|
+
createdAt: config._metadata?.createdAt || now,
|
|
678
|
+
updatedAt: now,
|
|
679
|
+
source: "wizard",
|
|
680
|
+
};
|
|
681
|
+
// Add inheritance marker if specified
|
|
682
|
+
if (inheritFrom) {
|
|
683
|
+
config._inherits = inheritFrom;
|
|
684
|
+
}
|
|
685
|
+
return config;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Main install wizard function
|
|
689
|
+
*
|
|
690
|
+
* Orchestrates the complete configuration workflow:
|
|
691
|
+
* 1. Load existing configuration (if any)
|
|
692
|
+
* 2. Infer Quilt configuration from AWS
|
|
693
|
+
* 3. Run interactive prompts for missing fields
|
|
694
|
+
* 4. Validate configuration
|
|
695
|
+
* 5. Save to XDG config directory
|
|
696
|
+
*/
|
|
697
|
+
async function runInstallWizard(options = {}) {
|
|
698
|
+
const { profile = "default", inheritFrom, nonInteractive = false, skipValidation = false, skipSecretsSync = false, skipDeployment = false, deployStage = "prod", awsProfile, awsRegion = "us-east-1", } = options;
|
|
699
|
+
const xdg = new xdg_config_1.XDGConfig();
|
|
700
|
+
console.log("\n╔═══════════════════════════════════════════════════════════╗");
|
|
701
|
+
const headerLine = `║ Benchling Webhook Setup (v${pkg.version})`;
|
|
702
|
+
console.log(`${headerLine.padEnd(59, " ")}║`);
|
|
703
|
+
console.log("╚═══════════════════════════════════════════════════════════╝\n");
|
|
704
|
+
// Step 1: Load existing configuration (if profile exists)
|
|
705
|
+
let existingConfig;
|
|
706
|
+
if (xdg.profileExists(profile)) {
|
|
707
|
+
console.log(`Loading existing configuration for profile: ${profile}\n`);
|
|
708
|
+
try {
|
|
709
|
+
existingConfig = inheritFrom
|
|
710
|
+
? xdg.readProfileWithInheritance(profile, inheritFrom)
|
|
711
|
+
: xdg.readProfile(profile);
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
console.warn(`Warning: Could not load existing config: ${error.message}`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
else if (inheritFrom) {
|
|
718
|
+
console.log(`Creating new profile '${profile}' inheriting from '${inheritFrom}'\n`);
|
|
719
|
+
try {
|
|
720
|
+
existingConfig = xdg.readProfile(inheritFrom);
|
|
721
|
+
}
|
|
722
|
+
catch (error) {
|
|
723
|
+
throw new Error(`Base profile '${inheritFrom}' not found: ${error.message}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
// Step 2: Infer Quilt configuration (unless inheriting from another profile)
|
|
727
|
+
let quiltConfig = existingConfig?.quilt || {};
|
|
728
|
+
if (!inheritFrom || !existingConfig?.quilt) {
|
|
729
|
+
console.log("Step 1: Inferring Quilt configuration from AWS...\n");
|
|
730
|
+
try {
|
|
731
|
+
const inferenceResult = await (0, infer_quilt_config_1.inferQuiltConfig)({
|
|
732
|
+
region: awsRegion,
|
|
733
|
+
profile: awsProfile,
|
|
734
|
+
interactive: !nonInteractive,
|
|
735
|
+
});
|
|
736
|
+
// Map InferenceResult to QuiltConfig fields
|
|
737
|
+
if (inferenceResult.quiltStackArn) {
|
|
738
|
+
quiltConfig.stackArn = inferenceResult.quiltStackArn;
|
|
739
|
+
}
|
|
740
|
+
if (inferenceResult.catalogUrl) {
|
|
741
|
+
// Strip protocol and trailing slash to store only domain
|
|
742
|
+
quiltConfig.catalog = inferenceResult.catalogUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
743
|
+
}
|
|
744
|
+
if (inferenceResult.quiltUserBucket) {
|
|
745
|
+
quiltConfig.bucket = inferenceResult.quiltUserBucket;
|
|
746
|
+
}
|
|
747
|
+
if (inferenceResult.quiltDatabase) {
|
|
748
|
+
quiltConfig.database = inferenceResult.quiltDatabase;
|
|
749
|
+
}
|
|
750
|
+
if (inferenceResult.queueArn) {
|
|
751
|
+
const queueArn = inferenceResult.queueArn;
|
|
752
|
+
if (queueArn.startsWith("arn:aws:sqs:") || !quiltConfig.queueArn) {
|
|
753
|
+
quiltConfig.queueArn = queueArn;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (inferenceResult.quiltRegion) {
|
|
757
|
+
quiltConfig.region = inferenceResult.quiltRegion;
|
|
758
|
+
}
|
|
759
|
+
console.log("✓ Quilt configuration inferred\n");
|
|
760
|
+
}
|
|
761
|
+
catch (error) {
|
|
762
|
+
console.error(`Failed to infer Quilt configuration: ${error.message}`);
|
|
763
|
+
if (nonInteractive) {
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
766
|
+
const { continueManually } = await inquirer_1.default.prompt([
|
|
767
|
+
{
|
|
768
|
+
type: "confirm",
|
|
769
|
+
name: "continueManually",
|
|
770
|
+
message: "Continue and enter Quilt configuration manually?",
|
|
771
|
+
default: true,
|
|
772
|
+
},
|
|
773
|
+
]);
|
|
774
|
+
if (!continueManually) {
|
|
775
|
+
throw new Error("Setup aborted by user");
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
// Merge inferred Quilt config with existing config
|
|
780
|
+
const partialConfig = {
|
|
781
|
+
...existingConfig,
|
|
782
|
+
quilt: {
|
|
783
|
+
...existingConfig?.quilt,
|
|
784
|
+
...quiltConfig,
|
|
785
|
+
},
|
|
786
|
+
};
|
|
787
|
+
// Step 3: Run interactive wizard for remaining configuration
|
|
788
|
+
let config = await runConfigWizard({
|
|
789
|
+
existingConfig: partialConfig,
|
|
790
|
+
nonInteractive,
|
|
791
|
+
inheritFrom,
|
|
792
|
+
});
|
|
793
|
+
// Step 4: Validate configuration
|
|
794
|
+
if (!skipValidation) {
|
|
795
|
+
console.log("\nValidating configuration...\n");
|
|
796
|
+
const validation = await validateConfig(config, {
|
|
797
|
+
skipValidation,
|
|
798
|
+
awsProfile,
|
|
799
|
+
});
|
|
800
|
+
if (!validation.isValid) {
|
|
801
|
+
console.error("\n❌ Configuration validation failed:");
|
|
802
|
+
validation.errors.forEach((err) => console.error(` - ${err}`));
|
|
803
|
+
if (nonInteractive) {
|
|
804
|
+
throw new Error("Configuration validation failed");
|
|
805
|
+
}
|
|
806
|
+
const { proceed } = await inquirer_1.default.prompt([
|
|
807
|
+
{
|
|
808
|
+
type: "confirm",
|
|
809
|
+
name: "proceed",
|
|
810
|
+
message: "Save configuration anyway?",
|
|
811
|
+
default: false,
|
|
812
|
+
},
|
|
813
|
+
]);
|
|
814
|
+
if (!proceed) {
|
|
815
|
+
throw new Error("Setup aborted by user");
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
console.log("✓ Configuration validated successfully\n");
|
|
820
|
+
}
|
|
821
|
+
if (validation.warnings && validation.warnings.length > 0) {
|
|
822
|
+
console.warn("\n⚠ Warnings:");
|
|
823
|
+
validation.warnings.forEach((warn) => console.warn(` - ${warn}`));
|
|
824
|
+
console.log("");
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
// Step 5: Persist configuration locally
|
|
828
|
+
console.log(`Saving configuration to profile: ${profile}...\n`);
|
|
829
|
+
try {
|
|
830
|
+
xdg.writeProfile(profile, config);
|
|
831
|
+
console.log(chalk_1.default.green(`✓ Configuration saved: ~/.config/benchling-webhook/${profile}/config.json\n`));
|
|
832
|
+
}
|
|
833
|
+
catch (error) {
|
|
834
|
+
throw new Error(`Failed to save configuration: ${error.message}`);
|
|
835
|
+
}
|
|
836
|
+
// Step 6: Sync secrets and deploy (unless skipped)
|
|
837
|
+
console.log(chalk_1.default.bold("Step 3: Deploy Stack and Return Webhook URL\n"));
|
|
838
|
+
if (skipSecretsSync) {
|
|
839
|
+
console.log(chalk_1.default.yellow("Skipping secrets sync (--skip-secrets-sync)."));
|
|
840
|
+
console.log(chalk_1.default.yellow(`Run \`npm run setup:sync-secrets -- --profile ${profile}\` when ready.\n`));
|
|
95
841
|
}
|
|
96
842
|
else {
|
|
97
|
-
console.log("
|
|
98
|
-
|
|
99
|
-
|
|
843
|
+
console.log("Syncing secrets to AWS Secrets Manager...\n");
|
|
844
|
+
try {
|
|
845
|
+
await (0, sync_secrets_1.syncSecretsToAWS)({
|
|
846
|
+
profile,
|
|
847
|
+
awsProfile,
|
|
848
|
+
region: config.deployment.region,
|
|
849
|
+
force: true,
|
|
850
|
+
});
|
|
851
|
+
console.log(chalk_1.default.green("\n✓ Secrets synced to AWS Secrets Manager\n"));
|
|
852
|
+
// Reload config to capture secret ARN written by sync
|
|
853
|
+
config = xdg.readProfile(profile);
|
|
854
|
+
}
|
|
855
|
+
catch (error) {
|
|
856
|
+
throw new Error(`Secrets sync failed: ${error.message}`);
|
|
857
|
+
}
|
|
100
858
|
}
|
|
101
|
-
|
|
859
|
+
if (skipDeployment) {
|
|
860
|
+
console.log(chalk_1.default.yellow("Skipping deployment (--skip-deployment)."));
|
|
861
|
+
console.log(chalk_1.default.yellow(`Run \`npm run deploy -- --profile ${profile} --stage ${deployStage}\` to deploy later.\n`));
|
|
862
|
+
return config;
|
|
863
|
+
}
|
|
864
|
+
if (!nonInteractive) {
|
|
865
|
+
console.log(`Deploy target: profile=${chalk_1.default.cyan(profile)}, stage=${chalk_1.default.cyan(deployStage)}, region=${chalk_1.default.cyan(config.deployment.region)}\n`);
|
|
866
|
+
const { confirmDeploy } = await inquirer_1.default.prompt([
|
|
867
|
+
{
|
|
868
|
+
type: "confirm",
|
|
869
|
+
name: "confirmDeploy",
|
|
870
|
+
message: "Deploy AWS infrastructure now? (takes ~5-10 minutes)",
|
|
871
|
+
default: true,
|
|
872
|
+
},
|
|
873
|
+
]);
|
|
874
|
+
if (!confirmDeploy) {
|
|
875
|
+
console.log(chalk_1.default.yellow("\nDeployment skipped by user."));
|
|
876
|
+
console.log(chalk_1.default.yellow(`Re-run with \`npm run deploy -- --profile ${profile} --stage ${deployStage}\` when ready.\n`));
|
|
877
|
+
return config;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
console.log("Deploying AWS infrastructure. This can take several minutes...\n");
|
|
881
|
+
await (0, deploy_1.deployCommand)({
|
|
882
|
+
profile,
|
|
883
|
+
stage: deployStage,
|
|
884
|
+
requireApproval: "never",
|
|
885
|
+
yes: true,
|
|
886
|
+
});
|
|
887
|
+
return config;
|
|
888
|
+
}
|
|
889
|
+
// =============================================================================
|
|
890
|
+
// CLI COMMAND EXPORT
|
|
891
|
+
// =============================================================================
|
|
892
|
+
/**
|
|
893
|
+
* Setup wizard command handler
|
|
894
|
+
*
|
|
895
|
+
* @param options - Wizard options
|
|
896
|
+
* @returns Promise that resolves when wizard completes
|
|
897
|
+
*/
|
|
898
|
+
async function setupWizardCommand(options = {}) {
|
|
899
|
+
await runInstallWizard(options);
|
|
102
900
|
}
|
|
103
901
|
//# sourceMappingURL=setup-wizard.js.map
|