@quiltdata/benchling-webhook 0.7.7 → 0.7.8-20251115T063729Z
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 +24 -11
- package/dist/bin/cli.js +77 -3
- package/dist/bin/cli.js.map +1 -1
- package/dist/bin/commands/config-show.d.ts +14 -0
- package/dist/bin/commands/config-show.d.ts.map +1 -0
- package/dist/bin/commands/config-show.js +24 -0
- package/dist/bin/commands/config-show.js.map +1 -0
- package/dist/bin/commands/deploy.d.ts +14 -0
- package/dist/bin/commands/deploy.d.ts.map +1 -1
- package/dist/bin/commands/deploy.js +1 -0
- package/dist/bin/commands/deploy.js.map +1 -1
- package/dist/bin/commands/infer-quilt-config.d.ts +3 -0
- package/dist/bin/commands/infer-quilt-config.d.ts.map +1 -1
- package/dist/bin/commands/infer-quilt-config.js +74 -8
- package/dist/bin/commands/infer-quilt-config.js.map +1 -1
- package/dist/bin/commands/install.d.ts.map +1 -1
- package/dist/bin/commands/install.js +40 -7
- package/dist/bin/commands/install.js.map +1 -1
- package/dist/bin/commands/manifest.d.ts +20 -3
- package/dist/bin/commands/manifest.d.ts.map +1 -1
- package/dist/bin/commands/manifest.js +23 -7
- package/dist/bin/commands/manifest.js.map +1 -1
- package/dist/bin/commands/setup-wizard.d.ts +67 -13
- package/dist/bin/commands/setup-wizard.d.ts.map +1 -1
- package/dist/bin/commands/setup-wizard.js +239 -742
- package/dist/bin/commands/setup-wizard.js.map +1 -1
- package/dist/bin/commands/sync-secrets.d.ts +7 -7
- package/dist/bin/commands/sync-secrets.d.ts.map +1 -1
- package/dist/bin/commands/sync-secrets.js +55 -15
- package/dist/bin/commands/sync-secrets.js.map +1 -1
- package/dist/bin/commands/validate.d.ts +18 -3
- package/dist/bin/commands/validate.d.ts.map +1 -1
- package/dist/bin/commands/validate.js +103 -69
- package/dist/bin/commands/validate.js.map +1 -1
- package/dist/lib/types/config.d.ts +13 -0
- package/dist/lib/types/config.d.ts.map +1 -1
- package/dist/lib/types/config.js +1 -0
- package/dist/lib/types/config.js.map +1 -1
- package/dist/lib/utils/config.d.ts +14 -17
- package/dist/lib/utils/config.d.ts.map +1 -1
- package/dist/lib/utils/config.js +6 -88
- package/dist/lib/utils/config.js.map +1 -1
- package/dist/lib/wizard/phase1-catalog-discovery.d.ts +41 -0
- package/dist/lib/wizard/phase1-catalog-discovery.d.ts.map +1 -0
- package/dist/lib/wizard/phase1-catalog-discovery.js +206 -0
- package/dist/lib/wizard/phase1-catalog-discovery.js.map +1 -0
- package/dist/lib/wizard/phase2-stack-query.d.ts +35 -0
- package/dist/lib/wizard/phase2-stack-query.d.ts.map +1 -0
- package/dist/lib/wizard/phase2-stack-query.js +99 -0
- package/dist/lib/wizard/phase2-stack-query.js.map +1 -0
- package/dist/lib/wizard/phase3-parameter-collection.d.ts +24 -0
- package/dist/lib/wizard/phase3-parameter-collection.d.ts.map +1 -0
- package/dist/lib/wizard/phase3-parameter-collection.js +399 -0
- package/dist/lib/wizard/phase3-parameter-collection.js.map +1 -0
- package/dist/lib/wizard/phase4-validation.d.ts +22 -0
- package/dist/lib/wizard/phase4-validation.d.ts.map +1 -0
- package/dist/lib/wizard/phase4-validation.js +294 -0
- package/dist/lib/wizard/phase4-validation.js.map +1 -0
- package/dist/lib/wizard/phase5-mode-decision.d.ts +22 -0
- package/dist/lib/wizard/phase5-mode-decision.d.ts.map +1 -0
- package/dist/lib/wizard/phase5-mode-decision.js +65 -0
- package/dist/lib/wizard/phase5-mode-decision.js.map +1 -0
- package/dist/lib/wizard/phase6-integrated-mode.d.ts +24 -0
- package/dist/lib/wizard/phase6-integrated-mode.d.ts.map +1 -0
- package/dist/lib/wizard/phase6-integrated-mode.js +131 -0
- package/dist/lib/wizard/phase6-integrated-mode.js.map +1 -0
- package/dist/lib/wizard/phase7-standalone-mode.d.ts +25 -0
- package/dist/lib/wizard/phase7-standalone-mode.d.ts.map +1 -0
- package/dist/lib/wizard/phase7-standalone-mode.js +210 -0
- package/dist/lib/wizard/phase7-standalone-mode.js.map +1 -0
- package/dist/lib/wizard/types.d.ts +175 -0
- package/dist/lib/wizard/types.d.ts.map +1 -0
- package/dist/lib/wizard/types.js +11 -0
- package/dist/lib/wizard/types.js.map +1 -0
- package/dist/package.json +3 -4
- package/package.json +3 -4
- package/dist/bin/commands/get-env.d.ts +0 -16
- package/dist/bin/commands/get-env.d.ts.map +0 -1
- package/dist/bin/commands/get-env.js +0 -210
- package/dist/bin/commands/get-env.js.map +0 -1
- package/env.template +0 -79
|
@@ -1,735 +1,223 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
/**
|
|
4
|
-
* Interactive Configuration Wizard (v0.
|
|
4
|
+
* Interactive Configuration Wizard (v0.8.0)
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* 1.
|
|
8
|
-
* 2.
|
|
9
|
-
* 3.
|
|
10
|
-
* 4.
|
|
6
|
+
* Phase-based modular setup wizard that orchestrates:
|
|
7
|
+
* 1. Catalog discovery and confirmation
|
|
8
|
+
* 2. Stack query for infrastructure details
|
|
9
|
+
* 3. Parameter collection from user
|
|
10
|
+
* 4. Configuration validation
|
|
11
|
+
* 5. Deployment mode decision (integrated vs standalone)
|
|
12
|
+
* 6. Mode-specific setup (integrated or standalone)
|
|
11
13
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
+
* Architecture:
|
|
15
|
+
* - Each phase is a separate, testable module
|
|
16
|
+
* - Explicit data flow between phases via TypeScript types
|
|
17
|
+
* - Cannot skip phases or execute out of order
|
|
18
|
+
* - Integrated mode has explicit return (no deployment)
|
|
14
19
|
*
|
|
15
20
|
* @module commands/setup-wizard
|
|
21
|
+
* @version 0.8.0
|
|
16
22
|
*/
|
|
17
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
-
if (k2 === undefined) k2 = k;
|
|
19
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
-
}
|
|
23
|
-
Object.defineProperty(o, k2, desc);
|
|
24
|
-
}) : (function(o, m, k, k2) {
|
|
25
|
-
if (k2 === undefined) k2 = k;
|
|
26
|
-
o[k2] = m[k];
|
|
27
|
-
}));
|
|
28
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
-
}) : function(o, v) {
|
|
31
|
-
o["default"] = v;
|
|
32
|
-
});
|
|
33
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
-
var ownKeys = function(o) {
|
|
35
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
-
var ar = [];
|
|
37
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
-
return ar;
|
|
39
|
-
};
|
|
40
|
-
return ownKeys(o);
|
|
41
|
-
};
|
|
42
|
-
return function (mod) {
|
|
43
|
-
if (mod && mod.__esModule) return mod;
|
|
44
|
-
var result = {};
|
|
45
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
-
__setModuleDefault(result, mod);
|
|
47
|
-
return result;
|
|
48
|
-
};
|
|
49
|
-
})();
|
|
50
23
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
51
24
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
52
25
|
};
|
|
53
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.runSetupWizard = runSetupWizard;
|
|
54
28
|
exports.setupWizardCommand = setupWizardCommand;
|
|
55
|
-
const https = __importStar(require("https"));
|
|
56
|
-
const inquirer_1 = __importDefault(require("inquirer"));
|
|
57
29
|
const chalk_1 = __importDefault(require("chalk"));
|
|
58
|
-
const
|
|
30
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
59
31
|
const xdg_config_1 = require("../../lib/xdg-config");
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
32
|
+
const fs_1 = require("fs");
|
|
33
|
+
const path_1 = require("path");
|
|
34
|
+
// Phase modules
|
|
35
|
+
const phase1_catalog_discovery_1 = require("../../lib/wizard/phase1-catalog-discovery");
|
|
36
|
+
const phase2_stack_query_1 = require("../../lib/wizard/phase2-stack-query");
|
|
37
|
+
const phase3_parameter_collection_1 = require("../../lib/wizard/phase3-parameter-collection");
|
|
38
|
+
const phase4_validation_1 = require("../../lib/wizard/phase4-validation");
|
|
39
|
+
const phase5_mode_decision_1 = require("../../lib/wizard/phase5-mode-decision");
|
|
40
|
+
const phase6_integrated_mode_1 = require("../../lib/wizard/phase6-integrated-mode");
|
|
41
|
+
const phase7_standalone_mode_1 = require("../../lib/wizard/phase7-standalone-mode");
|
|
67
42
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* @param bucketName - Name of the S3 bucket
|
|
71
|
-
* @param awsProfile - Optional AWS profile to use
|
|
72
|
-
* @returns The bucket's actual region, or null if detection fails
|
|
43
|
+
* Step titles for the wizard phases
|
|
44
|
+
* This is the single source of truth for step numbering and titles
|
|
73
45
|
*/
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const s3Client = new client_s3_1.S3Client(clientConfig);
|
|
84
|
-
const command = new client_s3_1.GetBucketLocationCommand({ Bucket: bucketName });
|
|
85
|
-
const response = await s3Client.send(command);
|
|
86
|
-
// AWS returns null for us-east-1, otherwise returns the region constraint
|
|
87
|
-
const region = response.LocationConstraint || "us-east-1";
|
|
88
|
-
return region;
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
// If we can't detect the region, return null and let validation proceed with the provided region
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Validates Benchling tenant accessibility
|
|
97
|
-
*/
|
|
98
|
-
async function validateBenchlingTenant(tenant) {
|
|
99
|
-
const result = {
|
|
100
|
-
isValid: false,
|
|
101
|
-
errors: [],
|
|
102
|
-
warnings: [],
|
|
103
|
-
};
|
|
104
|
-
if (!tenant || tenant.trim().length === 0) {
|
|
105
|
-
result.errors.push("Tenant name cannot be empty");
|
|
106
|
-
return result;
|
|
107
|
-
}
|
|
108
|
-
// Basic format validation
|
|
109
|
-
if (!/^[a-zA-Z0-9-_]+$/.test(tenant)) {
|
|
110
|
-
result.errors.push("Tenant name contains invalid characters (only alphanumeric, dash, underscore allowed)");
|
|
111
|
-
return result;
|
|
112
|
-
}
|
|
113
|
-
// Test tenant URL accessibility
|
|
114
|
-
const tenantUrl = `https://${tenant}.benchling.com`;
|
|
115
|
-
console.log(` Testing Benchling tenant URL: ${tenantUrl}`);
|
|
116
|
-
return new Promise((resolve) => {
|
|
117
|
-
https
|
|
118
|
-
.get(tenantUrl, { timeout: 5000 }, (res) => {
|
|
119
|
-
if (res.statusCode === 200 || res.statusCode === 302 || res.statusCode === 301) {
|
|
120
|
-
result.isValid = true;
|
|
121
|
-
console.log(` ✓ Tenant URL accessible: ${tenantUrl}`);
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
if (!result.warnings)
|
|
125
|
-
result.warnings = [];
|
|
126
|
-
result.warnings.push(`Tenant URL ${tenantUrl} returned status ${res.statusCode}`);
|
|
127
|
-
result.isValid = true; // Consider this a warning, not an error
|
|
128
|
-
}
|
|
129
|
-
resolve(result);
|
|
130
|
-
})
|
|
131
|
-
.on("error", (error) => {
|
|
132
|
-
if (!result.warnings)
|
|
133
|
-
result.warnings = [];
|
|
134
|
-
result.warnings.push(`Could not verify tenant URL ${tenantUrl}: ${error.message}`);
|
|
135
|
-
result.isValid = true; // Allow proceeding with warning
|
|
136
|
-
resolve(result);
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Validates Benchling OAuth credentials
|
|
142
|
-
*/
|
|
143
|
-
async function validateBenchlingCredentials(tenant, clientId, clientSecret) {
|
|
144
|
-
const result = {
|
|
145
|
-
isValid: false,
|
|
146
|
-
errors: [],
|
|
147
|
-
warnings: [],
|
|
148
|
-
};
|
|
149
|
-
if (!clientId || clientId.trim().length === 0) {
|
|
150
|
-
result.errors.push("Client ID cannot be empty");
|
|
151
|
-
}
|
|
152
|
-
if (!clientSecret || clientSecret.trim().length === 0) {
|
|
153
|
-
result.errors.push("Client secret cannot be empty");
|
|
154
|
-
}
|
|
155
|
-
if (result.errors.length > 0) {
|
|
156
|
-
return result;
|
|
157
|
-
}
|
|
158
|
-
// Test OAuth token endpoint
|
|
159
|
-
const tokenUrl = `https://${tenant}.benchling.com/api/v2/token`;
|
|
160
|
-
const authString = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
|
|
161
|
-
console.log(` Testing OAuth credentials: ${tokenUrl} (Client ID: ${clientId.substring(0, 8)}...)`);
|
|
162
|
-
return new Promise((resolve) => {
|
|
163
|
-
const postData = "grant_type=client_credentials";
|
|
164
|
-
const options = {
|
|
165
|
-
method: "POST",
|
|
166
|
-
headers: {
|
|
167
|
-
"Authorization": `Basic ${authString}`,
|
|
168
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
169
|
-
"Content-Length": postData.length,
|
|
170
|
-
},
|
|
171
|
-
timeout: 10000,
|
|
172
|
-
};
|
|
173
|
-
const req = https.request(tokenUrl, options, (res) => {
|
|
174
|
-
let data = "";
|
|
175
|
-
res.on("data", (chunk) => {
|
|
176
|
-
data += chunk;
|
|
177
|
-
});
|
|
178
|
-
res.on("end", () => {
|
|
179
|
-
if (res.statusCode === 200) {
|
|
180
|
-
result.isValid = true;
|
|
181
|
-
console.log(" ✓ OAuth credentials validated successfully");
|
|
182
|
-
}
|
|
183
|
-
else {
|
|
184
|
-
let errorDetail = data.substring(0, 200);
|
|
185
|
-
try {
|
|
186
|
-
const parsed = JSON.parse(data);
|
|
187
|
-
if (parsed.error_description) {
|
|
188
|
-
errorDetail = parsed.error_description;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
catch {
|
|
192
|
-
// Keep the raw data if not JSON
|
|
193
|
-
}
|
|
194
|
-
result.errors.push(`OAuth validation failed for tenant '${tenant}':\n` +
|
|
195
|
-
` Tested: POST ${tokenUrl}\n` +
|
|
196
|
-
` Status: ${res.statusCode}\n` +
|
|
197
|
-
` Error: ${errorDetail}\n` +
|
|
198
|
-
" Hint: Verify Client ID and Secret are correct and match the app definition");
|
|
199
|
-
}
|
|
200
|
-
resolve(result);
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
req.on("error", (error) => {
|
|
204
|
-
if (!result.warnings)
|
|
205
|
-
result.warnings = [];
|
|
206
|
-
result.warnings.push(`Could not validate OAuth credentials at ${tokenUrl}: ${error.message}\n` +
|
|
207
|
-
" This may be a network issue. Credentials will be validated during deployment.");
|
|
208
|
-
result.isValid = true; // Allow proceeding with warning
|
|
209
|
-
resolve(result);
|
|
210
|
-
});
|
|
211
|
-
req.write(postData);
|
|
212
|
-
req.end();
|
|
213
|
-
});
|
|
214
|
-
}
|
|
46
|
+
const STEP_TITLES = {
|
|
47
|
+
catalogDiscovery: "Quilt Catalog Discovery",
|
|
48
|
+
stackQuery: "Quilt Stack Configuration",
|
|
49
|
+
parameterCollection: "Configuration Parameters",
|
|
50
|
+
validation: "Validation",
|
|
51
|
+
modeDecision: "Deployment Mode",
|
|
52
|
+
integratedMode: "Integrated Setup",
|
|
53
|
+
standaloneMode: "Standalone Setup",
|
|
54
|
+
};
|
|
215
55
|
/**
|
|
216
|
-
*
|
|
56
|
+
* Gets the package version from package.json
|
|
217
57
|
*/
|
|
218
|
-
|
|
219
|
-
const result = {
|
|
220
|
-
isValid: false,
|
|
221
|
-
errors: [],
|
|
222
|
-
warnings: [],
|
|
223
|
-
};
|
|
224
|
-
if (!bucketName || bucketName.trim().length === 0) {
|
|
225
|
-
result.errors.push("Bucket name cannot be empty");
|
|
226
|
-
return result;
|
|
227
|
-
}
|
|
228
|
-
// First, try to detect the bucket's actual region
|
|
229
|
-
console.log(` Detecting region for bucket: ${bucketName}`);
|
|
230
|
-
const actualRegion = await detectBucketRegion(bucketName, awsProfile);
|
|
231
|
-
let regionToUse = region;
|
|
232
|
-
if (actualRegion && actualRegion !== region) {
|
|
233
|
-
console.log(` ⚠ Bucket is in ${actualRegion}, not ${region} - using detected region`);
|
|
234
|
-
regionToUse = actualRegion;
|
|
235
|
-
}
|
|
236
|
-
else if (actualRegion) {
|
|
237
|
-
console.log(` ✓ Bucket region confirmed: ${actualRegion}`);
|
|
238
|
-
}
|
|
58
|
+
function getVersion() {
|
|
239
59
|
try {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
clientConfig.credentials = fromIni({ profile: awsProfile });
|
|
244
|
-
}
|
|
245
|
-
const s3Client = new client_s3_1.S3Client(clientConfig);
|
|
246
|
-
// Test HeadBucket (verify bucket exists and we have access)
|
|
247
|
-
console.log(` Testing S3 bucket access: ${bucketName} (region: ${regionToUse}${awsProfile ? `, profile: ${awsProfile}` : ""})`);
|
|
248
|
-
const headCommand = new client_s3_1.HeadBucketCommand({ Bucket: bucketName });
|
|
249
|
-
await s3Client.send(headCommand);
|
|
250
|
-
console.log(` ✓ S3 bucket accessible: ${bucketName}`);
|
|
251
|
-
// Test ListObjects (verify we can list objects)
|
|
252
|
-
const listCommand = new client_s3_1.ListObjectsV2Command({
|
|
253
|
-
Bucket: bucketName,
|
|
254
|
-
MaxKeys: 1,
|
|
255
|
-
});
|
|
256
|
-
await s3Client.send(listCommand);
|
|
257
|
-
console.log(" ✓ S3 bucket list permission confirmed");
|
|
258
|
-
result.isValid = true;
|
|
60
|
+
const packagePath = (0, path_1.join)(__dirname, "../../package.json");
|
|
61
|
+
const packageJson = JSON.parse((0, fs_1.readFileSync)(packagePath, "utf-8"));
|
|
62
|
+
return packageJson.version;
|
|
259
63
|
}
|
|
260
|
-
catch
|
|
261
|
-
|
|
262
|
-
const errorCode = err.Code || err.name || "UnknownError";
|
|
263
|
-
const errorMsg = err.message || "Unknown error occurred";
|
|
264
|
-
const statusCode = err.$metadata?.httpStatusCode;
|
|
265
|
-
// Provide specific guidance based on error type
|
|
266
|
-
let hint = "Verify bucket exists, region is correct, and you have s3:GetBucketLocation and s3:ListBucket permissions";
|
|
267
|
-
if (errorCode === "NoSuchBucket" || errorMsg.includes("does not exist")) {
|
|
268
|
-
hint = "The bucket does not exist. Verify the bucket name is correct.";
|
|
269
|
-
}
|
|
270
|
-
else if (errorCode === "AccessDenied" || errorCode === "403" || statusCode === 403) {
|
|
271
|
-
hint = "Access denied. Verify your AWS credentials have s3:GetBucketLocation and s3:ListBucket permissions for this bucket.";
|
|
272
|
-
}
|
|
273
|
-
else if (errorCode === "PermanentRedirect" || errorCode === "301" || statusCode === 301) {
|
|
274
|
-
hint = `The bucket exists but is in a different region. Try specifying the correct region for bucket '${bucketName}'.`;
|
|
275
|
-
}
|
|
276
|
-
result.errors.push(`S3 bucket validation failed for '${bucketName}' in region '${regionToUse}'${awsProfile ? ` (AWS profile: ${awsProfile})` : ""}:\n` +
|
|
277
|
-
` Error: ${errorCode}${statusCode ? ` (HTTP ${statusCode})` : ""}\n` +
|
|
278
|
-
` Message: ${errorMsg}\n` +
|
|
279
|
-
" Tested: HeadBucket operation\n" +
|
|
280
|
-
` Hint: ${hint}`);
|
|
64
|
+
catch {
|
|
65
|
+
return "unknown";
|
|
281
66
|
}
|
|
282
|
-
return result;
|
|
283
67
|
}
|
|
284
68
|
/**
|
|
285
|
-
*
|
|
69
|
+
* Prints the wizard welcome banner
|
|
286
70
|
*/
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const tenantValidation = await validateBenchlingTenant(config.benchling.tenant);
|
|
298
|
-
if (!tenantValidation.isValid) {
|
|
299
|
-
result.isValid = false;
|
|
300
|
-
result.errors.push(...tenantValidation.errors);
|
|
301
|
-
}
|
|
302
|
-
if (tenantValidation.warnings && tenantValidation.warnings.length > 0) {
|
|
303
|
-
if (!result.warnings)
|
|
304
|
-
result.warnings = [];
|
|
305
|
-
result.warnings.push(...tenantValidation.warnings);
|
|
306
|
-
}
|
|
307
|
-
// Validate OAuth credentials (if secret is provided)
|
|
308
|
-
if (config.benchling.clientSecret) {
|
|
309
|
-
const credValidation = await validateBenchlingCredentials(config.benchling.tenant, config.benchling.clientId, config.benchling.clientSecret);
|
|
310
|
-
if (!credValidation.isValid) {
|
|
311
|
-
result.isValid = false;
|
|
312
|
-
result.errors.push(...credValidation.errors);
|
|
313
|
-
}
|
|
314
|
-
if (credValidation.warnings && credValidation.warnings.length > 0) {
|
|
315
|
-
if (!result.warnings)
|
|
316
|
-
result.warnings = [];
|
|
317
|
-
result.warnings.push(...credValidation.warnings);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
// Validate S3 bucket access
|
|
321
|
-
const bucketValidation = await validateS3BucketAccess(config.packages.bucket, config.deployment.region, options.awsProfile);
|
|
322
|
-
if (!bucketValidation.isValid) {
|
|
323
|
-
result.isValid = false;
|
|
324
|
-
result.errors.push(...bucketValidation.errors);
|
|
325
|
-
}
|
|
326
|
-
if (bucketValidation.warnings && bucketValidation.warnings.length > 0) {
|
|
327
|
-
if (!result.warnings)
|
|
328
|
-
result.warnings = [];
|
|
329
|
-
result.warnings.push(...bucketValidation.warnings);
|
|
330
|
-
}
|
|
331
|
-
return result;
|
|
71
|
+
function printWelcomeBanner() {
|
|
72
|
+
const version = getVersion();
|
|
73
|
+
const prefix = " Benchling Webhook Setup (v";
|
|
74
|
+
const suffix = ")";
|
|
75
|
+
const totalWidth = 63; // Width between the ║ symbols
|
|
76
|
+
const contentLength = prefix.length + version.length + suffix.length;
|
|
77
|
+
const padding = " ".repeat(totalWidth - contentLength);
|
|
78
|
+
console.log("\n╔═══════════════════════════════════════════════════════════╗");
|
|
79
|
+
console.log(`║${prefix}${version}${suffix}${padding}║`);
|
|
80
|
+
console.log("╚═══════════════════════════════════════════════════════════╝");
|
|
332
81
|
}
|
|
333
82
|
/**
|
|
334
|
-
*
|
|
83
|
+
* Prints a step header with proper numbering
|
|
84
|
+
* @param stepNumber - The current step number
|
|
85
|
+
* @param title - The step title
|
|
335
86
|
*/
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
console.log("╔═══════════════════════════════════════════════════════════╗");
|
|
339
|
-
console.log("║ Benchling Webhook Configuration Wizard ║");
|
|
340
|
-
console.log("╚═══════════════════════════════════════════════════════════╝\n");
|
|
341
|
-
if (inheritFrom) {
|
|
342
|
-
console.log(`Creating profile inheriting from: ${inheritFrom}\n`);
|
|
343
|
-
}
|
|
344
|
-
const config = { ...existingConfig };
|
|
345
|
-
let awsAccountId;
|
|
346
|
-
// If non-interactive, validate that all required fields are present
|
|
347
|
-
if (yes) {
|
|
348
|
-
if (!config.benchling?.tenant || !config.benchling?.clientId || !config.benchling?.clientSecret) {
|
|
349
|
-
throw new Error("Non-interactive mode requires benchlingTenant, benchlingClientId, and benchlingClientSecret to be already configured");
|
|
350
|
-
}
|
|
351
|
-
// Add metadata and inheritance marker before returning
|
|
352
|
-
const now = new Date().toISOString();
|
|
353
|
-
const finalConfig = config;
|
|
354
|
-
finalConfig._metadata = {
|
|
355
|
-
version: "0.7.0",
|
|
356
|
-
createdAt: config._metadata?.createdAt || now,
|
|
357
|
-
updatedAt: now,
|
|
358
|
-
source: "wizard",
|
|
359
|
-
};
|
|
360
|
-
if (inheritFrom) {
|
|
361
|
-
finalConfig._inherits = inheritFrom;
|
|
362
|
-
}
|
|
363
|
-
return finalConfig;
|
|
364
|
-
}
|
|
365
|
-
// Always prompt for Quilt configuration (use existing/inferred values as defaults)
|
|
366
|
-
console.log("Step 1: Quilt Configuration\n");
|
|
367
|
-
const quiltAnswers = await inquirer_1.default.prompt([
|
|
368
|
-
{
|
|
369
|
-
type: "input",
|
|
370
|
-
name: "stackArn",
|
|
371
|
-
message: "Quilt Stack ARN:",
|
|
372
|
-
default: config.quilt?.stackArn,
|
|
373
|
-
validate: (input) => input.trim().length > 0 && input.startsWith("arn:aws:cloudformation:") ||
|
|
374
|
-
"Stack ARN is required and must start with arn:aws:cloudformation:",
|
|
375
|
-
},
|
|
376
|
-
{
|
|
377
|
-
type: "input",
|
|
378
|
-
name: "catalog",
|
|
379
|
-
message: "Quilt Catalog URL (domain or full URL):",
|
|
380
|
-
default: config.quilt?.catalog,
|
|
381
|
-
validate: (input) => {
|
|
382
|
-
const trimmed = input.trim();
|
|
383
|
-
if (trimmed.length === 0) {
|
|
384
|
-
return "Catalog URL is required";
|
|
385
|
-
}
|
|
386
|
-
return true;
|
|
387
|
-
},
|
|
388
|
-
filter: (input) => {
|
|
389
|
-
// Strip protocol if present, store only domain
|
|
390
|
-
return input.trim().replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
391
|
-
},
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
type: "input",
|
|
395
|
-
name: "database",
|
|
396
|
-
message: "Quilt Athena Database:",
|
|
397
|
-
// NOTE: "quilt_catalog" is a prompt default ONLY, NOT an OPTIONAL preset
|
|
398
|
-
// This field is REQUIRED - users must provide a value or it must be inferred from Quilt stack
|
|
399
|
-
// With --yes flag, this prompt should NOT be skipped even though it has a default here
|
|
400
|
-
default: config.quilt?.database || "quilt_catalog",
|
|
401
|
-
validate: (input) => input.trim().length > 0 || "Database name is required",
|
|
402
|
-
},
|
|
403
|
-
{
|
|
404
|
-
type: "input",
|
|
405
|
-
name: "queueUrl",
|
|
406
|
-
message: "SQS Queue URL:",
|
|
407
|
-
default: config.quilt?.queueUrl,
|
|
408
|
-
validate: (input) => {
|
|
409
|
-
return (0, sqs_1.isQueueUrl)(input) ||
|
|
410
|
-
"Queue URL is required and must look like https://sqs.<region>.amazonaws.com/<account>/<queue>";
|
|
411
|
-
},
|
|
412
|
-
},
|
|
413
|
-
]);
|
|
414
|
-
// Extract region and account ID from stack ARN
|
|
415
|
-
// ARN format: arn:aws:cloudformation:REGION:ACCOUNT_ID:stack/STACK_NAME/STACK_ID
|
|
416
|
-
const arnMatch = quiltAnswers.stackArn.match(/^arn:aws:cloudformation:([^:]+):(\d{12}):/);
|
|
417
|
-
const quiltRegion = arnMatch ? arnMatch[1] : "us-east-1";
|
|
418
|
-
awsAccountId = arnMatch ? arnMatch[2] : undefined;
|
|
419
|
-
config.quilt = {
|
|
420
|
-
stackArn: quiltAnswers.stackArn,
|
|
421
|
-
catalog: quiltAnswers.catalog,
|
|
422
|
-
database: quiltAnswers.database,
|
|
423
|
-
queueUrl: quiltAnswers.queueUrl,
|
|
424
|
-
region: quiltRegion,
|
|
425
|
-
};
|
|
426
|
-
// Prompt for Benchling configuration
|
|
427
|
-
console.log("\nStep 2: Benchling Configuration\n");
|
|
428
|
-
// First, get tenant
|
|
429
|
-
const tenantAnswer = await inquirer_1.default.prompt([
|
|
430
|
-
{
|
|
431
|
-
type: "input",
|
|
432
|
-
name: "tenant",
|
|
433
|
-
message: "Benchling Tenant:",
|
|
434
|
-
default: config.benchling?.tenant,
|
|
435
|
-
validate: (input) => input.trim().length > 0 || "Tenant is required",
|
|
436
|
-
},
|
|
437
|
-
]);
|
|
438
|
-
// Ask if they have an app_definition_id BEFORE asking for credentials
|
|
439
|
-
const hasAppDefId = await inquirer_1.default.prompt([
|
|
440
|
-
{
|
|
441
|
-
type: "confirm",
|
|
442
|
-
name: "hasIt",
|
|
443
|
-
message: "Do you have a Benchling App Definition ID for this app?",
|
|
444
|
-
default: !!config.benchling?.appDefinitionId,
|
|
445
|
-
},
|
|
446
|
-
]);
|
|
447
|
-
let appDefinitionId;
|
|
448
|
-
if (hasAppDefId.hasIt) {
|
|
449
|
-
// They have it, ask for it
|
|
450
|
-
const appDefAnswer = await inquirer_1.default.prompt([
|
|
451
|
-
{
|
|
452
|
-
type: "input",
|
|
453
|
-
name: "appDefinitionId",
|
|
454
|
-
message: "Benchling App Definition ID:",
|
|
455
|
-
default: config.benchling?.appDefinitionId,
|
|
456
|
-
validate: (input) => input.trim().length > 0 || "App definition ID is required",
|
|
457
|
-
},
|
|
458
|
-
]);
|
|
459
|
-
appDefinitionId = appDefAnswer.appDefinitionId;
|
|
460
|
-
}
|
|
461
|
-
else {
|
|
462
|
-
// They don't have it, create the manifest and show instructions
|
|
463
|
-
console.log("\n" + chalk_1.default.blue("Creating app manifest...") + "\n");
|
|
464
|
-
// Create manifest using the existing command
|
|
465
|
-
await (0, manifest_1.manifestCommand)({
|
|
466
|
-
catalog: config.quilt?.catalog,
|
|
467
|
-
output: "app-manifest.yaml",
|
|
468
|
-
});
|
|
469
|
-
console.log("\n" + chalk_1.default.yellow("After you have installed the app in Benchling and have the App Definition ID, you can continue.") + "\n");
|
|
470
|
-
// Now ask for the app definition ID
|
|
471
|
-
const appDefAnswer = await inquirer_1.default.prompt([
|
|
472
|
-
{
|
|
473
|
-
type: "input",
|
|
474
|
-
name: "appDefinitionId",
|
|
475
|
-
message: "Benchling App Definition ID:",
|
|
476
|
-
validate: (input) => input.trim().length > 0 || "App definition ID is required",
|
|
477
|
-
},
|
|
478
|
-
]);
|
|
479
|
-
appDefinitionId = appDefAnswer.appDefinitionId;
|
|
480
|
-
}
|
|
481
|
-
// Now ask for OAuth credentials (which must come from the app)
|
|
482
|
-
const credentialAnswers = await inquirer_1.default.prompt([
|
|
483
|
-
{
|
|
484
|
-
type: "input",
|
|
485
|
-
name: "clientId",
|
|
486
|
-
message: "Benchling OAuth Client ID (from the app above):",
|
|
487
|
-
default: config.benchling?.clientId,
|
|
488
|
-
validate: (input) => input.trim().length > 0 || "Client ID is required",
|
|
489
|
-
},
|
|
490
|
-
{
|
|
491
|
-
type: "password",
|
|
492
|
-
name: "clientSecret",
|
|
493
|
-
message: config.benchling?.clientSecret
|
|
494
|
-
? "Benchling OAuth Client Secret (press Enter to keep existing):"
|
|
495
|
-
: "Benchling OAuth Client Secret (from the app above):",
|
|
496
|
-
validate: (input) => {
|
|
497
|
-
// If there's an existing secret and input is empty, we'll keep the existing one
|
|
498
|
-
if (config.benchling?.clientSecret && input.trim().length === 0) {
|
|
499
|
-
return true;
|
|
500
|
-
}
|
|
501
|
-
return input.trim().length > 0 || "Client secret is required";
|
|
502
|
-
},
|
|
503
|
-
},
|
|
504
|
-
]);
|
|
505
|
-
// Ask for optional test entry ID
|
|
506
|
-
const testEntryAnswer = await inquirer_1.default.prompt([
|
|
507
|
-
{
|
|
508
|
-
type: "input",
|
|
509
|
-
name: "testEntryId",
|
|
510
|
-
message: "Benchling Test Entry ID (optional):",
|
|
511
|
-
default: config.benchling?.testEntryId || "",
|
|
512
|
-
},
|
|
513
|
-
]);
|
|
514
|
-
// Handle empty password input - keep existing secret if user pressed Enter
|
|
515
|
-
if (credentialAnswers.clientSecret.trim().length === 0 && config.benchling?.clientSecret) {
|
|
516
|
-
credentialAnswers.clientSecret = config.benchling.clientSecret;
|
|
517
|
-
}
|
|
518
|
-
config.benchling = {
|
|
519
|
-
tenant: tenantAnswer.tenant,
|
|
520
|
-
clientId: credentialAnswers.clientId,
|
|
521
|
-
clientSecret: credentialAnswers.clientSecret,
|
|
522
|
-
appDefinitionId: appDefinitionId,
|
|
523
|
-
};
|
|
524
|
-
if (testEntryAnswer.testEntryId && testEntryAnswer.testEntryId.trim() !== "") {
|
|
525
|
-
config.benchling.testEntryId = testEntryAnswer.testEntryId;
|
|
526
|
-
}
|
|
527
|
-
// Prompt for package configuration
|
|
528
|
-
console.log("\nStep 3: Package Configuration\n");
|
|
529
|
-
const packageAnswers = await inquirer_1.default.prompt([
|
|
530
|
-
{
|
|
531
|
-
type: "input",
|
|
532
|
-
name: "bucket",
|
|
533
|
-
message: "Package S3 Bucket:",
|
|
534
|
-
default: config.packages?.bucket,
|
|
535
|
-
validate: (input) => input.trim().length > 0 || "Bucket name is required",
|
|
536
|
-
},
|
|
537
|
-
{
|
|
538
|
-
type: "input",
|
|
539
|
-
name: "prefix",
|
|
540
|
-
message: "Package S3 prefix:",
|
|
541
|
-
// NOTE: "benchling" is an OPTIONAL preset - can be auto-applied with --yes flag
|
|
542
|
-
default: config.packages?.prefix || "benchling",
|
|
543
|
-
},
|
|
544
|
-
{
|
|
545
|
-
type: "input",
|
|
546
|
-
name: "metadataKey",
|
|
547
|
-
message: "Package metadata key:",
|
|
548
|
-
// NOTE: "experiment_id" is an OPTIONAL preset - can be auto-applied with --yes flag
|
|
549
|
-
default: config.packages?.metadataKey || "experiment_id",
|
|
550
|
-
},
|
|
551
|
-
]);
|
|
552
|
-
config.packages = {
|
|
553
|
-
bucket: packageAnswers.bucket,
|
|
554
|
-
prefix: packageAnswers.prefix,
|
|
555
|
-
metadataKey: packageAnswers.metadataKey,
|
|
556
|
-
};
|
|
557
|
-
// Prompt for deployment configuration
|
|
558
|
-
console.log("\nStep 4: Deployment Configuration\n");
|
|
559
|
-
const deploymentAnswers = await inquirer_1.default.prompt([
|
|
560
|
-
{
|
|
561
|
-
type: "input",
|
|
562
|
-
name: "region",
|
|
563
|
-
message: "AWS Deployment Region:",
|
|
564
|
-
// Prefer inferred region from Quilt stack, then existing deployment config, then fallback
|
|
565
|
-
default: config.quilt?.region || config.deployment?.region || "us-east-1",
|
|
566
|
-
},
|
|
567
|
-
{
|
|
568
|
-
type: "input",
|
|
569
|
-
name: "account",
|
|
570
|
-
message: "AWS Account ID:",
|
|
571
|
-
default: config.deployment?.account || awsAccountId || config.quilt?.stackArn?.match(/:(\d{12}):/)?.[1],
|
|
572
|
-
validate: (input) => {
|
|
573
|
-
if (!input || input.trim().length === 0) {
|
|
574
|
-
return "AWS Account ID is required";
|
|
575
|
-
}
|
|
576
|
-
if (!/^\d{12}$/.test(input.trim())) {
|
|
577
|
-
return "AWS Account ID must be a 12-digit number";
|
|
578
|
-
}
|
|
579
|
-
return true;
|
|
580
|
-
},
|
|
581
|
-
},
|
|
582
|
-
]);
|
|
583
|
-
config.deployment = {
|
|
584
|
-
region: deploymentAnswers.region,
|
|
585
|
-
account: deploymentAnswers.account,
|
|
586
|
-
};
|
|
587
|
-
// Optional: Logging configuration
|
|
588
|
-
console.log("\nStep 5: Optional Configuration\n");
|
|
589
|
-
const optionalAnswers = await inquirer_1.default.prompt([
|
|
590
|
-
{
|
|
591
|
-
type: "list",
|
|
592
|
-
name: "logLevel",
|
|
593
|
-
message: "Log level:",
|
|
594
|
-
choices: ["DEBUG", "INFO", "WARNING", "ERROR"],
|
|
595
|
-
default: config.logging?.level || "INFO",
|
|
596
|
-
},
|
|
597
|
-
{
|
|
598
|
-
type: "input",
|
|
599
|
-
name: "webhookAllowList",
|
|
600
|
-
message: "Webhook IP allowlist (comma-separated, empty for none):",
|
|
601
|
-
default: config.security?.webhookAllowList || "",
|
|
602
|
-
},
|
|
603
|
-
]);
|
|
604
|
-
config.logging = {
|
|
605
|
-
level: optionalAnswers.logLevel,
|
|
606
|
-
};
|
|
607
|
-
config.security = {
|
|
608
|
-
enableVerification: true,
|
|
609
|
-
webhookAllowList: optionalAnswers.webhookAllowList,
|
|
610
|
-
};
|
|
611
|
-
// Add metadata
|
|
612
|
-
const now = new Date().toISOString();
|
|
613
|
-
config._metadata = {
|
|
614
|
-
version: "0.7.0",
|
|
615
|
-
createdAt: config._metadata?.createdAt || now,
|
|
616
|
-
updatedAt: now,
|
|
617
|
-
source: "wizard",
|
|
618
|
-
};
|
|
619
|
-
// Add inheritance marker if specified
|
|
620
|
-
if (inheritFrom) {
|
|
621
|
-
config._inherits = inheritFrom;
|
|
622
|
-
}
|
|
623
|
-
return config;
|
|
87
|
+
function printStepHeader(stepNumber, title) {
|
|
88
|
+
console.log(chalk_1.default.bold(`\nStep ${stepNumber}: ${title}\n`));
|
|
624
89
|
}
|
|
625
90
|
/**
|
|
626
|
-
* Main
|
|
91
|
+
* Main setup wizard orchestrator
|
|
92
|
+
*
|
|
93
|
+
* This function orchestrates the 7 phases of the setup wizard in sequence.
|
|
94
|
+
* Each phase is isolated and testable. The flow is enforced by the code
|
|
95
|
+
* structure - integrated mode explicitly returns, preventing fall-through
|
|
96
|
+
* to deployment.
|
|
627
97
|
*
|
|
628
|
-
*
|
|
629
|
-
* 1.
|
|
630
|
-
* 2.
|
|
631
|
-
* 3.
|
|
632
|
-
* 4.
|
|
633
|
-
* 5.
|
|
634
|
-
*
|
|
98
|
+
* Flow:
|
|
99
|
+
* 1. Phase 1: Catalog Discovery (local config only, no AWS)
|
|
100
|
+
* 2. Phase 2: Stack Query (query CloudFormation for confirmed catalog)
|
|
101
|
+
* 3. Phase 3: Parameter Collection (collect user inputs)
|
|
102
|
+
* 4. Phase 4: Validation (validate all parameters)
|
|
103
|
+
* 5. Phase 5: Mode Decision (choose integrated vs standalone)
|
|
104
|
+
* 6a. Phase 6: Integrated Mode (update secret, EXIT) OR
|
|
105
|
+
* 6b. Phase 7: Standalone Mode (create secret, optionally deploy, EXIT)
|
|
106
|
+
*
|
|
107
|
+
* The orchestrator prints step headers before each phase to ensure
|
|
108
|
+
* consistent numbering regardless of execution path.
|
|
109
|
+
*
|
|
110
|
+
* @param options - Setup wizard options
|
|
111
|
+
* @returns Setup wizard result
|
|
635
112
|
*/
|
|
636
|
-
async function
|
|
637
|
-
const { profile = "default",
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
console.log("╚═══════════════════════════════════════════════════════════╝\n");
|
|
644
|
-
// Step 1: Load existing configuration (if profile exists) - for suggestions only
|
|
645
|
-
let existingConfig;
|
|
646
|
-
if (xdg.profileExists(profile)) {
|
|
647
|
-
console.log(`Loading existing configuration for profile: ${profile}\n`);
|
|
648
|
-
try {
|
|
649
|
-
existingConfig = xdg.readProfile(profile);
|
|
650
|
-
}
|
|
651
|
-
catch (error) {
|
|
652
|
-
console.warn(`Warning: Could not load existing config: ${error.message}`);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
else if (inheritFrom) {
|
|
656
|
-
// Only use explicit inheritFrom if specified (for suggestions)
|
|
657
|
-
console.log(`Creating new profile '${profile}' with suggestions from '${inheritFrom}'\n`);
|
|
658
|
-
try {
|
|
659
|
-
existingConfig = xdg.readProfile(inheritFrom);
|
|
660
|
-
}
|
|
661
|
-
catch (error) {
|
|
662
|
-
throw new Error(`Base profile '${inheritFrom}' not found: ${error.message}`);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
// Step 2: Always infer Quilt configuration from AWS (provides suggestions)
|
|
666
|
-
let quiltConfig = existingConfig?.quilt || {};
|
|
667
|
-
let inferredAccountId;
|
|
668
|
-
console.log("Step 1: Inferring Quilt configuration from AWS...\n");
|
|
113
|
+
async function runSetupWizard(options = {}) {
|
|
114
|
+
const { profile = "default", yes = false, skipValidation = false, awsProfile, awsRegion, setupOnly = false, configStorage, } = options;
|
|
115
|
+
const xdg = configStorage || new xdg_config_1.XDGConfig();
|
|
116
|
+
printWelcomeBanner();
|
|
117
|
+
// Load existing configuration if it exists
|
|
118
|
+
let existingConfig = null;
|
|
119
|
+
let inheritFrom = null;
|
|
669
120
|
try {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
profile: awsProfile,
|
|
673
|
-
interactive: !yes,
|
|
674
|
-
});
|
|
675
|
-
// Merge inferred config with existing (inferred takes precedence as fresher data)
|
|
676
|
-
quiltConfig = {
|
|
677
|
-
...quiltConfig,
|
|
678
|
-
...inferenceResult,
|
|
679
|
-
};
|
|
680
|
-
inferredAccountId = inferenceResult.account;
|
|
681
|
-
console.log("✓ Quilt configuration inferred\n");
|
|
121
|
+
existingConfig = xdg.readProfile(profile);
|
|
122
|
+
console.log(chalk_1.default.dim(`\nLoading existing configuration for profile: ${profile}\n`));
|
|
682
123
|
}
|
|
683
124
|
catch (error) {
|
|
684
|
-
|
|
685
|
-
if (yes) {
|
|
686
|
-
|
|
125
|
+
// Profile doesn't exist - offer to copy from default
|
|
126
|
+
if (profile !== "default" && !yes) {
|
|
127
|
+
try {
|
|
128
|
+
const defaultConfig = xdg.readProfile("default");
|
|
129
|
+
const { copy } = await inquirer_1.default.prompt([
|
|
130
|
+
{
|
|
131
|
+
type: "confirm",
|
|
132
|
+
name: "copy",
|
|
133
|
+
message: `Profile '${profile}' doesn't exist. Copy configuration from 'default'?`,
|
|
134
|
+
default: true,
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
if (copy) {
|
|
138
|
+
existingConfig = defaultConfig;
|
|
139
|
+
inheritFrom = "default";
|
|
140
|
+
console.log(chalk_1.default.dim(`\nCopying configuration from profile: default\n`));
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
console.log(chalk_1.default.dim(`\nCreating new configuration for profile: ${profile}\n`));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// No default profile either - fresh setup
|
|
148
|
+
console.log(chalk_1.default.dim(`\nCreating new configuration for profile: ${profile}\n`));
|
|
149
|
+
}
|
|
687
150
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
name: "continueManually",
|
|
692
|
-
message: "Continue and enter Quilt configuration manually?",
|
|
693
|
-
default: true,
|
|
694
|
-
},
|
|
695
|
-
]);
|
|
696
|
-
if (!continueManually) {
|
|
697
|
-
throw new Error("Setup aborted by user");
|
|
151
|
+
else {
|
|
152
|
+
// Creating default profile or in --yes mode
|
|
153
|
+
console.log(chalk_1.default.dim(`\nCreating new configuration for profile: ${profile}\n`));
|
|
698
154
|
}
|
|
699
155
|
}
|
|
700
|
-
//
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
156
|
+
// =========================================================================
|
|
157
|
+
// PHASE 1: CATALOG DISCOVERY
|
|
158
|
+
// =========================================================================
|
|
159
|
+
// Detects and confirms catalog DNS (local config only, NO AWS queries)
|
|
160
|
+
// Priority: CLI arg > existing config > quilt3 detection > manual entry
|
|
161
|
+
printStepHeader(1, STEP_TITLES.catalogDiscovery);
|
|
162
|
+
const catalogResult = await (0, phase1_catalog_discovery_1.runCatalogDiscovery)({
|
|
163
|
+
yes,
|
|
164
|
+
catalogUrl: options.catalogUrl,
|
|
165
|
+
existingCatalog: existingConfig?.quilt?.catalog,
|
|
166
|
+
});
|
|
167
|
+
// =========================================================================
|
|
168
|
+
// PHASE 2: STACK QUERY
|
|
169
|
+
// =========================================================================
|
|
170
|
+
// NOW query AWS for the CONFIRMED catalog
|
|
171
|
+
// This extracts ALL parameters including BenchlingSecret ARN
|
|
172
|
+
printStepHeader(2, STEP_TITLES.stackQuery);
|
|
173
|
+
const stackQuery = await (0, phase2_stack_query_1.runStackQuery)(catalogResult.catalogDns, {
|
|
174
|
+
awsProfile,
|
|
175
|
+
awsRegion,
|
|
176
|
+
yes,
|
|
177
|
+
});
|
|
178
|
+
// Handle stack query failure
|
|
179
|
+
if (!stackQuery.stackQuerySucceeded) {
|
|
180
|
+
console.error(chalk_1.default.red("\n❌ Stack query failed. Cannot continue setup."));
|
|
181
|
+
console.error(chalk_1.default.yellow("Please verify:"));
|
|
182
|
+
console.error(chalk_1.default.yellow(" 1. The catalog DNS is correct"));
|
|
183
|
+
console.error(chalk_1.default.yellow(" 2. You have AWS credentials configured"));
|
|
184
|
+
console.error(chalk_1.default.yellow(" 3. The CloudFormation stack exists for this catalog\n"));
|
|
185
|
+
throw new Error("Stack query failed");
|
|
186
|
+
}
|
|
187
|
+
// =========================================================================
|
|
188
|
+
// PHASE 3: PARAMETER COLLECTION
|
|
189
|
+
// =========================================================================
|
|
190
|
+
// Collect user inputs, using existing config and stack query results as defaults
|
|
191
|
+
printStepHeader(3, STEP_TITLES.parameterCollection);
|
|
192
|
+
const parameters = await (0, phase3_parameter_collection_1.runParameterCollection)({
|
|
193
|
+
stackQuery,
|
|
194
|
+
existingConfig,
|
|
713
195
|
yes,
|
|
714
|
-
|
|
196
|
+
benchlingTenant: options.benchlingTenant,
|
|
197
|
+
benchlingClientId: options.benchlingClientId,
|
|
198
|
+
benchlingClientSecret: options.benchlingClientSecret,
|
|
199
|
+
benchlingAppDefinitionId: options.benchlingAppDefinitionId,
|
|
200
|
+
benchlingTestEntryId: options.benchlingTestEntryId,
|
|
201
|
+
userBucket: options.userBucket,
|
|
202
|
+
pkgPrefix: options.pkgPrefix,
|
|
203
|
+
pkgKey: options.pkgKey,
|
|
204
|
+
logLevel: options.logLevel,
|
|
205
|
+
webhookAllowList: options.webhookAllowList,
|
|
715
206
|
});
|
|
716
|
-
//
|
|
207
|
+
// =========================================================================
|
|
208
|
+
// PHASE 4: VALIDATION
|
|
209
|
+
// =========================================================================
|
|
210
|
+
// Validate all collected parameters BEFORE making mode decision
|
|
717
211
|
if (!skipValidation) {
|
|
718
|
-
|
|
719
|
-
const validation = await
|
|
720
|
-
|
|
212
|
+
printStepHeader(4, STEP_TITLES.validation);
|
|
213
|
+
const validation = await (0, phase4_validation_1.runValidation)({
|
|
214
|
+
stackQuery,
|
|
215
|
+
parameters,
|
|
721
216
|
awsProfile,
|
|
722
217
|
});
|
|
723
|
-
if (!validation.
|
|
724
|
-
console.error("\n❌ Configuration validation failed:");
|
|
725
|
-
console.error(chalk_1.default.gray(" The following validations were performed:"));
|
|
726
|
-
console.error(chalk_1.default.gray(` - Benchling tenant: ${config.benchling.tenant}`));
|
|
727
|
-
console.error(chalk_1.default.gray(` - OAuth credentials: Client ID ${config.benchling.clientId.substring(0, 8)}...`));
|
|
728
|
-
console.error(chalk_1.default.gray(` - S3 bucket: ${config.packages.bucket} (region: ${config.deployment.region})`));
|
|
729
|
-
console.error("");
|
|
730
|
-
validation.errors.forEach((err) => console.error(`${err}\n`));
|
|
218
|
+
if (!validation.success) {
|
|
731
219
|
if (yes) {
|
|
732
|
-
throw new Error(
|
|
220
|
+
throw new Error(`Validation failed: ${validation.errors.join(", ")}`);
|
|
733
221
|
}
|
|
734
222
|
const { proceed } = await inquirer_1.default.prompt([
|
|
735
223
|
{
|
|
@@ -743,74 +231,83 @@ async function runInstallWizard(options = {}) {
|
|
|
743
231
|
throw new Error("Setup aborted by user");
|
|
744
232
|
}
|
|
745
233
|
}
|
|
746
|
-
else {
|
|
747
|
-
console.log("✓ Configuration validated successfully\n");
|
|
748
|
-
}
|
|
749
|
-
if (validation.warnings && validation.warnings.length > 0) {
|
|
750
|
-
console.warn("\n⚠ Warnings:");
|
|
751
|
-
validation.warnings.forEach((warn) => console.warn(` ${warn}\n`));
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
// Step 5: Save configuration
|
|
755
|
-
console.log(`Saving configuration to profile: ${profile}...\n`);
|
|
756
|
-
try {
|
|
757
|
-
xdg.writeProfile(profile, config);
|
|
758
|
-
console.log(`✓ Configuration saved to: ~/.config/benchling-webhook/${profile}/config.json\n`);
|
|
759
|
-
}
|
|
760
|
-
catch (error) {
|
|
761
|
-
throw new Error(`Failed to save configuration: ${error.message}`);
|
|
762
234
|
}
|
|
763
|
-
//
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
235
|
+
// =========================================================================
|
|
236
|
+
// PHASE 5: MODE DECISION
|
|
237
|
+
// =========================================================================
|
|
238
|
+
// Decide between integrated mode (use existing BenchlingSecret) or
|
|
239
|
+
// standalone mode (create new dedicated secret)
|
|
240
|
+
printStepHeader(5, STEP_TITLES.modeDecision);
|
|
241
|
+
const modeDecision = await (0, phase5_mode_decision_1.runModeDecision)({
|
|
242
|
+
stackQuery,
|
|
243
|
+
yes,
|
|
244
|
+
});
|
|
245
|
+
// =========================================================================
|
|
246
|
+
// PHASE 6 OR 7: MODE-SPECIFIC EXECUTION
|
|
247
|
+
// =========================================================================
|
|
248
|
+
if (modeDecision.mode === "integrated") {
|
|
249
|
+
// =====================================================================
|
|
250
|
+
// PHASE 6: INTEGRATED MODE
|
|
251
|
+
// =====================================================================
|
|
252
|
+
// Update existing BenchlingSecret in Quilt stack
|
|
253
|
+
// NO deployment - Quilt stack handles webhook
|
|
254
|
+
// MUST return here to prevent fall-through
|
|
255
|
+
printStepHeader(6, STEP_TITLES.integratedMode);
|
|
256
|
+
await (0, phase6_integrated_mode_1.runIntegratedMode)({
|
|
257
|
+
profile,
|
|
258
|
+
catalogDns: catalogResult.catalogDns,
|
|
259
|
+
stackQuery,
|
|
260
|
+
parameters,
|
|
261
|
+
benchlingSecretArn: modeDecision.benchlingSecretArn,
|
|
262
|
+
configStorage: xdg,
|
|
263
|
+
awsProfile,
|
|
264
|
+
});
|
|
265
|
+
// CRITICAL: Explicit return for integrated mode
|
|
266
|
+
// Cannot fall through to deployment
|
|
267
|
+
const finalConfig = xdg.readProfile(profile);
|
|
268
|
+
return {
|
|
269
|
+
success: true,
|
|
270
|
+
profile,
|
|
271
|
+
config: finalConfig,
|
|
272
|
+
};
|
|
782
273
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
//
|
|
789
|
-
|
|
274
|
+
else {
|
|
275
|
+
// =====================================================================
|
|
276
|
+
// PHASE 7: STANDALONE MODE
|
|
277
|
+
// =====================================================================
|
|
278
|
+
// Create new dedicated secret
|
|
279
|
+
// Optionally deploy as separate stack
|
|
280
|
+
printStepHeader(6, STEP_TITLES.standaloneMode);
|
|
281
|
+
await (0, phase7_standalone_mode_1.runStandaloneMode)({
|
|
790
282
|
profile,
|
|
791
|
-
|
|
283
|
+
catalogDns: catalogResult.catalogDns,
|
|
284
|
+
stackQuery,
|
|
285
|
+
parameters,
|
|
286
|
+
configStorage: xdg,
|
|
287
|
+
yes,
|
|
288
|
+
setupOnly,
|
|
289
|
+
awsProfile,
|
|
792
290
|
});
|
|
793
|
-
|
|
291
|
+
// CRITICAL: Explicit return for standalone mode
|
|
292
|
+
const finalConfig = xdg.readProfile(profile);
|
|
293
|
+
return {
|
|
294
|
+
success: true,
|
|
295
|
+
profile,
|
|
296
|
+
config: finalConfig,
|
|
297
|
+
};
|
|
794
298
|
}
|
|
795
|
-
// Return result for install command orchestration
|
|
796
|
-
return {
|
|
797
|
-
success: true,
|
|
798
|
-
profile,
|
|
799
|
-
config,
|
|
800
|
-
};
|
|
801
299
|
}
|
|
802
|
-
// =============================================================================
|
|
803
|
-
// CLI COMMAND EXPORT
|
|
804
|
-
// =============================================================================
|
|
805
300
|
/**
|
|
806
301
|
* Setup wizard command handler
|
|
807
302
|
*
|
|
303
|
+
* Wraps runSetupWizard with error handling for graceful user cancellation.
|
|
304
|
+
*
|
|
808
305
|
* @param options - Wizard options
|
|
809
306
|
* @returns Promise that resolves with setup result
|
|
810
307
|
*/
|
|
811
308
|
async function setupWizardCommand(options = {}) {
|
|
812
309
|
try {
|
|
813
|
-
return await
|
|
310
|
+
return await runSetupWizard(options);
|
|
814
311
|
}
|
|
815
312
|
catch (error) {
|
|
816
313
|
// Handle user cancellation (Ctrl+C) gracefully
|