@quiltdata/benchling-webhook 0.7.8-20251115T063729Z → 0.7.10
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 +49 -0
- package/dist/bin/benchling-webhook.js +4 -2
- package/dist/bin/benchling-webhook.js.map +1 -1
- package/dist/bin/cli.js +87 -2
- package/dist/bin/cli.js.map +1 -1
- package/dist/bin/commands/infer-quilt-config.d.ts +1 -0
- package/dist/bin/commands/infer-quilt-config.d.ts.map +1 -1
- package/dist/bin/commands/infer-quilt-config.js +89 -46
- 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 +3 -13
- package/dist/bin/commands/install.js.map +1 -1
- package/dist/bin/commands/logs.d.ts +30 -0
- package/dist/bin/commands/logs.d.ts.map +1 -0
- package/dist/bin/commands/logs.js +152 -0
- package/dist/bin/commands/logs.js.map +1 -0
- package/dist/bin/commands/setup-wizard.d.ts.map +1 -1
- package/dist/bin/commands/setup-wizard.js +3 -4
- package/dist/bin/commands/setup-wizard.js.map +1 -1
- package/dist/bin/commands/status.d.ts +85 -0
- package/dist/bin/commands/status.d.ts.map +1 -0
- package/dist/bin/commands/status.js +765 -0
- package/dist/bin/commands/status.js.map +1 -0
- package/dist/bin/commands/sync-secrets.js +1 -1
- package/dist/lib/benchling-webhook-stack.d.ts.map +1 -1
- package/dist/lib/benchling-webhook-stack.js +3 -1
- package/dist/lib/benchling-webhook-stack.js.map +1 -1
- package/dist/lib/fargate-service.d.ts.map +1 -1
- package/dist/lib/fargate-service.js +3 -1
- package/dist/lib/fargate-service.js.map +1 -1
- package/dist/lib/next-steps-generator.js +25 -4
- package/dist/lib/next-steps-generator.js.map +1 -1
- package/dist/lib/utils/stack-parameter-update.d.ts +37 -0
- package/dist/lib/utils/stack-parameter-update.d.ts.map +1 -0
- package/dist/lib/utils/stack-parameter-update.js +129 -0
- package/dist/lib/utils/stack-parameter-update.js.map +1 -0
- package/dist/lib/wizard/phase2-stack-query.d.ts.map +1 -1
- package/dist/lib/wizard/phase2-stack-query.js +7 -1
- package/dist/lib/wizard/phase2-stack-query.js.map +1 -1
- package/dist/lib/wizard/phase3-parameter-collection.d.ts.map +1 -1
- package/dist/lib/wizard/phase3-parameter-collection.js +7 -17
- package/dist/lib/wizard/phase3-parameter-collection.js.map +1 -1
- package/dist/lib/wizard/phase4-validation.js +1 -1
- 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 +1 -0
- package/dist/lib/wizard/phase6-integrated-mode.d.ts.map +1 -1
- package/dist/lib/wizard/phase6-integrated-mode.js +104 -5
- package/dist/lib/wizard/phase6-integrated-mode.js.map +1 -1
- package/dist/lib/wizard/types.d.ts +3 -0
- package/dist/lib/wizard/types.d.ts.map +1 -1
- package/dist/package.json +6 -4
- package/dist/scripts/check-logs.d.ts +9 -2
- package/dist/scripts/check-logs.d.ts.map +1 -1
- package/dist/scripts/check-logs.js +26 -2
- package/dist/scripts/check-logs.js.map +1 -1
- package/package.json +6 -4
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Stack Status Command
|
|
5
|
+
*
|
|
6
|
+
* Reports CloudFormation stack status and BenchlingIntegration parameter state
|
|
7
|
+
* for a given configuration profile.
|
|
8
|
+
*
|
|
9
|
+
* @module commands/status
|
|
10
|
+
*/
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.formatStackStatus = formatStackStatus;
|
|
16
|
+
exports.statusCommand = statusCommand;
|
|
17
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
18
|
+
const ora_1 = __importDefault(require("ora"));
|
|
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");
|
|
23
|
+
const credential_providers_1 = require("@aws-sdk/credential-providers");
|
|
24
|
+
const xdg_config_1 = require("../../lib/xdg-config");
|
|
25
|
+
/**
|
|
26
|
+
* Gets stack status from CloudFormation
|
|
27
|
+
*/
|
|
28
|
+
async function getStackStatus(stackArn, region, awsProfile) {
|
|
29
|
+
try {
|
|
30
|
+
// Extract stack name from ARN
|
|
31
|
+
const stackNameMatch = stackArn.match(/stack\/([^/]+)\//);
|
|
32
|
+
if (!stackNameMatch) {
|
|
33
|
+
throw new Error(`Invalid stack ARN format: ${stackArn}`);
|
|
34
|
+
}
|
|
35
|
+
const stackName = stackNameMatch[1];
|
|
36
|
+
// Configure AWS SDK client
|
|
37
|
+
const clientConfig = { region };
|
|
38
|
+
if (awsProfile) {
|
|
39
|
+
clientConfig.credentials = (0, credential_providers_1.fromIni)({ profile: awsProfile });
|
|
40
|
+
}
|
|
41
|
+
const client = new client_cloudformation_1.CloudFormationClient(clientConfig);
|
|
42
|
+
// Describe stack
|
|
43
|
+
const command = new client_cloudformation_1.DescribeStacksCommand({
|
|
44
|
+
StackName: stackName,
|
|
45
|
+
});
|
|
46
|
+
const response = await client.send(command);
|
|
47
|
+
const stack = response.Stacks?.[0];
|
|
48
|
+
if (!stack) {
|
|
49
|
+
throw new Error(`Stack not found: ${stackName}`);
|
|
50
|
+
}
|
|
51
|
+
// Extract BenchlingIntegration parameter
|
|
52
|
+
const param = stack.Parameters?.find((p) => p.ParameterKey === "BenchlingIntegration");
|
|
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
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
success: true,
|
|
65
|
+
stackStatus: stack.StackStatus,
|
|
66
|
+
benchlingIntegrationEnabled,
|
|
67
|
+
lastUpdateTime: stack.LastUpdatedTime?.toISOString() || stack.CreationTime?.toISOString(),
|
|
68
|
+
stackArn,
|
|
69
|
+
region,
|
|
70
|
+
stackOutputs,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
error: error.message,
|
|
77
|
+
stackArn,
|
|
78
|
+
region,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Formats stack status with color coding
|
|
84
|
+
*/
|
|
85
|
+
function formatStackStatus(status) {
|
|
86
|
+
if (status.includes("COMPLETE") && !status.includes("ROLLBACK")) {
|
|
87
|
+
return chalk_1.default.green(status);
|
|
88
|
+
}
|
|
89
|
+
else if (status.includes("IN_PROGRESS")) {
|
|
90
|
+
return chalk_1.default.yellow(status);
|
|
91
|
+
}
|
|
92
|
+
else if (status.includes("FAILED") || status.includes("ROLLBACK")) {
|
|
93
|
+
return chalk_1.default.red(status);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
return chalk_1.default.dim(status);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Checks if stack status is terminal (no further updates expected)
|
|
101
|
+
*/
|
|
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) {
|
|
138
|
+
try {
|
|
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;
|
|
206
|
+
}
|
|
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;
|
|
327
|
+
return {
|
|
328
|
+
name: fullSecretName,
|
|
329
|
+
lastModified: response.LastChangedDate,
|
|
330
|
+
accessible: true,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
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;
|
|
337
|
+
return {
|
|
338
|
+
name: secretName,
|
|
339
|
+
accessible: false,
|
|
340
|
+
error: error.message,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
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) {
|
|
404
|
+
const result = await getStackStatus(stackArn, region, awsProfile);
|
|
405
|
+
if (!result.success) {
|
|
406
|
+
return result;
|
|
407
|
+
}
|
|
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) {
|
|
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")}`);
|
|
451
|
+
console.log("");
|
|
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
|
+
}
|
|
460
|
+
}
|
|
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
|
+
console.log("");
|
|
508
|
+
// Display listener rules
|
|
509
|
+
if (result.listenerRules && result.listenerRules.length > 0) {
|
|
510
|
+
console.log(chalk_1.default.bold("ALB Listener Rules:"));
|
|
511
|
+
for (const rule of result.listenerRules) {
|
|
512
|
+
console.log(` ${chalk_1.default.cyan(rule.path)} ${chalk_1.default.dim(`(priority: ${rule.priority})`)}`);
|
|
513
|
+
console.log(` → ${chalk_1.default.dim(rule.targetGroupArn)}`);
|
|
514
|
+
}
|
|
515
|
+
console.log("");
|
|
516
|
+
}
|
|
517
|
+
// Display ECS service health in compact table format
|
|
518
|
+
if (result.ecsServices && result.ecsServices.length > 0) {
|
|
519
|
+
console.log(chalk_1.default.bold("ECS Services:"));
|
|
520
|
+
// Table header
|
|
521
|
+
const statusHeader = "Status";
|
|
522
|
+
const nameHeader = "Service";
|
|
523
|
+
const tasksHeader = "Tasks";
|
|
524
|
+
const rolloutHeader = "Rollout";
|
|
525
|
+
const logHeader = "Log Group";
|
|
526
|
+
// Calculate column widths
|
|
527
|
+
const maxNameLen = Math.max(nameHeader.length, ...result.ecsServices.map(s => s.serviceName.length));
|
|
528
|
+
const nameWidth = Math.min(maxNameLen + 2, 40); // Cap at 40 chars
|
|
529
|
+
const tasksWidth = 12;
|
|
530
|
+
const rolloutWidth = 15;
|
|
531
|
+
// Print header
|
|
532
|
+
console.log(` ${statusHeader.padEnd(8)} ${nameHeader.padEnd(nameWidth)} ${tasksHeader.padEnd(tasksWidth)} ${rolloutHeader.padEnd(rolloutWidth)} ${logHeader}`);
|
|
533
|
+
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))}`);
|
|
534
|
+
// Print rows
|
|
535
|
+
for (const svc of result.ecsServices) {
|
|
536
|
+
const statusIcon = svc.status === "ACTIVE" ? "✓" : "⚠";
|
|
537
|
+
const statusColor = svc.status === "ACTIVE" ? chalk_1.default.green : chalk_1.default.yellow;
|
|
538
|
+
const tasksMatch = svc.runningCount === svc.desiredCount;
|
|
539
|
+
const tasksColor = tasksMatch ? chalk_1.default.green : chalk_1.default.yellow;
|
|
540
|
+
const statusCol = `${statusColor(statusIcon)} ${statusColor(svc.status)}`.padEnd(8 + 10); // +10 for ANSI codes
|
|
541
|
+
const nameCol = chalk_1.default.cyan(svc.serviceName.padEnd(nameWidth));
|
|
542
|
+
const tasksText = svc.pendingCount > 0
|
|
543
|
+
? `${svc.runningCount}/${svc.desiredCount} (${svc.pendingCount} pending)`
|
|
544
|
+
: `${svc.runningCount}/${svc.desiredCount}`;
|
|
545
|
+
const tasksCol = tasksColor(tasksText).padEnd(tasksWidth + (tasksMatch ? 10 : 10)); // Account for ANSI
|
|
546
|
+
let rolloutCol = "";
|
|
547
|
+
if (svc.rolloutState) {
|
|
548
|
+
if (svc.rolloutState === "COMPLETED") {
|
|
549
|
+
rolloutCol = chalk_1.default.green(svc.rolloutState).padEnd(rolloutWidth + 10);
|
|
550
|
+
}
|
|
551
|
+
else if (svc.rolloutState === "FAILED") {
|
|
552
|
+
rolloutCol = chalk_1.default.red(svc.rolloutState + " ❌").padEnd(rolloutWidth + 10);
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
rolloutCol = chalk_1.default.yellow(svc.rolloutState).padEnd(rolloutWidth + 10);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
rolloutCol = chalk_1.default.dim("-").padEnd(rolloutWidth + 10);
|
|
560
|
+
}
|
|
561
|
+
let logCol;
|
|
562
|
+
if (svc.logGroup) {
|
|
563
|
+
if (svc.logStreamPrefix) {
|
|
564
|
+
logCol = chalk_1.default.dim(`${svc.logGroup}/${svc.logStreamPrefix}`);
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
logCol = chalk_1.default.dim(svc.logGroup);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
logCol = chalk_1.default.dim("-");
|
|
572
|
+
}
|
|
573
|
+
console.log(` ${statusCol} ${nameCol} ${tasksCol} ${rolloutCol} ${logCol}`);
|
|
574
|
+
}
|
|
575
|
+
console.log("");
|
|
576
|
+
}
|
|
577
|
+
// Display ALB target group health
|
|
578
|
+
if (result.albTargetGroups && result.albTargetGroups.length > 0) {
|
|
579
|
+
console.log(chalk_1.default.bold("ALB Target Groups:"));
|
|
580
|
+
for (const tg of result.albTargetGroups) {
|
|
581
|
+
const allHealthy = tg.healthyCount > 0 && tg.unhealthyCount === 0;
|
|
582
|
+
const hasUnhealthy = tg.unhealthyCount > 0;
|
|
583
|
+
const statusIcon = allHealthy ? "✓" : hasUnhealthy ? "✗" : "⚠";
|
|
584
|
+
const statusColor = allHealthy ? chalk_1.default.green : hasUnhealthy ? chalk_1.default.red : chalk_1.default.yellow;
|
|
585
|
+
console.log(` ${statusColor(statusIcon)} ${chalk_1.default.cyan(tg.targetGroupName)}`);
|
|
586
|
+
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`) : ""}`);
|
|
587
|
+
// Show unhealthy target details
|
|
588
|
+
const unhealthyTargets = tg.targets.filter((t) => t.health === "unhealthy");
|
|
589
|
+
if (unhealthyTargets.length > 0) {
|
|
590
|
+
for (const target of unhealthyTargets) {
|
|
591
|
+
console.log(` ${chalk_1.default.red("✗")} ${chalk_1.default.dim(target.id)}: ${chalk_1.default.red(target.health)}`);
|
|
592
|
+
if (target.reason) {
|
|
593
|
+
console.log(` ${chalk_1.default.dim(target.reason)}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
console.log("");
|
|
599
|
+
}
|
|
600
|
+
// Display recent stack events
|
|
601
|
+
if (result.stackEvents && result.stackEvents.length > 0) {
|
|
602
|
+
const now = new Date();
|
|
603
|
+
console.log(chalk_1.default.bold("Recent Stack Events:"));
|
|
604
|
+
console.log(chalk_1.default.dim(` Current time: ${now.toISOString().replace("T", " ").substring(0, 19)} UTC\n`));
|
|
605
|
+
for (const event of result.stackEvents) {
|
|
606
|
+
// Calculate time delta
|
|
607
|
+
const deltaMs = now.getTime() - event.timestamp.getTime();
|
|
608
|
+
const deltaMinutes = Math.floor(deltaMs / 60000);
|
|
609
|
+
const deltaHours = Math.floor(deltaMinutes / 60);
|
|
610
|
+
const deltaDays = Math.floor(deltaHours / 24);
|
|
611
|
+
let deltaStr;
|
|
612
|
+
if (deltaDays > 0) {
|
|
613
|
+
deltaStr = `${deltaDays}d ${deltaHours % 24}h ago`;
|
|
614
|
+
}
|
|
615
|
+
else if (deltaHours > 0) {
|
|
616
|
+
deltaStr = `${deltaHours}h ${deltaMinutes % 60}m ago`;
|
|
617
|
+
}
|
|
618
|
+
else if (deltaMinutes > 0) {
|
|
619
|
+
deltaStr = `${deltaMinutes}m ago`;
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
deltaStr = "just now";
|
|
623
|
+
}
|
|
624
|
+
const statusStr = formatStackStatus(event.status);
|
|
625
|
+
console.log(` ${chalk_1.default.dim(deltaStr.padEnd(15))} ${statusStr}`);
|
|
626
|
+
console.log(` ${chalk_1.default.cyan(event.resourceId)}`);
|
|
627
|
+
if (event.reason && event.reason !== "None") {
|
|
628
|
+
console.log(` ${chalk_1.default.dim(event.reason)}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
console.log("");
|
|
632
|
+
}
|
|
633
|
+
// Show next steps based on status
|
|
634
|
+
if (result.stackStatus?.includes("IN_PROGRESS")) {
|
|
635
|
+
console.log(chalk_1.default.bold("Status:"));
|
|
636
|
+
console.log(chalk_1.default.yellow(" ⏳ Stack update in progress..."));
|
|
637
|
+
console.log(chalk_1.default.dim(" Auto-refreshing until complete...\n"));
|
|
638
|
+
}
|
|
639
|
+
else if (result.stackStatus?.includes("COMPLETE") && !result.stackStatus.includes("ROLLBACK")) {
|
|
640
|
+
console.log(chalk_1.default.bold("Status:"));
|
|
641
|
+
console.log(chalk_1.default.green(" ✓ Stack is up to date\n"));
|
|
642
|
+
if (!result.benchlingIntegrationEnabled) {
|
|
643
|
+
console.log(chalk_1.default.bold("Action Required:"));
|
|
644
|
+
console.log(chalk_1.default.yellow(" BenchlingIntegration is Disabled"));
|
|
645
|
+
console.log(chalk_1.default.dim(" Enable it via CloudFormation console or re-run setup\n"));
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
else if (result.stackStatus?.includes("FAILED") || result.stackStatus?.includes("ROLLBACK")) {
|
|
649
|
+
console.log(chalk_1.default.bold("Status:"));
|
|
650
|
+
console.log(chalk_1.default.red(" ❌ Stack update failed or rolled back"));
|
|
651
|
+
console.log(chalk_1.default.dim(" Check CloudFormation console for detailed error messages\n"));
|
|
652
|
+
}
|
|
653
|
+
// CloudFormation console link
|
|
654
|
+
const stackArn = result.stackArn || "";
|
|
655
|
+
const consoleUrl = `https://${region}.console.aws.amazon.com/cloudformation/home?region=${region}#/stacks/stackinfo?stackId=${encodeURIComponent(stackArn)}`;
|
|
656
|
+
console.log(chalk_1.default.bold("CloudFormation Console:"));
|
|
657
|
+
console.log(chalk_1.default.cyan(` ${consoleUrl}\n`));
|
|
658
|
+
console.log(chalk_1.default.dim("─".repeat(80)));
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Status command implementation
|
|
662
|
+
*/
|
|
663
|
+
async function statusCommand(options = {}) {
|
|
664
|
+
const { profile = "default", awsProfile, configStorage, timer, } = options;
|
|
665
|
+
const xdg = configStorage || new xdg_config_1.XDGConfig();
|
|
666
|
+
// Load configuration
|
|
667
|
+
let config;
|
|
668
|
+
try {
|
|
669
|
+
config = xdg.readProfile(profile);
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
const errorMsg = `Profile '${profile}' not found. Run setup first.`;
|
|
673
|
+
console.error(chalk_1.default.red(`\n❌ ${errorMsg}\n`));
|
|
674
|
+
return {
|
|
675
|
+
success: false,
|
|
676
|
+
error: errorMsg,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
// Check if integrated stack
|
|
680
|
+
if (!config.integratedStack) {
|
|
681
|
+
const errorMsg = "Status command is only available for integrated stack mode";
|
|
682
|
+
console.log(chalk_1.default.yellow(`\n⚠️ ${errorMsg}\n`));
|
|
683
|
+
console.log(chalk_1.default.dim("This profile is configured for standalone deployment."));
|
|
684
|
+
console.log(chalk_1.default.dim("Use CloudFormation console to check webhook stack status.\n"));
|
|
685
|
+
return {
|
|
686
|
+
success: false,
|
|
687
|
+
error: errorMsg,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
// Extract stack info
|
|
691
|
+
const stackArn = config.quilt.stackArn;
|
|
692
|
+
const region = config.deployment.region;
|
|
693
|
+
const stackName = stackArn.match(/stack\/([^/]+)\//)?.[1] || stackArn;
|
|
694
|
+
// Parse timer value
|
|
695
|
+
const refreshInterval = parseTimerValue(timer);
|
|
696
|
+
// Setup Ctrl+C handler for graceful exit
|
|
697
|
+
let shouldExit = false;
|
|
698
|
+
const exitHandler = () => {
|
|
699
|
+
shouldExit = true;
|
|
700
|
+
console.log(chalk_1.default.dim("\n\n⚠️ Interrupted by user. Exiting...\n"));
|
|
701
|
+
process.exit(0);
|
|
702
|
+
};
|
|
703
|
+
process.on("SIGINT", exitHandler);
|
|
704
|
+
try {
|
|
705
|
+
let result;
|
|
706
|
+
let isFirstRun = true;
|
|
707
|
+
// Watch loop
|
|
708
|
+
while (true) {
|
|
709
|
+
// Clear screen on subsequent runs
|
|
710
|
+
if (!isFirstRun && refreshInterval) {
|
|
711
|
+
clearScreen();
|
|
712
|
+
}
|
|
713
|
+
// Fetch and display status
|
|
714
|
+
result = await fetchCompleteStatus(stackArn, stackName, region, awsProfile);
|
|
715
|
+
if (!result.success) {
|
|
716
|
+
console.error(chalk_1.default.red(`❌ Failed to get stack status: ${result.error}\n`));
|
|
717
|
+
return result;
|
|
718
|
+
}
|
|
719
|
+
displayStatusResult(result, profile);
|
|
720
|
+
// Check if we should exit (no timer or user disabled it)
|
|
721
|
+
if (!refreshInterval) {
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
// If terminal status, announce completion and exit
|
|
725
|
+
if (isTerminalStatus(result.stackStatus)) {
|
|
726
|
+
if (result.stackStatus?.includes("COMPLETE") && !result.stackStatus.includes("ROLLBACK")) {
|
|
727
|
+
console.log(chalk_1.default.green("✓ Stack reached stable state. Monitoring complete.\n"));
|
|
728
|
+
}
|
|
729
|
+
else if (result.stackStatus?.includes("FAILED") || result.stackStatus?.includes("ROLLBACK")) {
|
|
730
|
+
console.log(chalk_1.default.red("✗ Stack operation failed. Monitoring stopped.\n"));
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
console.log(chalk_1.default.dim("⟳ Stack reached terminal state. Auto-refresh stopped.\n"));
|
|
734
|
+
}
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
// Show countdown with live updates
|
|
738
|
+
const totalSeconds = Math.floor(refreshInterval / 1000);
|
|
739
|
+
const spinner = (0, ora_1.default)({
|
|
740
|
+
text: chalk_1.default.dim(`⟳ Refreshing in ${totalSeconds} second${totalSeconds !== 1 ? "s" : ""}... (Ctrl+C to exit)`),
|
|
741
|
+
color: "gray",
|
|
742
|
+
}).start();
|
|
743
|
+
for (let i = totalSeconds; i > 0; i--) {
|
|
744
|
+
spinner.text = chalk_1.default.dim(`⟳ Refreshing in ${i} second${i !== 1 ? "s" : ""}... (Ctrl+C to exit)`);
|
|
745
|
+
await sleep(1000);
|
|
746
|
+
if (shouldExit)
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
spinner.stop();
|
|
750
|
+
if (shouldExit) {
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
isFirstRun = false;
|
|
754
|
+
}
|
|
755
|
+
// Clean up handler
|
|
756
|
+
process.off("SIGINT", exitHandler);
|
|
757
|
+
return result;
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
// Clean up handler on error
|
|
761
|
+
process.off("SIGINT", exitHandler);
|
|
762
|
+
throw error;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
//# sourceMappingURL=status.js.map
|