@quiltdata/benchling-webhook 0.7.9 → 0.8.0-20251117T215047Z
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 +1 -0
- package/dist/bin/benchling-webhook.js +4 -2
- package/dist/bin/benchling-webhook.js.map +1 -1
- package/dist/bin/cli.js +15 -7
- package/dist/bin/cli.js.map +1 -1
- package/dist/bin/commands/deploy.d.ts.map +1 -1
- package/dist/bin/commands/deploy.js +64 -8
- package/dist/bin/commands/deploy.js.map +1 -1
- package/dist/bin/commands/health-check.d.ts.map +1 -1
- package/dist/bin/commands/health-check.js +2 -35
- package/dist/bin/commands/health-check.js.map +1 -1
- package/dist/bin/commands/infer-quilt-config.d.ts +6 -0
- package/dist/bin/commands/infer-quilt-config.d.ts.map +1 -1
- package/dist/bin/commands/infer-quilt-config.js +127 -49
- 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 +13 -17
- package/dist/bin/commands/install.js.map +1 -1
- package/dist/bin/commands/logs.d.ts.map +1 -1
- package/dist/bin/commands/logs.js +4 -128
- package/dist/bin/commands/logs.js.map +1 -1
- package/dist/bin/commands/manifest.d.ts.map +1 -1
- package/dist/bin/commands/manifest.js +2 -3
- package/dist/bin/commands/manifest.js.map +1 -1
- package/dist/bin/commands/setup-wizard.d.ts +2 -0
- package/dist/bin/commands/setup-wizard.d.ts.map +1 -1
- package/dist/bin/commands/setup-wizard.js +5 -5
- package/dist/bin/commands/setup-wizard.js.map +1 -1
- package/dist/bin/commands/status.d.ts +49 -0
- package/dist/bin/commands/status.d.ts.map +1 -1
- package/dist/bin/commands/status.js +666 -41
- package/dist/bin/commands/status.js.map +1 -1
- package/dist/bin/commands/sync-secrets.d.ts.map +1 -1
- package/dist/bin/commands/sync-secrets.js +3 -36
- package/dist/bin/commands/sync-secrets.js.map +1 -1
- package/dist/bin/commands/validate.js +1 -1
- package/dist/bin/commands/validate.js.map +1 -1
- package/dist/bin/xdg-launch.d.ts +74 -0
- package/dist/bin/xdg-launch.d.ts.map +1 -0
- package/dist/bin/xdg-launch.js +588 -0
- package/dist/bin/xdg-launch.js.map +1 -0
- package/dist/lib/benchling-webhook-stack.d.ts.map +1 -1
- package/dist/lib/benchling-webhook-stack.js +59 -7
- package/dist/lib/benchling-webhook-stack.js.map +1 -1
- package/dist/lib/fargate-service.d.ts +24 -4
- package/dist/lib/fargate-service.d.ts.map +1 -1
- package/dist/lib/fargate-service.js +75 -27
- package/dist/lib/fargate-service.js.map +1 -1
- package/dist/lib/types/config.d.ts +99 -5
- package/dist/lib/types/config.d.ts.map +1 -1
- package/dist/lib/types/config.js +4 -1
- package/dist/lib/types/config.js.map +1 -1
- package/dist/lib/utils/service-resolver.d.ts +155 -0
- package/dist/lib/utils/service-resolver.d.ts.map +1 -0
- package/dist/lib/utils/service-resolver.js +195 -0
- package/dist/lib/utils/service-resolver.js.map +1 -0
- package/dist/lib/utils/stack-inference.d.ts +58 -0
- package/dist/lib/utils/stack-inference.d.ts.map +1 -1
- package/dist/lib/utils/stack-inference.js +76 -2
- package/dist/lib/utils/stack-inference.js.map +1 -1
- package/dist/lib/utils/stack-parameter-update.js +2 -2
- package/dist/lib/utils/stack-parameter-update.js.map +1 -1
- package/dist/lib/wizard/phase2-stack-query.d.ts.map +1 -1
- package/dist/lib/wizard/phase2-stack-query.js +46 -9
- package/dist/lib/wizard/phase2-stack-query.js.map +1 -1
- package/dist/lib/wizard/phase3-parameter-collection.js +5 -5
- package/dist/lib/wizard/phase3-parameter-collection.js.map +1 -1
- package/dist/lib/wizard/phase4-validation.d.ts.map +1 -1
- package/dist/lib/wizard/phase4-validation.js +4 -5
- package/dist/lib/wizard/phase4-validation.js.map +1 -1
- package/dist/lib/wizard/phase5-mode-decision.js +1 -1
- package/dist/lib/wizard/phase5-mode-decision.js.map +1 -1
- package/dist/lib/wizard/phase6-integrated-mode.d.ts.map +1 -1
- package/dist/lib/wizard/phase6-integrated-mode.js +19 -0
- package/dist/lib/wizard/phase6-integrated-mode.js.map +1 -1
- package/dist/lib/wizard/phase7-standalone-mode.d.ts.map +1 -1
- package/dist/lib/wizard/phase7-standalone-mode.js +24 -10
- package/dist/lib/wizard/phase7-standalone-mode.js.map +1 -1
- package/dist/lib/wizard/types.d.ts +14 -0
- package/dist/lib/wizard/types.d.ts.map +1 -1
- package/dist/package.json +20 -9
- package/package.json +20 -9
- package/dist/lib/utils/config-loader.d.ts +0 -48
- package/dist/lib/utils/config-loader.d.ts.map +0 -1
- package/dist/lib/utils/config-loader.js +0 -110
- package/dist/lib/utils/config-loader.js.map +0 -1
- package/dist/lib/utils/config-resolver.d.ts +0 -138
- package/dist/lib/utils/config-resolver.d.ts.map +0 -1
- package/dist/lib/utils/config-resolver.js +0 -279
- package/dist/lib/utils/config-resolver.js.map +0 -1
|
@@ -15,7 +15,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
exports.formatStackStatus = formatStackStatus;
|
|
16
16
|
exports.statusCommand = statusCommand;
|
|
17
17
|
const chalk_1 = __importDefault(require("chalk"));
|
|
18
|
+
const ora_1 = __importDefault(require("ora"));
|
|
18
19
|
const client_cloudformation_1 = require("@aws-sdk/client-cloudformation");
|
|
20
|
+
const client_ecs_1 = require("@aws-sdk/client-ecs");
|
|
21
|
+
const client_elastic_load_balancing_v2_1 = require("@aws-sdk/client-elastic-load-balancing-v2");
|
|
22
|
+
const client_secrets_manager_1 = require("@aws-sdk/client-secrets-manager");
|
|
19
23
|
const credential_providers_1 = require("@aws-sdk/credential-providers");
|
|
20
24
|
const xdg_config_1 = require("../../lib/xdg-config");
|
|
21
25
|
/**
|
|
@@ -47,6 +51,15 @@ async function getStackStatus(stackArn, region, awsProfile) {
|
|
|
47
51
|
// Extract BenchlingIntegration parameter
|
|
48
52
|
const param = stack.Parameters?.find((p) => p.ParameterKey === "BenchlingIntegration");
|
|
49
53
|
const benchlingIntegrationEnabled = param?.ParameterValue === "Enabled";
|
|
54
|
+
// Extract stack outputs
|
|
55
|
+
const outputs = stack.Outputs || [];
|
|
56
|
+
const stackOutputs = {
|
|
57
|
+
benchlingUrl: outputs.find((o) => o.OutputKey === "BenchlingUrl")?.OutputValue,
|
|
58
|
+
secretArn: outputs.find((o) => o.OutputKey === "BenchlingSecretArn" || o.OutputKey === "BenchlingClientSecretArn" || o.OutputKey === "SecretArn")?.OutputValue,
|
|
59
|
+
dockerImage: outputs.find((o) => o.OutputKey === "BenchlingDockerImage" || o.OutputKey === "DockerImage")?.OutputValue,
|
|
60
|
+
ecsLogGroup: outputs.find((o) => o.OutputKey === "EcsLogGroup")?.OutputValue,
|
|
61
|
+
apiGatewayLogGroup: outputs.find((o) => o.OutputKey === "ApiGatewayLogGroup")?.OutputValue,
|
|
62
|
+
};
|
|
50
63
|
return {
|
|
51
64
|
success: true,
|
|
52
65
|
stackStatus: stack.StackStatus,
|
|
@@ -54,6 +67,7 @@ async function getStackStatus(stackArn, region, awsProfile) {
|
|
|
54
67
|
lastUpdateTime: stack.LastUpdatedTime?.toISOString() || stack.CreationTime?.toISOString(),
|
|
55
68
|
stackArn,
|
|
56
69
|
region,
|
|
70
|
+
stackOutputs,
|
|
57
71
|
};
|
|
58
72
|
}
|
|
59
73
|
catch (error) {
|
|
@@ -83,69 +97,566 @@ function formatStackStatus(status) {
|
|
|
83
97
|
}
|
|
84
98
|
}
|
|
85
99
|
/**
|
|
86
|
-
*
|
|
100
|
+
* Checks if stack status is terminal (no further updates expected)
|
|
87
101
|
*/
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
102
|
+
function isTerminalStatus(status) {
|
|
103
|
+
if (!status)
|
|
104
|
+
return false;
|
|
105
|
+
return status.endsWith("_COMPLETE") || status.endsWith("_FAILED");
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Parses timer value (string or number) and returns interval in milliseconds
|
|
109
|
+
* Returns null if timer is disabled (0 or non-numeric string)
|
|
110
|
+
*/
|
|
111
|
+
function parseTimerValue(timer) {
|
|
112
|
+
if (timer === undefined)
|
|
113
|
+
return 10000; // Default 10 seconds
|
|
114
|
+
const numValue = typeof timer === "string" ? parseFloat(timer) : timer;
|
|
115
|
+
// If NaN or 0, disable timer
|
|
116
|
+
if (isNaN(numValue) || numValue === 0) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
// Return milliseconds
|
|
120
|
+
return numValue * 1000;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Sleep helper
|
|
124
|
+
*/
|
|
125
|
+
function sleep(ms) {
|
|
126
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Clear screen and move cursor to top
|
|
130
|
+
*/
|
|
131
|
+
function clearScreen() {
|
|
132
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Gets ECS service health information
|
|
136
|
+
*/
|
|
137
|
+
async function getEcsServiceHealth(stackName, region, awsProfile) {
|
|
95
138
|
try {
|
|
96
|
-
|
|
139
|
+
// Configure AWS SDK clients
|
|
140
|
+
const clientConfig = { region };
|
|
141
|
+
if (awsProfile) {
|
|
142
|
+
clientConfig.credentials = (0, credential_providers_1.fromIni)({ profile: awsProfile });
|
|
143
|
+
}
|
|
144
|
+
const cfClient = new client_cloudformation_1.CloudFormationClient(clientConfig);
|
|
145
|
+
const ecsClient = new client_ecs_1.ECSClient(clientConfig);
|
|
146
|
+
// Find ECS resources in stack
|
|
147
|
+
const resourcesCommand = new client_cloudformation_1.DescribeStackResourcesCommand({
|
|
148
|
+
StackName: stackName,
|
|
149
|
+
});
|
|
150
|
+
const resourcesResponse = await cfClient.send(resourcesCommand);
|
|
151
|
+
const ecsServices = resourcesResponse.StackResources?.filter((r) => r.ResourceType === "AWS::ECS::Service") || [];
|
|
152
|
+
if (ecsServices.length === 0) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
// Get cluster name (assuming all services use the same cluster)
|
|
156
|
+
const clusterResource = resourcesResponse.StackResources?.find((r) => r.ResourceType === "AWS::ECS::Cluster");
|
|
157
|
+
const clusterName = clusterResource?.PhysicalResourceId || stackName;
|
|
158
|
+
// Describe all ECS services
|
|
159
|
+
const serviceArns = ecsServices
|
|
160
|
+
.map((s) => s.PhysicalResourceId)
|
|
161
|
+
.filter((arn) => !!arn);
|
|
162
|
+
if (serviceArns.length === 0) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
const servicesCommand = new client_ecs_1.DescribeServicesCommand({
|
|
166
|
+
cluster: clusterName,
|
|
167
|
+
services: serviceArns,
|
|
168
|
+
});
|
|
169
|
+
const servicesResponse = await ecsClient.send(servicesCommand);
|
|
170
|
+
// Get log groups from task definitions
|
|
171
|
+
const servicesWithLogs = await Promise.all((servicesResponse.services || []).map(async (svc) => {
|
|
172
|
+
let logGroup;
|
|
173
|
+
let logStreamPrefix;
|
|
174
|
+
// Get task definition ARN from the current deployment
|
|
175
|
+
const taskDefArn = svc.deployments?.[0]?.taskDefinition;
|
|
176
|
+
if (taskDefArn) {
|
|
177
|
+
try {
|
|
178
|
+
const taskDefCommand = new client_ecs_1.DescribeTaskDefinitionCommand({
|
|
179
|
+
taskDefinition: taskDefArn,
|
|
180
|
+
});
|
|
181
|
+
const taskDefResponse = await ecsClient.send(taskDefCommand);
|
|
182
|
+
// Extract log group and stream prefix from first container's log configuration
|
|
183
|
+
const logConfig = taskDefResponse.taskDefinition?.containerDefinitions?.[0]?.logConfiguration;
|
|
184
|
+
if (logConfig?.logDriver === "awslogs") {
|
|
185
|
+
logGroup = logConfig.options?.["awslogs-group"];
|
|
186
|
+
logStreamPrefix = logConfig.options?.["awslogs-stream-prefix"];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
// Log group query failed, continue without it
|
|
191
|
+
console.error(chalk_1.default.dim(` Could not retrieve log group for ${svc.serviceName}: ${error.message}`));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
serviceName: svc.serviceName || "Unknown",
|
|
196
|
+
status: svc.status || "UNKNOWN",
|
|
197
|
+
desiredCount: svc.desiredCount || 0,
|
|
198
|
+
runningCount: svc.runningCount || 0,
|
|
199
|
+
pendingCount: svc.pendingCount || 0,
|
|
200
|
+
rolloutState: svc.deployments?.[0]?.rolloutState,
|
|
201
|
+
logGroup,
|
|
202
|
+
logStreamPrefix,
|
|
203
|
+
};
|
|
204
|
+
}));
|
|
205
|
+
return servicesWithLogs;
|
|
97
206
|
}
|
|
98
|
-
catch {
|
|
99
|
-
|
|
100
|
-
console.error(chalk_1.default.
|
|
207
|
+
catch (error) {
|
|
208
|
+
// ECS health check is optional, don't fail the entire command
|
|
209
|
+
console.error(chalk_1.default.dim(` Could not retrieve ECS service health: ${error.message}`));
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Gets recent stack events
|
|
215
|
+
*/
|
|
216
|
+
async function getRecentStackEvents(stackName, region, awsProfile, maxEvents = 10) {
|
|
217
|
+
try {
|
|
218
|
+
// Configure AWS SDK client
|
|
219
|
+
const clientConfig = { region };
|
|
220
|
+
if (awsProfile) {
|
|
221
|
+
clientConfig.credentials = (0, credential_providers_1.fromIni)({ profile: awsProfile });
|
|
222
|
+
}
|
|
223
|
+
const client = new client_cloudformation_1.CloudFormationClient(clientConfig);
|
|
224
|
+
const command = new client_cloudformation_1.DescribeStackEventsCommand({
|
|
225
|
+
StackName: stackName,
|
|
226
|
+
});
|
|
227
|
+
const response = await client.send(command);
|
|
228
|
+
return response.StackEvents?.slice(0, maxEvents).map((event) => ({
|
|
229
|
+
timestamp: event.Timestamp || new Date(),
|
|
230
|
+
resourceId: event.LogicalResourceId || "Unknown",
|
|
231
|
+
status: event.ResourceStatus || "UNKNOWN",
|
|
232
|
+
reason: event.ResourceStatusReason,
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
// Stack events are optional, don't fail the entire command
|
|
237
|
+
console.error(chalk_1.default.dim(` Could not retrieve stack events: ${error.message}`));
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Gets ALB target group health information
|
|
243
|
+
*/
|
|
244
|
+
async function getAlbTargetHealth(stackName, region, awsProfile) {
|
|
245
|
+
try {
|
|
246
|
+
// Configure AWS SDK clients
|
|
247
|
+
const clientConfig = { region };
|
|
248
|
+
if (awsProfile) {
|
|
249
|
+
clientConfig.credentials = (0, credential_providers_1.fromIni)({ profile: awsProfile });
|
|
250
|
+
}
|
|
251
|
+
const cfClient = new client_cloudformation_1.CloudFormationClient(clientConfig);
|
|
252
|
+
const elbClient = new client_elastic_load_balancing_v2_1.ElasticLoadBalancingV2Client(clientConfig);
|
|
253
|
+
// Find Target Group resources in stack
|
|
254
|
+
const resourcesCommand = new client_cloudformation_1.DescribeStackResourcesCommand({
|
|
255
|
+
StackName: stackName,
|
|
256
|
+
});
|
|
257
|
+
const resourcesResponse = await cfClient.send(resourcesCommand);
|
|
258
|
+
const targetGroups = resourcesResponse.StackResources?.filter((r) => r.ResourceType === "AWS::ElasticLoadBalancingV2::TargetGroup") || [];
|
|
259
|
+
if (targetGroups.length === 0) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
// Get target group ARNs
|
|
263
|
+
const targetGroupArns = targetGroups
|
|
264
|
+
.map((tg) => tg.PhysicalResourceId)
|
|
265
|
+
.filter((arn) => !!arn);
|
|
266
|
+
if (targetGroupArns.length === 0) {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
// Get target group names
|
|
270
|
+
const tgInfoCommand = new client_elastic_load_balancing_v2_1.DescribeTargetGroupsCommand({
|
|
271
|
+
TargetGroupArns: targetGroupArns,
|
|
272
|
+
});
|
|
273
|
+
const tgInfoResponse = await elbClient.send(tgInfoCommand);
|
|
274
|
+
const result = [];
|
|
275
|
+
// Get health for each target group
|
|
276
|
+
for (const tgArn of targetGroupArns) {
|
|
277
|
+
const healthCommand = new client_elastic_load_balancing_v2_1.DescribeTargetHealthCommand({
|
|
278
|
+
TargetGroupArn: tgArn,
|
|
279
|
+
});
|
|
280
|
+
const healthResponse = await elbClient.send(healthCommand);
|
|
281
|
+
const tgInfo = tgInfoResponse.TargetGroups?.find((tg) => tg.TargetGroupArn === tgArn);
|
|
282
|
+
const tgName = tgInfo?.TargetGroupName || tgArn.split("/").pop() || "Unknown";
|
|
283
|
+
const targets = healthResponse.TargetHealthDescriptions?.map((target) => ({
|
|
284
|
+
id: target.Target?.Id || "Unknown",
|
|
285
|
+
health: target.TargetHealth?.State || "unknown",
|
|
286
|
+
reason: target.TargetHealth?.Reason,
|
|
287
|
+
})) || [];
|
|
288
|
+
const healthyCount = targets.filter((t) => t.health === "healthy").length;
|
|
289
|
+
const unhealthyCount = targets.filter((t) => t.health === "unhealthy").length;
|
|
290
|
+
const drainingCount = targets.filter((t) => t.health === "draining").length;
|
|
291
|
+
result.push({
|
|
292
|
+
targetGroupName: tgName,
|
|
293
|
+
healthyCount,
|
|
294
|
+
unhealthyCount,
|
|
295
|
+
drainingCount,
|
|
296
|
+
targets,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return result.length > 0 ? result : undefined;
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
// ALB health check is optional, don't fail the entire command
|
|
303
|
+
console.error(chalk_1.default.dim(` Could not retrieve ALB target health: ${error.message}`));
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Gets Secrets Manager secret information
|
|
309
|
+
*/
|
|
310
|
+
async function getSecretInfo(secretArn, region, awsProfile) {
|
|
311
|
+
try {
|
|
312
|
+
// Configure AWS SDK client
|
|
313
|
+
const clientConfig = { region };
|
|
314
|
+
if (awsProfile) {
|
|
315
|
+
clientConfig.credentials = (0, credential_providers_1.fromIni)({ profile: awsProfile });
|
|
316
|
+
}
|
|
317
|
+
const client = new client_secrets_manager_1.SecretsManagerClient(clientConfig);
|
|
318
|
+
const command = new client_secrets_manager_1.DescribeSecretCommand({
|
|
319
|
+
SecretId: secretArn,
|
|
320
|
+
});
|
|
321
|
+
const response = await client.send(command);
|
|
322
|
+
// Only use LastChangedDate - do NOT fall back to CreatedDate
|
|
323
|
+
// If LastChangedDate is undefined, the secret has never been modified
|
|
324
|
+
// Extract full secret name from ARN (includes random suffix)
|
|
325
|
+
// ARN format: arn:aws:secretsmanager:region:account:secret:name-suffix
|
|
326
|
+
const fullSecretName = secretArn.split(":secret:")[1] || response.Name || secretArn;
|
|
101
327
|
return {
|
|
102
|
-
|
|
103
|
-
|
|
328
|
+
name: fullSecretName,
|
|
329
|
+
lastModified: response.LastChangedDate,
|
|
330
|
+
accessible: true,
|
|
104
331
|
};
|
|
105
332
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
console.log(chalk_1.default.dim("This profile is configured for standalone deployment."));
|
|
111
|
-
console.log(chalk_1.default.dim("Use CloudFormation console to check webhook stack status.\n"));
|
|
333
|
+
catch (error) {
|
|
334
|
+
// Extract secret name from ARN: arn:aws:secretsmanager:region:account:secret:name-6chars
|
|
335
|
+
// The secret name is everything after "secret:" in the ARN
|
|
336
|
+
const secretName = secretArn.split(":secret:")[1] || secretArn.split(":").pop() || secretArn;
|
|
112
337
|
return {
|
|
113
|
-
|
|
114
|
-
|
|
338
|
+
name: secretName,
|
|
339
|
+
accessible: false,
|
|
340
|
+
error: error.message,
|
|
115
341
|
};
|
|
116
342
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Gets ALB listener rules information
|
|
346
|
+
*/
|
|
347
|
+
async function getListenerRules(stackName, region, awsProfile) {
|
|
348
|
+
try {
|
|
349
|
+
// Configure AWS SDK clients
|
|
350
|
+
const clientConfig = { region };
|
|
351
|
+
if (awsProfile) {
|
|
352
|
+
clientConfig.credentials = (0, credential_providers_1.fromIni)({ profile: awsProfile });
|
|
353
|
+
}
|
|
354
|
+
const cfClient = new client_cloudformation_1.CloudFormationClient(clientConfig);
|
|
355
|
+
const elbClient = new client_elastic_load_balancing_v2_1.ElasticLoadBalancingV2Client(clientConfig);
|
|
356
|
+
// Find Listener Rule resources in stack
|
|
357
|
+
const resourcesCommand = new client_cloudformation_1.DescribeStackResourcesCommand({
|
|
358
|
+
StackName: stackName,
|
|
359
|
+
});
|
|
360
|
+
const resourcesResponse = await cfClient.send(resourcesCommand);
|
|
361
|
+
const listenerRules = resourcesResponse.StackResources?.filter((r) => r.ResourceType === "AWS::ElasticLoadBalancingV2::ListenerRule") || [];
|
|
362
|
+
if (listenerRules.length === 0) {
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
const result = [];
|
|
366
|
+
// Get details for each listener rule
|
|
367
|
+
for (const ruleResource of listenerRules) {
|
|
368
|
+
const ruleArn = ruleResource.PhysicalResourceId;
|
|
369
|
+
if (!ruleArn)
|
|
370
|
+
continue;
|
|
371
|
+
// Extract listener ARN from rule ARN
|
|
372
|
+
const listenerArnMatch = ruleArn.match(/(arn:aws:elasticloadbalancing:[^:]+:[^:]+:listener\/[^/]+\/[^/]+\/[^/]+)/);
|
|
373
|
+
if (!listenerArnMatch)
|
|
374
|
+
continue;
|
|
375
|
+
const listenerArn = listenerArnMatch[1];
|
|
376
|
+
const rulesCommand = new client_elastic_load_balancing_v2_1.DescribeRulesCommand({
|
|
377
|
+
ListenerArn: listenerArn,
|
|
378
|
+
});
|
|
379
|
+
const rulesResponse = await elbClient.send(rulesCommand);
|
|
380
|
+
const rule = rulesResponse.Rules?.find((r) => r.RuleArn === ruleArn);
|
|
381
|
+
if (rule) {
|
|
382
|
+
const pathCondition = rule.Conditions?.find((c) => c.Field === "path-pattern");
|
|
383
|
+
const path = pathCondition?.Values?.[0] || "N/A";
|
|
384
|
+
const targetGroupArn = rule.Actions?.[0]?.TargetGroupArn || "N/A";
|
|
385
|
+
result.push({
|
|
386
|
+
priority: rule.Priority || "N/A",
|
|
387
|
+
path,
|
|
388
|
+
targetGroupArn: targetGroupArn.split("/").pop() || targetGroupArn,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return result.length > 0 ? result : undefined;
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
// Listener rules are optional, don't fail the entire command
|
|
396
|
+
console.error(chalk_1.default.dim(` Could not retrieve listener rules: ${error.message}`));
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Fetches complete status including all health checks
|
|
402
|
+
*/
|
|
403
|
+
async function fetchCompleteStatus(stackArn, stackName, region, awsProfile) {
|
|
122
404
|
const result = await getStackStatus(stackArn, region, awsProfile);
|
|
123
405
|
if (!result.success) {
|
|
124
|
-
console.error(chalk_1.default.red(`❌ Failed to get stack status: ${result.error}\n`));
|
|
125
406
|
return result;
|
|
126
407
|
}
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
408
|
+
// Get additional info in parallel
|
|
409
|
+
const secretArn = result.stackOutputs?.secretArn;
|
|
410
|
+
const [ecsServices, albTargetGroups, secretInfo, listenerRules, stackEvents] = await Promise.all([
|
|
411
|
+
getEcsServiceHealth(stackName, region, awsProfile),
|
|
412
|
+
getAlbTargetHealth(stackName, region, awsProfile),
|
|
413
|
+
secretArn ? getSecretInfo(secretArn, region, awsProfile) : Promise.resolve(undefined),
|
|
414
|
+
getListenerRules(stackName, region, awsProfile),
|
|
415
|
+
getRecentStackEvents(stackName, region, awsProfile, 3),
|
|
416
|
+
]);
|
|
417
|
+
result.ecsServices = ecsServices;
|
|
418
|
+
result.albTargetGroups = albTargetGroups;
|
|
419
|
+
result.secretInfo = secretInfo;
|
|
420
|
+
result.listenerRules = listenerRules;
|
|
421
|
+
result.stackEvents = stackEvents;
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Displays status result to console
|
|
426
|
+
*/
|
|
427
|
+
/* istanbul ignore next */
|
|
428
|
+
function displayStatusResult(result, profile, quiltConfig) {
|
|
429
|
+
const stackName = result.stackArn?.match(/stack\/([^/]+)\//)?.[1] || result.stackArn || "Unknown";
|
|
430
|
+
const region = result.region || "Unknown";
|
|
431
|
+
// Format last updated time in local timezone
|
|
432
|
+
let lastUpdatedStr = "";
|
|
433
|
+
if (result.lastUpdateTime) {
|
|
434
|
+
const lastUpdated = new Date(result.lastUpdateTime);
|
|
435
|
+
const timeStr = lastUpdated.toLocaleString("en-US", {
|
|
436
|
+
year: "numeric",
|
|
437
|
+
month: "2-digit",
|
|
438
|
+
day: "2-digit",
|
|
439
|
+
hour: "2-digit",
|
|
440
|
+
minute: "2-digit",
|
|
441
|
+
hour12: false,
|
|
442
|
+
});
|
|
443
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
444
|
+
lastUpdatedStr = ` @ ${timeStr} (${timezone})`;
|
|
445
|
+
}
|
|
446
|
+
// Display header with last updated time
|
|
447
|
+
console.log(chalk_1.default.bold(`\nStack Status for Profile: ${profile}${lastUpdatedStr}\n`));
|
|
448
|
+
console.log(chalk_1.default.dim("─".repeat(80)));
|
|
449
|
+
console.log(`${chalk_1.default.bold("Stack:")} ${chalk_1.default.cyan(stackName)} ${chalk_1.default.bold("Region:")} ${chalk_1.default.cyan(region)}`);
|
|
450
|
+
console.log(`${chalk_1.default.bold("Stack Status:")} ${formatStackStatus(result.stackStatus)} ${chalk_1.default.bold("BenchlingIntegration:")} ${result.benchlingIntegrationEnabled ? chalk_1.default.green("✓ Enabled") : chalk_1.default.yellow("⚠ Disabled")}`);
|
|
130
451
|
console.log("");
|
|
131
|
-
|
|
132
|
-
if (result.
|
|
133
|
-
|
|
452
|
+
// Display stack outputs and secret info on one line each
|
|
453
|
+
if (result.stackOutputs) {
|
|
454
|
+
if (result.stackOutputs.benchlingUrl) {
|
|
455
|
+
console.log(`${chalk_1.default.bold("Benchling URL:")} ${chalk_1.default.cyan(result.stackOutputs.benchlingUrl)}`);
|
|
456
|
+
}
|
|
457
|
+
if (result.stackOutputs.dockerImage) {
|
|
458
|
+
console.log(`${chalk_1.default.bold("Docker Image:")} ${chalk_1.default.dim(result.stackOutputs.dockerImage)}`);
|
|
459
|
+
}
|
|
134
460
|
}
|
|
135
|
-
|
|
136
|
-
|
|
461
|
+
// Display secret info on one line
|
|
462
|
+
if (result.secretInfo) {
|
|
463
|
+
let secretLine = `${chalk_1.default.bold("Secrets Manager:")} `;
|
|
464
|
+
if (result.secretInfo.accessible) {
|
|
465
|
+
if (result.secretInfo.lastModified) {
|
|
466
|
+
// Secret has been modified - show with green checkmark
|
|
467
|
+
secretLine += `${chalk_1.default.green("✓")} ${chalk_1.default.cyan(result.secretInfo.name)}`;
|
|
468
|
+
const deltaMs = Date.now() - result.secretInfo.lastModified.getTime();
|
|
469
|
+
const minutes = Math.floor(deltaMs / 60000);
|
|
470
|
+
const hours = Math.floor(minutes / 60);
|
|
471
|
+
const days = Math.floor(hours / 24);
|
|
472
|
+
let timeStr;
|
|
473
|
+
if (days > 0) {
|
|
474
|
+
timeStr = `${days} day${days !== 1 ? "s" : ""} ago`;
|
|
475
|
+
}
|
|
476
|
+
else if (hours > 0) {
|
|
477
|
+
timeStr = `${hours} hour${hours !== 1 ? "s" : ""} ago`;
|
|
478
|
+
}
|
|
479
|
+
else if (minutes > 0) {
|
|
480
|
+
timeStr = `${minutes} minute${minutes !== 1 ? "s" : ""} ago`;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
timeStr = "just now";
|
|
484
|
+
}
|
|
485
|
+
secretLine += chalk_1.default.dim(` (Last modified: ${timeStr})`);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
// Secret never modified - needs attention!
|
|
489
|
+
secretLine += `${chalk_1.default.red(result.secretInfo.name)} ${chalk_1.default.red("(NEVER MODIFIED - needs updating)")}`;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
secretLine += `${chalk_1.default.red("✗")} ${chalk_1.default.red(result.secretInfo.name)} - ${chalk_1.default.dim(result.secretInfo.error || "Inaccessible")}`;
|
|
494
|
+
}
|
|
495
|
+
console.log(secretLine);
|
|
496
|
+
}
|
|
497
|
+
// Display log groups
|
|
498
|
+
if (result.stackOutputs?.ecsLogGroup || result.stackOutputs?.apiGatewayLogGroup) {
|
|
499
|
+
console.log(`${chalk_1.default.bold("CloudWatch Logs:")}`);
|
|
500
|
+
if (result.stackOutputs.ecsLogGroup) {
|
|
501
|
+
console.log(` ${chalk_1.default.cyan("ECS:")} ${chalk_1.default.dim(result.stackOutputs.ecsLogGroup)}`);
|
|
502
|
+
}
|
|
503
|
+
if (result.stackOutputs.apiGatewayLogGroup) {
|
|
504
|
+
console.log(` ${chalk_1.default.cyan("API Gateway:")} ${chalk_1.default.dim(result.stackOutputs.apiGatewayLogGroup)}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
// Display Quilt stack resources (discovered from stack, not outputs)
|
|
508
|
+
if (quiltConfig) {
|
|
509
|
+
const resources = [];
|
|
510
|
+
if (quiltConfig.athenaUserWorkgroup)
|
|
511
|
+
resources.push({ label: "User Workgroup", value: quiltConfig.athenaUserWorkgroup });
|
|
512
|
+
if (quiltConfig.athenaUserPolicy)
|
|
513
|
+
resources.push({ label: "User Policy", value: quiltConfig.athenaUserPolicy });
|
|
514
|
+
if (quiltConfig.icebergWorkgroup)
|
|
515
|
+
resources.push({ label: "Iceberg Workgroup", value: quiltConfig.icebergWorkgroup });
|
|
516
|
+
if (quiltConfig.icebergDatabase)
|
|
517
|
+
resources.push({ label: "Iceberg Database", value: quiltConfig.icebergDatabase });
|
|
518
|
+
if (quiltConfig.athenaResultsBucket)
|
|
519
|
+
resources.push({ label: "Athena Results Bucket", value: quiltConfig.athenaResultsBucket });
|
|
520
|
+
if (quiltConfig.athenaResultsBucketPolicy)
|
|
521
|
+
resources.push({ label: "Results Bucket Policy", value: quiltConfig.athenaResultsBucketPolicy });
|
|
522
|
+
if (resources.length > 0) {
|
|
523
|
+
console.log(`${chalk_1.default.bold("Quilt Stack Resources:")}`);
|
|
524
|
+
for (const res of resources) {
|
|
525
|
+
console.log(` ${chalk_1.default.cyan(res.label + ":")} ${chalk_1.default.dim(res.value)}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
137
528
|
}
|
|
138
529
|
console.log("");
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
console.log(
|
|
530
|
+
// Display listener rules
|
|
531
|
+
if (result.listenerRules && result.listenerRules.length > 0) {
|
|
532
|
+
console.log(chalk_1.default.bold("ALB Listener Rules:"));
|
|
533
|
+
for (const rule of result.listenerRules) {
|
|
534
|
+
console.log(` ${chalk_1.default.cyan(rule.path)} ${chalk_1.default.dim(`(priority: ${rule.priority})`)}`);
|
|
535
|
+
console.log(` → ${chalk_1.default.dim(rule.targetGroupArn)}`);
|
|
536
|
+
}
|
|
537
|
+
console.log("");
|
|
538
|
+
}
|
|
539
|
+
// Display ECS service health in compact table format
|
|
540
|
+
if (result.ecsServices && result.ecsServices.length > 0) {
|
|
541
|
+
console.log(chalk_1.default.bold("ECS Services:"));
|
|
542
|
+
// Table header
|
|
543
|
+
const statusHeader = "Status";
|
|
544
|
+
const nameHeader = "Service";
|
|
545
|
+
const tasksHeader = "Tasks";
|
|
546
|
+
const rolloutHeader = "Rollout";
|
|
547
|
+
const logHeader = "Log Group";
|
|
548
|
+
// Calculate column widths
|
|
549
|
+
const maxNameLen = Math.max(nameHeader.length, ...result.ecsServices.map(s => s.serviceName.length));
|
|
550
|
+
const nameWidth = Math.min(maxNameLen + 2, 40); // Cap at 40 chars
|
|
551
|
+
const tasksWidth = 12;
|
|
552
|
+
const rolloutWidth = 15;
|
|
553
|
+
// Print header
|
|
554
|
+
console.log(` ${statusHeader.padEnd(8)} ${nameHeader.padEnd(nameWidth)} ${tasksHeader.padEnd(tasksWidth)} ${rolloutHeader.padEnd(rolloutWidth)} ${logHeader}`);
|
|
555
|
+
console.log(` ${chalk_1.default.dim("─".repeat(8))} ${chalk_1.default.dim("─".repeat(nameWidth))} ${chalk_1.default.dim("─".repeat(tasksWidth))} ${chalk_1.default.dim("─".repeat(rolloutWidth))} ${chalk_1.default.dim("─".repeat(30))}`);
|
|
556
|
+
// Print rows
|
|
557
|
+
for (const svc of result.ecsServices) {
|
|
558
|
+
const statusIcon = svc.status === "ACTIVE" ? "✓" : "⚠";
|
|
559
|
+
const statusColor = svc.status === "ACTIVE" ? chalk_1.default.green : chalk_1.default.yellow;
|
|
560
|
+
const tasksMatch = svc.runningCount === svc.desiredCount;
|
|
561
|
+
const tasksColor = tasksMatch ? chalk_1.default.green : chalk_1.default.yellow;
|
|
562
|
+
const statusCol = `${statusColor(statusIcon)} ${statusColor(svc.status)}`.padEnd(8 + 10); // +10 for ANSI codes
|
|
563
|
+
const nameCol = chalk_1.default.cyan(svc.serviceName.padEnd(nameWidth));
|
|
564
|
+
const tasksText = svc.pendingCount > 0
|
|
565
|
+
? `${svc.runningCount}/${svc.desiredCount} (${svc.pendingCount} pending)`
|
|
566
|
+
: `${svc.runningCount}/${svc.desiredCount}`;
|
|
567
|
+
const tasksCol = tasksColor(tasksText).padEnd(tasksWidth + (tasksMatch ? 10 : 10)); // Account for ANSI
|
|
568
|
+
let rolloutCol = "";
|
|
569
|
+
if (svc.rolloutState) {
|
|
570
|
+
if (svc.rolloutState === "COMPLETED") {
|
|
571
|
+
rolloutCol = chalk_1.default.green(svc.rolloutState).padEnd(rolloutWidth + 10);
|
|
572
|
+
}
|
|
573
|
+
else if (svc.rolloutState === "FAILED") {
|
|
574
|
+
rolloutCol = chalk_1.default.red(svc.rolloutState + " ❌").padEnd(rolloutWidth + 10);
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
rolloutCol = chalk_1.default.yellow(svc.rolloutState).padEnd(rolloutWidth + 10);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
rolloutCol = chalk_1.default.dim("-").padEnd(rolloutWidth + 10);
|
|
582
|
+
}
|
|
583
|
+
let logCol;
|
|
584
|
+
if (svc.logGroup) {
|
|
585
|
+
if (svc.logStreamPrefix) {
|
|
586
|
+
logCol = chalk_1.default.dim(`${svc.logGroup}/${svc.logStreamPrefix}`);
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
logCol = chalk_1.default.dim(svc.logGroup);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
logCol = chalk_1.default.dim("-");
|
|
594
|
+
}
|
|
595
|
+
console.log(` ${statusCol} ${nameCol} ${tasksCol} ${rolloutCol} ${logCol}`);
|
|
596
|
+
}
|
|
597
|
+
console.log("");
|
|
598
|
+
}
|
|
599
|
+
// Display ALB target group health
|
|
600
|
+
if (result.albTargetGroups && result.albTargetGroups.length > 0) {
|
|
601
|
+
console.log(chalk_1.default.bold("ALB Target Groups:"));
|
|
602
|
+
for (const tg of result.albTargetGroups) {
|
|
603
|
+
const allHealthy = tg.healthyCount > 0 && tg.unhealthyCount === 0;
|
|
604
|
+
const hasUnhealthy = tg.unhealthyCount > 0;
|
|
605
|
+
const statusIcon = allHealthy ? "✓" : hasUnhealthy ? "✗" : "⚠";
|
|
606
|
+
const statusColor = allHealthy ? chalk_1.default.green : hasUnhealthy ? chalk_1.default.red : chalk_1.default.yellow;
|
|
607
|
+
console.log(` ${statusColor(statusIcon)} ${chalk_1.default.cyan(tg.targetGroupName)}`);
|
|
608
|
+
console.log(` Targets: ${chalk_1.default.green(`${tg.healthyCount} healthy`)}${tg.unhealthyCount > 0 ? chalk_1.default.red(` / ${tg.unhealthyCount} unhealthy`) : ""}${tg.drainingCount > 0 ? chalk_1.default.dim(` / ${tg.drainingCount} draining`) : ""}`);
|
|
609
|
+
// Show unhealthy target details
|
|
610
|
+
const unhealthyTargets = tg.targets.filter((t) => t.health === "unhealthy");
|
|
611
|
+
if (unhealthyTargets.length > 0) {
|
|
612
|
+
for (const target of unhealthyTargets) {
|
|
613
|
+
console.log(` ${chalk_1.default.red("✗")} ${chalk_1.default.dim(target.id)}: ${chalk_1.default.red(target.health)}`);
|
|
614
|
+
if (target.reason) {
|
|
615
|
+
console.log(` ${chalk_1.default.dim(target.reason)}`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
console.log("");
|
|
621
|
+
}
|
|
622
|
+
// Display recent stack events
|
|
623
|
+
if (result.stackEvents && result.stackEvents.length > 0) {
|
|
624
|
+
const now = new Date();
|
|
625
|
+
console.log(chalk_1.default.bold("Recent Stack Events:"));
|
|
626
|
+
console.log(chalk_1.default.dim(` Current time: ${now.toISOString().replace("T", " ").substring(0, 19)} UTC\n`));
|
|
627
|
+
for (const event of result.stackEvents) {
|
|
628
|
+
// Calculate time delta
|
|
629
|
+
const deltaMs = now.getTime() - event.timestamp.getTime();
|
|
630
|
+
const deltaMinutes = Math.floor(deltaMs / 60000);
|
|
631
|
+
const deltaHours = Math.floor(deltaMinutes / 60);
|
|
632
|
+
const deltaDays = Math.floor(deltaHours / 24);
|
|
633
|
+
let deltaStr;
|
|
634
|
+
if (deltaDays > 0) {
|
|
635
|
+
deltaStr = `${deltaDays}d ${deltaHours % 24}h ago`;
|
|
636
|
+
}
|
|
637
|
+
else if (deltaHours > 0) {
|
|
638
|
+
deltaStr = `${deltaHours}h ${deltaMinutes % 60}m ago`;
|
|
639
|
+
}
|
|
640
|
+
else if (deltaMinutes > 0) {
|
|
641
|
+
deltaStr = `${deltaMinutes}m ago`;
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
deltaStr = "just now";
|
|
645
|
+
}
|
|
646
|
+
const statusStr = formatStackStatus(event.status);
|
|
647
|
+
console.log(` ${chalk_1.default.dim(deltaStr.padEnd(15))} ${statusStr}`);
|
|
648
|
+
console.log(` ${chalk_1.default.cyan(event.resourceId)}`);
|
|
649
|
+
if (event.reason && event.reason !== "None") {
|
|
650
|
+
console.log(` ${chalk_1.default.dim(event.reason)}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
142
653
|
console.log("");
|
|
143
654
|
}
|
|
144
655
|
// Show next steps based on status
|
|
145
656
|
if (result.stackStatus?.includes("IN_PROGRESS")) {
|
|
146
657
|
console.log(chalk_1.default.bold("Status:"));
|
|
147
658
|
console.log(chalk_1.default.yellow(" ⏳ Stack update in progress..."));
|
|
148
|
-
console.log(chalk_1.default.dim("
|
|
659
|
+
console.log(chalk_1.default.dim(" Auto-refreshing until complete...\n"));
|
|
149
660
|
}
|
|
150
661
|
else if (result.stackStatus?.includes("COMPLETE") && !result.stackStatus.includes("ROLLBACK")) {
|
|
151
662
|
console.log(chalk_1.default.bold("Status:"));
|
|
@@ -162,10 +673,124 @@ async function statusCommand(options = {}) {
|
|
|
162
673
|
console.log(chalk_1.default.dim(" Check CloudFormation console for detailed error messages\n"));
|
|
163
674
|
}
|
|
164
675
|
// CloudFormation console link
|
|
676
|
+
const stackArn = result.stackArn || "";
|
|
165
677
|
const consoleUrl = `https://${region}.console.aws.amazon.com/cloudformation/home?region=${region}#/stacks/stackinfo?stackId=${encodeURIComponent(stackArn)}`;
|
|
166
678
|
console.log(chalk_1.default.bold("CloudFormation Console:"));
|
|
167
679
|
console.log(chalk_1.default.cyan(` ${consoleUrl}\n`));
|
|
168
680
|
console.log(chalk_1.default.dim("─".repeat(80)));
|
|
169
|
-
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Status command implementation
|
|
684
|
+
*/
|
|
685
|
+
async function statusCommand(options = {}) {
|
|
686
|
+
const { profile = "default", awsProfile, configStorage, timer, exit = true, } = options;
|
|
687
|
+
const xdg = configStorage || new xdg_config_1.XDGConfig();
|
|
688
|
+
// Load configuration
|
|
689
|
+
let config;
|
|
690
|
+
try {
|
|
691
|
+
config = xdg.readProfile(profile);
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
const errorMsg = `Profile '${profile}' not found. Run setup first.`;
|
|
695
|
+
console.error(chalk_1.default.red(`\n❌ ${errorMsg}\n`));
|
|
696
|
+
return {
|
|
697
|
+
success: false,
|
|
698
|
+
error: errorMsg,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
// Check if integrated stack
|
|
702
|
+
if (!config.integratedStack) {
|
|
703
|
+
const errorMsg = "Status command is only available for integrated stack mode";
|
|
704
|
+
console.log(chalk_1.default.yellow(`\n⚠️ ${errorMsg}\n`));
|
|
705
|
+
console.log(chalk_1.default.dim("This profile is configured for standalone deployment."));
|
|
706
|
+
console.log(chalk_1.default.dim("Use CloudFormation console to check webhook stack status.\n"));
|
|
707
|
+
return {
|
|
708
|
+
success: false,
|
|
709
|
+
error: errorMsg,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
// Extract stack info
|
|
713
|
+
const stackArn = config.quilt.stackArn;
|
|
714
|
+
if (!stackArn) {
|
|
715
|
+
return {
|
|
716
|
+
success: false,
|
|
717
|
+
error: "Quilt stack ARN not found in configuration. This command requires a Quilt stack ARN to check integration status.",
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
const region = config.deployment.region;
|
|
721
|
+
const stackName = stackArn.match(/stack\/([^/]+)\//)?.[1] || stackArn;
|
|
722
|
+
// Parse timer value
|
|
723
|
+
const refreshInterval = parseTimerValue(timer);
|
|
724
|
+
// Setup Ctrl+C handler for graceful exit
|
|
725
|
+
let shouldExit = false;
|
|
726
|
+
const exitHandler = () => {
|
|
727
|
+
shouldExit = true;
|
|
728
|
+
console.log(chalk_1.default.dim("\n\n⚠️ Interrupted by user. Exiting...\n"));
|
|
729
|
+
process.exit(0);
|
|
730
|
+
};
|
|
731
|
+
process.on("SIGINT", exitHandler);
|
|
732
|
+
try {
|
|
733
|
+
let result;
|
|
734
|
+
let isFirstRun = true;
|
|
735
|
+
// Watch loop
|
|
736
|
+
while (true) {
|
|
737
|
+
// Clear screen on subsequent runs
|
|
738
|
+
if (!isFirstRun && refreshInterval) {
|
|
739
|
+
clearScreen();
|
|
740
|
+
}
|
|
741
|
+
// Fetch and display status
|
|
742
|
+
result = await fetchCompleteStatus(stackArn, stackName, region, awsProfile);
|
|
743
|
+
if (!result.success) {
|
|
744
|
+
console.error(chalk_1.default.red(`❌ Failed to get stack status: ${result.error}\n`));
|
|
745
|
+
return result;
|
|
746
|
+
}
|
|
747
|
+
displayStatusResult(result, profile, config.quilt);
|
|
748
|
+
// Check if we should exit (no timer or user disabled it)
|
|
749
|
+
if (!refreshInterval) {
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
// If terminal status, announce completion and exit (unless --no-exit is set)
|
|
753
|
+
if (isTerminalStatus(result.stackStatus)) {
|
|
754
|
+
if (exit) {
|
|
755
|
+
if (result.stackStatus?.includes("COMPLETE") && !result.stackStatus.includes("ROLLBACK")) {
|
|
756
|
+
console.log(chalk_1.default.green("✓ Stack reached stable state. Monitoring complete.\n"));
|
|
757
|
+
}
|
|
758
|
+
else if (result.stackStatus?.includes("FAILED") || result.stackStatus?.includes("ROLLBACK")) {
|
|
759
|
+
console.log(chalk_1.default.red("✗ Stack operation failed. Monitoring stopped.\n"));
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
console.log(chalk_1.default.dim("⟳ Stack reached terminal state. Auto-refresh stopped.\n"));
|
|
763
|
+
}
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
// If --no-exit is set, continue monitoring even after terminal status
|
|
767
|
+
}
|
|
768
|
+
// Show countdown with live updates
|
|
769
|
+
const totalSeconds = Math.floor(refreshInterval / 1000);
|
|
770
|
+
const spinner = (0, ora_1.default)({
|
|
771
|
+
text: chalk_1.default.dim(`⟳ Refreshing in ${totalSeconds} second${totalSeconds !== 1 ? "s" : ""}... (Ctrl+C to exit)`),
|
|
772
|
+
color: "gray",
|
|
773
|
+
}).start();
|
|
774
|
+
for (let i = totalSeconds; i > 0; i--) {
|
|
775
|
+
spinner.text = chalk_1.default.dim(`⟳ Refreshing in ${i} second${i !== 1 ? "s" : ""}... (Ctrl+C to exit)`);
|
|
776
|
+
await sleep(1000);
|
|
777
|
+
if (shouldExit)
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
spinner.stop();
|
|
781
|
+
if (shouldExit) {
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
isFirstRun = false;
|
|
785
|
+
}
|
|
786
|
+
// Clean up handler
|
|
787
|
+
process.off("SIGINT", exitHandler);
|
|
788
|
+
return result;
|
|
789
|
+
}
|
|
790
|
+
catch (error) {
|
|
791
|
+
// Clean up handler on error
|
|
792
|
+
process.off("SIGINT", exitHandler);
|
|
793
|
+
throw error;
|
|
794
|
+
}
|
|
170
795
|
}
|
|
171
796
|
//# sourceMappingURL=status.js.map
|