@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.
Files changed (90) hide show
  1. package/README.md +1 -0
  2. package/dist/bin/benchling-webhook.js +4 -2
  3. package/dist/bin/benchling-webhook.js.map +1 -1
  4. package/dist/bin/cli.js +15 -7
  5. package/dist/bin/cli.js.map +1 -1
  6. package/dist/bin/commands/deploy.d.ts.map +1 -1
  7. package/dist/bin/commands/deploy.js +64 -8
  8. package/dist/bin/commands/deploy.js.map +1 -1
  9. package/dist/bin/commands/health-check.d.ts.map +1 -1
  10. package/dist/bin/commands/health-check.js +2 -35
  11. package/dist/bin/commands/health-check.js.map +1 -1
  12. package/dist/bin/commands/infer-quilt-config.d.ts +6 -0
  13. package/dist/bin/commands/infer-quilt-config.d.ts.map +1 -1
  14. package/dist/bin/commands/infer-quilt-config.js +127 -49
  15. package/dist/bin/commands/infer-quilt-config.js.map +1 -1
  16. package/dist/bin/commands/install.d.ts.map +1 -1
  17. package/dist/bin/commands/install.js +13 -17
  18. package/dist/bin/commands/install.js.map +1 -1
  19. package/dist/bin/commands/logs.d.ts.map +1 -1
  20. package/dist/bin/commands/logs.js +4 -128
  21. package/dist/bin/commands/logs.js.map +1 -1
  22. package/dist/bin/commands/manifest.d.ts.map +1 -1
  23. package/dist/bin/commands/manifest.js +2 -3
  24. package/dist/bin/commands/manifest.js.map +1 -1
  25. package/dist/bin/commands/setup-wizard.d.ts +2 -0
  26. package/dist/bin/commands/setup-wizard.d.ts.map +1 -1
  27. package/dist/bin/commands/setup-wizard.js +5 -5
  28. package/dist/bin/commands/setup-wizard.js.map +1 -1
  29. package/dist/bin/commands/status.d.ts +49 -0
  30. package/dist/bin/commands/status.d.ts.map +1 -1
  31. package/dist/bin/commands/status.js +666 -41
  32. package/dist/bin/commands/status.js.map +1 -1
  33. package/dist/bin/commands/sync-secrets.d.ts.map +1 -1
  34. package/dist/bin/commands/sync-secrets.js +3 -36
  35. package/dist/bin/commands/sync-secrets.js.map +1 -1
  36. package/dist/bin/commands/validate.js +1 -1
  37. package/dist/bin/commands/validate.js.map +1 -1
  38. package/dist/bin/xdg-launch.d.ts +74 -0
  39. package/dist/bin/xdg-launch.d.ts.map +1 -0
  40. package/dist/bin/xdg-launch.js +588 -0
  41. package/dist/bin/xdg-launch.js.map +1 -0
  42. package/dist/lib/benchling-webhook-stack.d.ts.map +1 -1
  43. package/dist/lib/benchling-webhook-stack.js +59 -7
  44. package/dist/lib/benchling-webhook-stack.js.map +1 -1
  45. package/dist/lib/fargate-service.d.ts +24 -4
  46. package/dist/lib/fargate-service.d.ts.map +1 -1
  47. package/dist/lib/fargate-service.js +75 -27
  48. package/dist/lib/fargate-service.js.map +1 -1
  49. package/dist/lib/types/config.d.ts +99 -5
  50. package/dist/lib/types/config.d.ts.map +1 -1
  51. package/dist/lib/types/config.js +4 -1
  52. package/dist/lib/types/config.js.map +1 -1
  53. package/dist/lib/utils/service-resolver.d.ts +155 -0
  54. package/dist/lib/utils/service-resolver.d.ts.map +1 -0
  55. package/dist/lib/utils/service-resolver.js +195 -0
  56. package/dist/lib/utils/service-resolver.js.map +1 -0
  57. package/dist/lib/utils/stack-inference.d.ts +58 -0
  58. package/dist/lib/utils/stack-inference.d.ts.map +1 -1
  59. package/dist/lib/utils/stack-inference.js +76 -2
  60. package/dist/lib/utils/stack-inference.js.map +1 -1
  61. package/dist/lib/utils/stack-parameter-update.js +2 -2
  62. package/dist/lib/utils/stack-parameter-update.js.map +1 -1
  63. package/dist/lib/wizard/phase2-stack-query.d.ts.map +1 -1
  64. package/dist/lib/wizard/phase2-stack-query.js +46 -9
  65. package/dist/lib/wizard/phase2-stack-query.js.map +1 -1
  66. package/dist/lib/wizard/phase3-parameter-collection.js +5 -5
  67. package/dist/lib/wizard/phase3-parameter-collection.js.map +1 -1
  68. package/dist/lib/wizard/phase4-validation.d.ts.map +1 -1
  69. package/dist/lib/wizard/phase4-validation.js +4 -5
  70. package/dist/lib/wizard/phase4-validation.js.map +1 -1
  71. package/dist/lib/wizard/phase5-mode-decision.js +1 -1
  72. package/dist/lib/wizard/phase5-mode-decision.js.map +1 -1
  73. package/dist/lib/wizard/phase6-integrated-mode.d.ts.map +1 -1
  74. package/dist/lib/wizard/phase6-integrated-mode.js +19 -0
  75. package/dist/lib/wizard/phase6-integrated-mode.js.map +1 -1
  76. package/dist/lib/wizard/phase7-standalone-mode.d.ts.map +1 -1
  77. package/dist/lib/wizard/phase7-standalone-mode.js +24 -10
  78. package/dist/lib/wizard/phase7-standalone-mode.js.map +1 -1
  79. package/dist/lib/wizard/types.d.ts +14 -0
  80. package/dist/lib/wizard/types.d.ts.map +1 -1
  81. package/dist/package.json +20 -9
  82. package/package.json +20 -9
  83. package/dist/lib/utils/config-loader.d.ts +0 -48
  84. package/dist/lib/utils/config-loader.d.ts.map +0 -1
  85. package/dist/lib/utils/config-loader.js +0 -110
  86. package/dist/lib/utils/config-loader.js.map +0 -1
  87. package/dist/lib/utils/config-resolver.d.ts +0 -138
  88. package/dist/lib/utils/config-resolver.d.ts.map +0 -1
  89. package/dist/lib/utils/config-resolver.js +0 -279
  90. 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
- * Status command implementation
100
+ * Checks if stack status is terminal (no further updates expected)
87
101
  */
88
- async function statusCommand(options = {}) {
89
- const { profile = "default", awsProfile, configStorage, } = options;
90
- const xdg = configStorage || new xdg_config_1.XDGConfig();
91
- console.log(chalk_1.default.bold(`\nStack Status for Profile: ${profile}\n`));
92
- console.log(chalk_1.default.dim("─".repeat(80)));
93
- // Load configuration
94
- let config;
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
- config = xdg.readProfile(profile);
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
- const errorMsg = `Profile '${profile}' not found. Run setup first.`;
100
- console.error(chalk_1.default.red(`\n❌ ${errorMsg}\n`));
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
- success: false,
103
- error: errorMsg,
328
+ name: fullSecretName,
329
+ lastModified: response.LastChangedDate,
330
+ accessible: true,
104
331
  };
105
332
  }
106
- // Check if integrated stack
107
- if (!config.integratedStack) {
108
- const errorMsg = "Status command is only available for integrated stack mode";
109
- console.log(chalk_1.default.yellow(`\n⚠️ ${errorMsg}\n`));
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
- success: false,
114
- error: errorMsg,
338
+ name: secretName,
339
+ accessible: false,
340
+ error: error.message,
115
341
  };
116
342
  }
117
- // Get stack status
118
- const stackArn = config.quilt.stackArn;
119
- const region = config.deployment.region;
120
- console.log(`Stack: ${chalk_1.default.cyan(stackArn.match(/stack\/([^/]+)\//)?.[1] || stackArn)}`);
121
- console.log(`Region: ${chalk_1.default.cyan(region)}\n`);
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
- // Display status
128
- console.log(chalk_1.default.bold("Stack Status:"));
129
- console.log(` ${formatStackStatus(result.stackStatus)}`);
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
- console.log(chalk_1.default.bold("BenchlingIntegration:"));
132
- if (result.benchlingIntegrationEnabled) {
133
- console.log(chalk_1.default.green(" ✓ Enabled"));
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
- else {
136
- console.log(chalk_1.default.yellow(" ⚠ Disabled"));
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
- if (result.lastUpdateTime) {
140
- console.log(chalk_1.default.bold("Last Updated:"));
141
- console.log(` ${chalk_1.default.dim(result.lastUpdateTime)}`);
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(" Run this command again in a few minutes to check progress\n"));
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
- return result;
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