cloudops-cli 0.1.0
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 +132 -0
- package/dist/api/api-client.d.ts +78 -0
- package/dist/api/api-client.d.ts.map +1 -0
- package/dist/api/api-client.js +197 -0
- package/dist/api/api-client.js.map +1 -0
- package/dist/api/index.d.ts +3 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +8 -0
- package/dist/api/index.js.map +1 -0
- package/dist/auth/__mocks__/auth-handler.d.ts +27 -0
- package/dist/auth/__mocks__/auth-handler.d.ts.map +1 -0
- package/dist/auth/__mocks__/auth-handler.js +24 -0
- package/dist/auth/__mocks__/auth-handler.js.map +1 -0
- package/dist/auth/auth-handler.d.ts +80 -0
- package/dist/auth/auth-handler.d.ts.map +1 -0
- package/dist/auth/auth-handler.js +266 -0
- package/dist/auth/auth-handler.js.map +1 -0
- package/dist/auth/callback-server.d.ts +31 -0
- package/dist/auth/callback-server.d.ts.map +1 -0
- package/dist/auth/callback-server.js +143 -0
- package/dist/auth/callback-server.js.map +1 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +8 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +422 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/accounts.d.ts +15 -0
- package/dist/commands/accounts.d.ts.map +1 -0
- package/dist/commands/accounts.js +307 -0
- package/dist/commands/accounts.js.map +1 -0
- package/dist/commands/audit.d.ts +23 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +348 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/config.d.ts +15 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +148 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/incident.d.ts +59 -0
- package/dist/commands/incident.d.ts.map +1 -0
- package/dist/commands/incident.js +1032 -0
- package/dist/commands/incident.js.map +1 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +300 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/investigate.d.ts +15 -0
- package/dist/commands/investigate.d.ts.map +1 -0
- package/dist/commands/investigate.js +65 -0
- package/dist/commands/investigate.js.map +1 -0
- package/dist/commands/runbook.d.ts +20 -0
- package/dist/commands/runbook.d.ts.map +1 -0
- package/dist/commands/runbook.js +265 -0
- package/dist/commands/runbook.js.map +1 -0
- package/dist/config/__mocks__/config-handler.d.ts +11 -0
- package/dist/config/__mocks__/config-handler.d.ts.map +1 -0
- package/dist/config/__mocks__/config-handler.js +31 -0
- package/dist/config/__mocks__/config-handler.js.map +1 -0
- package/dist/config/certificate-pinning.d.ts +68 -0
- package/dist/config/certificate-pinning.d.ts.map +1 -0
- package/dist/config/certificate-pinning.js +249 -0
- package/dist/config/certificate-pinning.js.map +1 -0
- package/dist/config/config-handler.d.ts +45 -0
- package/dist/config/config-handler.d.ts.map +1 -0
- package/dist/config/config-handler.js +149 -0
- package/dist/config/config-handler.js.map +1 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +7 -0
- package/dist/config/index.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/types/cli-options.d.ts +31 -0
- package/dist/types/cli-options.d.ts.map +1 -0
- package/dist/types/cli-options.js +3 -0
- package/dist/types/cli-options.js.map +1 -0
- package/dist/utils/output-formatter.d.ts +67 -0
- package/dist/utils/output-formatter.d.ts.map +1 -0
- package/dist/utils/output-formatter.js +147 -0
- package/dist/utils/output-formatter.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,1032 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.incidentStatusCommand = incidentStatusCommand;
|
|
40
|
+
exports.incidentContextCommand = incidentContextCommand;
|
|
41
|
+
exports.incidentDiagnoseCommand = incidentDiagnoseCommand;
|
|
42
|
+
exports.incidentAskCommand = incidentAskCommand;
|
|
43
|
+
exports.incidentRemediateCommand = incidentRemediateCommand;
|
|
44
|
+
exports.incidentPlanShowCommand = incidentPlanShowCommand;
|
|
45
|
+
exports.incidentPlanEditCommand = incidentPlanEditCommand;
|
|
46
|
+
exports.incidentExecuteCommand = incidentExecuteCommand;
|
|
47
|
+
exports.incidentListCommand = incidentListCommand;
|
|
48
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
49
|
+
const cli_table3_1 = __importDefault(require("cli-table3"));
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
const api_client_1 = require("../api/api-client");
|
|
53
|
+
/**
|
|
54
|
+
* Get incident status
|
|
55
|
+
* Displays current status and metadata for an incident
|
|
56
|
+
*/
|
|
57
|
+
async function incidentStatusCommand(incidentId, _options) {
|
|
58
|
+
if (!incidentId || incidentId.trim().length === 0) {
|
|
59
|
+
console.error(chalk_1.default.red('Error: Incident ID is required'));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
console.log(chalk_1.default.bold.blue(`\n📊 Fetching incident status...\n`));
|
|
63
|
+
try {
|
|
64
|
+
const apiClient = (0, api_client_1.createAPIClient)();
|
|
65
|
+
const incident = await apiClient.get(`/api/v1/incidents/${incidentId}`);
|
|
66
|
+
// Display incident details
|
|
67
|
+
console.log(chalk_1.default.green('✓ Incident found\n'));
|
|
68
|
+
const table = new cli_table3_1.default({
|
|
69
|
+
colWidths: [25, 55],
|
|
70
|
+
wordWrap: true,
|
|
71
|
+
});
|
|
72
|
+
table.push([chalk_1.default.bold('Incident ID'), chalk_1.default.cyan(incident.incident_id)], [chalk_1.default.bold('Description'), incident.description], [chalk_1.default.bold('Status'), formatStatus(incident.status)], [chalk_1.default.bold('Severity'), formatSeverity(incident.severity)], [chalk_1.default.bold('Created By'), incident.created_by], [chalk_1.default.bold('Created At'), new Date(incident.created_at).toLocaleString()], [chalk_1.default.bold('Updated At'), new Date(incident.updated_at).toLocaleString()]);
|
|
73
|
+
if (incident.context_gathered_at) {
|
|
74
|
+
table.push([
|
|
75
|
+
chalk_1.default.bold('Context Gathered'),
|
|
76
|
+
new Date(incident.context_gathered_at).toLocaleString(),
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
if (incident.diagnosed_at) {
|
|
80
|
+
table.push([
|
|
81
|
+
chalk_1.default.bold('Diagnosed At'),
|
|
82
|
+
new Date(incident.diagnosed_at).toLocaleString(),
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
if (incident.resolved_at) {
|
|
86
|
+
table.push([
|
|
87
|
+
chalk_1.default.bold('Resolved At'),
|
|
88
|
+
new Date(incident.resolved_at).toLocaleString(),
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
if (incident.tags && Object.keys(incident.tags).length > 0) {
|
|
92
|
+
table.push([
|
|
93
|
+
chalk_1.default.bold('Tags'),
|
|
94
|
+
Object.entries(incident.tags)
|
|
95
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
96
|
+
.join(', '),
|
|
97
|
+
]);
|
|
98
|
+
}
|
|
99
|
+
console.log(table.toString());
|
|
100
|
+
console.log();
|
|
101
|
+
// Show next steps based on status
|
|
102
|
+
showNextSteps(incident);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
handleAPIError(error, 'fetch incident status');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Get incident context
|
|
110
|
+
* Displays gathered context including metrics, logs, events, topology, and changes
|
|
111
|
+
*/
|
|
112
|
+
async function incidentContextCommand(incidentId, options) {
|
|
113
|
+
if (!incidentId || incidentId.trim().length === 0) {
|
|
114
|
+
console.error(chalk_1.default.red('Error: Incident ID is required'));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
console.log(chalk_1.default.bold.blue(`\n📦 Fetching incident context...\n`));
|
|
118
|
+
try {
|
|
119
|
+
const apiClient = (0, api_client_1.createAPIClient)();
|
|
120
|
+
const response = await apiClient.get(`/api/v1/incidents/${incidentId}/context`);
|
|
121
|
+
const context = response.context;
|
|
122
|
+
// Export to file if requested
|
|
123
|
+
if (options.export) {
|
|
124
|
+
exportContext(context, options.export, incidentId);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// Display context summary
|
|
128
|
+
console.log(chalk_1.default.green('✓ Context retrieved\n'));
|
|
129
|
+
const summaryTable = new cli_table3_1.default({
|
|
130
|
+
head: [chalk_1.default.bold('Category'), chalk_1.default.bold('Count'), chalk_1.default.bold('Details')],
|
|
131
|
+
colWidths: [20, 10, 50],
|
|
132
|
+
wordWrap: true,
|
|
133
|
+
});
|
|
134
|
+
summaryTable.push([
|
|
135
|
+
'Metrics',
|
|
136
|
+
context.metrics.length,
|
|
137
|
+
context.metrics.length > 0
|
|
138
|
+
? `Sources: ${[...new Set(context.metrics.map((m) => m.source))].join(', ')}`
|
|
139
|
+
: 'No metrics gathered',
|
|
140
|
+
], [
|
|
141
|
+
'Logs',
|
|
142
|
+
context.logs.length,
|
|
143
|
+
context.logs.length > 0
|
|
144
|
+
? `Levels: ${[...new Set(context.logs.map((l) => l.level))].join(', ')}`
|
|
145
|
+
: 'No logs gathered',
|
|
146
|
+
], [
|
|
147
|
+
'Events',
|
|
148
|
+
context.events.length,
|
|
149
|
+
context.events.length > 0
|
|
150
|
+
? `Types: ${[...new Set(context.events.map((e) => e.event_type))].slice(0, 3).join(', ')}`
|
|
151
|
+
: 'No events gathered',
|
|
152
|
+
], [
|
|
153
|
+
'Resources',
|
|
154
|
+
context.topology.resources.length,
|
|
155
|
+
context.topology.resources.length > 0
|
|
156
|
+
? `Providers: ${[...new Set(context.topology.resources.map((r) => r.provider))].join(', ')}`
|
|
157
|
+
: 'No resources mapped',
|
|
158
|
+
], [
|
|
159
|
+
'Dependencies',
|
|
160
|
+
context.topology.dependencies.length,
|
|
161
|
+
context.topology.dependencies.length > 0
|
|
162
|
+
? `Types: ${[...new Set(context.topology.dependencies.map((d) => d.type))].join(', ')}`
|
|
163
|
+
: 'No dependencies mapped',
|
|
164
|
+
], [
|
|
165
|
+
'Recent Changes',
|
|
166
|
+
context.recent_changes.length,
|
|
167
|
+
context.recent_changes.length > 0
|
|
168
|
+
? `Types: ${[...new Set(context.recent_changes.map((c) => c.type))].join(', ')}`
|
|
169
|
+
: 'No recent changes',
|
|
170
|
+
]);
|
|
171
|
+
console.log(summaryTable.toString());
|
|
172
|
+
console.log();
|
|
173
|
+
// Display metadata
|
|
174
|
+
console.log(chalk_1.default.bold('Context Metadata:'));
|
|
175
|
+
console.log(chalk_1.default.gray(` Gathered at: ${new Date(context.gathered_at).toLocaleString()}`));
|
|
176
|
+
console.log(chalk_1.default.gray(` Duration: ${context.duration_ms}ms`));
|
|
177
|
+
console.log();
|
|
178
|
+
// Show sample data if available
|
|
179
|
+
if (context.metrics.length > 0) {
|
|
180
|
+
console.log(chalk_1.default.bold('Sample Metrics:'));
|
|
181
|
+
context.metrics.slice(0, 3).forEach((metric) => {
|
|
182
|
+
console.log(chalk_1.default.gray(` • ${metric.metric_name} (${metric.resource})`));
|
|
183
|
+
console.log(chalk_1.default.gray(` Range: ${metric.time_range.start} to ${metric.time_range.end}`));
|
|
184
|
+
console.log(chalk_1.default.gray(` Stats: min=${metric.statistics.min}, max=${metric.statistics.max}, avg=${metric.statistics.avg.toFixed(2)}`));
|
|
185
|
+
});
|
|
186
|
+
if (context.metrics.length > 3) {
|
|
187
|
+
console.log(chalk_1.default.gray(` ... and ${context.metrics.length - 3} more`));
|
|
188
|
+
}
|
|
189
|
+
console.log();
|
|
190
|
+
}
|
|
191
|
+
if (context.logs.length > 0) {
|
|
192
|
+
console.log(chalk_1.default.bold('Recent Logs:'));
|
|
193
|
+
context.logs.slice(0, 5).forEach((log) => {
|
|
194
|
+
const levelColor = getLevelColor(log.level);
|
|
195
|
+
console.log(chalk_1.default.gray(` [${new Date(log.timestamp).toLocaleTimeString()}] `) +
|
|
196
|
+
levelColor(log.level.toUpperCase()) +
|
|
197
|
+
chalk_1.default.gray(` ${log.message.substring(0, 80)}${log.message.length > 80 ? '...' : ''}`));
|
|
198
|
+
});
|
|
199
|
+
if (context.logs.length > 5) {
|
|
200
|
+
console.log(chalk_1.default.gray(` ... and ${context.logs.length - 5} more`));
|
|
201
|
+
}
|
|
202
|
+
console.log();
|
|
203
|
+
}
|
|
204
|
+
if (context.recent_changes.length > 0) {
|
|
205
|
+
console.log(chalk_1.default.bold('Recent Changes:'));
|
|
206
|
+
context.recent_changes.slice(0, 3).forEach((change) => {
|
|
207
|
+
console.log(chalk_1.default.gray(` • [${new Date(change.timestamp).toLocaleString()}] `) +
|
|
208
|
+
chalk_1.default.yellow(change.type) +
|
|
209
|
+
chalk_1.default.gray(` on ${change.resource}`));
|
|
210
|
+
console.log(chalk_1.default.gray(` ${change.description}`));
|
|
211
|
+
});
|
|
212
|
+
if (context.recent_changes.length > 3) {
|
|
213
|
+
console.log(chalk_1.default.gray(` ... and ${context.recent_changes.length - 3} more`));
|
|
214
|
+
}
|
|
215
|
+
console.log();
|
|
216
|
+
}
|
|
217
|
+
// Show export option
|
|
218
|
+
console.log(chalk_1.default.bold('Export Options:'));
|
|
219
|
+
console.log(chalk_1.default.gray(` Export full context: cloudops incident ${incidentId} context --export <file>`));
|
|
220
|
+
console.log();
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
handleAPIError(error, 'fetch incident context');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Export context to file
|
|
228
|
+
*/
|
|
229
|
+
function exportContext(context, filePath, _incidentId) {
|
|
230
|
+
try {
|
|
231
|
+
// Resolve file path
|
|
232
|
+
const resolvedPath = path.resolve(filePath);
|
|
233
|
+
const dir = path.dirname(resolvedPath);
|
|
234
|
+
// Ensure directory exists
|
|
235
|
+
if (!fs.existsSync(dir)) {
|
|
236
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
237
|
+
}
|
|
238
|
+
// Write context to file
|
|
239
|
+
fs.writeFileSync(resolvedPath, JSON.stringify(context, null, 2), 'utf-8');
|
|
240
|
+
console.log(chalk_1.default.green(`✓ Context exported successfully\n`));
|
|
241
|
+
console.log(chalk_1.default.bold('File:'), chalk_1.default.cyan(resolvedPath));
|
|
242
|
+
console.log(chalk_1.default.gray('Size:'), `${(fs.statSync(resolvedPath).size / 1024).toFixed(2)} KB`);
|
|
243
|
+
console.log();
|
|
244
|
+
console.log(chalk_1.default.bold('Context Summary:'));
|
|
245
|
+
console.log(chalk_1.default.gray(` Metrics: ${context.metrics.length}`));
|
|
246
|
+
console.log(chalk_1.default.gray(` Logs: ${context.logs.length}`));
|
|
247
|
+
console.log(chalk_1.default.gray(` Events: ${context.events.length}`));
|
|
248
|
+
console.log(chalk_1.default.gray(` Resources: ${context.topology.resources.length}`));
|
|
249
|
+
console.log(chalk_1.default.gray(` Dependencies: ${context.topology.dependencies.length}`));
|
|
250
|
+
console.log(chalk_1.default.gray(` Recent Changes: ${context.recent_changes.length}`));
|
|
251
|
+
console.log();
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
console.error(chalk_1.default.red('\n✗ Failed to export context:'));
|
|
255
|
+
console.error(chalk_1.default.red(` ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Format incident status with color
|
|
261
|
+
*/
|
|
262
|
+
function formatStatus(status) {
|
|
263
|
+
const statusColors = {
|
|
264
|
+
created: chalk_1.default.gray,
|
|
265
|
+
gathering_context: chalk_1.default.blue,
|
|
266
|
+
context_ready: chalk_1.default.cyan,
|
|
267
|
+
diagnosing: chalk_1.default.blue,
|
|
268
|
+
diagnosed: chalk_1.default.cyan,
|
|
269
|
+
planning_remediation: chalk_1.default.blue,
|
|
270
|
+
plan_ready: chalk_1.default.cyan,
|
|
271
|
+
awaiting_approval: chalk_1.default.yellow,
|
|
272
|
+
approved: chalk_1.default.green,
|
|
273
|
+
executing: chalk_1.default.blue,
|
|
274
|
+
resolved: chalk_1.default.green,
|
|
275
|
+
failed: chalk_1.default.red,
|
|
276
|
+
};
|
|
277
|
+
const colorFn = statusColors[status] || chalk_1.default.white;
|
|
278
|
+
return colorFn(status.replace(/_/g, ' ').toUpperCase());
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Format severity with color
|
|
282
|
+
*/
|
|
283
|
+
function formatSeverity(severity) {
|
|
284
|
+
const severityColors = {
|
|
285
|
+
low: chalk_1.default.gray,
|
|
286
|
+
medium: chalk_1.default.yellow,
|
|
287
|
+
high: chalk_1.default.red,
|
|
288
|
+
critical: chalk_1.default.red.bold,
|
|
289
|
+
};
|
|
290
|
+
const colorFn = severityColors[severity] || chalk_1.default.white;
|
|
291
|
+
return colorFn(severity.toUpperCase());
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Get color function for log level
|
|
295
|
+
*/
|
|
296
|
+
function getLevelColor(level) {
|
|
297
|
+
const levelColors = {
|
|
298
|
+
debug: chalk_1.default.gray,
|
|
299
|
+
info: chalk_1.default.blue,
|
|
300
|
+
warn: chalk_1.default.yellow,
|
|
301
|
+
error: chalk_1.default.red,
|
|
302
|
+
fatal: chalk_1.default.red.bold,
|
|
303
|
+
};
|
|
304
|
+
return levelColors[level] || chalk_1.default.white;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Show next steps based on incident status
|
|
308
|
+
*/
|
|
309
|
+
function showNextSteps(incident) {
|
|
310
|
+
console.log(chalk_1.default.bold('Next Steps:'));
|
|
311
|
+
switch (incident.status) {
|
|
312
|
+
case 'created':
|
|
313
|
+
case 'gathering_context':
|
|
314
|
+
console.log(chalk_1.default.gray(` • Wait for context gathering to complete (usually < 60 seconds)`));
|
|
315
|
+
console.log(chalk_1.default.gray(` • Check status: cloudops incident ${incident.incident_id} status`));
|
|
316
|
+
break;
|
|
317
|
+
case 'context_ready':
|
|
318
|
+
console.log(chalk_1.default.gray(` • View context: cloudops incident ${incident.incident_id} context`));
|
|
319
|
+
console.log(chalk_1.default.gray(` • Start diagnosis: cloudops incident ${incident.incident_id} diagnose`));
|
|
320
|
+
break;
|
|
321
|
+
case 'diagnosing':
|
|
322
|
+
console.log(chalk_1.default.gray(` • Wait for AI diagnosis to complete (usually < 30 seconds)`));
|
|
323
|
+
console.log(chalk_1.default.gray(` • Check status: cloudops incident ${incident.incident_id} status`));
|
|
324
|
+
break;
|
|
325
|
+
case 'diagnosed':
|
|
326
|
+
console.log(chalk_1.default.gray(` • View diagnosis: cloudops incident ${incident.incident_id} diagnose`));
|
|
327
|
+
console.log(chalk_1.default.gray(` • Ask questions: cloudops incident ${incident.incident_id} ask "<question>"`));
|
|
328
|
+
console.log(chalk_1.default.gray(` • Generate plan: cloudops incident ${incident.incident_id} remediate`));
|
|
329
|
+
break;
|
|
330
|
+
case 'planning_remediation':
|
|
331
|
+
console.log(chalk_1.default.gray(` • Wait for remediation plan generation`));
|
|
332
|
+
break;
|
|
333
|
+
case 'plan_ready':
|
|
334
|
+
console.log(chalk_1.default.gray(` • Review plan: cloudops incident ${incident.incident_id} plan show`));
|
|
335
|
+
console.log(chalk_1.default.gray(` • Execute plan: cloudops incident ${incident.incident_id} execute`));
|
|
336
|
+
break;
|
|
337
|
+
case 'awaiting_approval':
|
|
338
|
+
console.log(chalk_1.default.gray(` • Waiting for approval from designated approvers`));
|
|
339
|
+
console.log(chalk_1.default.gray(` • Check status: cloudops incident ${incident.incident_id} status`));
|
|
340
|
+
break;
|
|
341
|
+
case 'approved':
|
|
342
|
+
case 'executing':
|
|
343
|
+
console.log(chalk_1.default.gray(` • Execution in progress via GitOps workflow`));
|
|
344
|
+
console.log(chalk_1.default.gray(` • Check status: cloudops incident ${incident.incident_id} status`));
|
|
345
|
+
break;
|
|
346
|
+
case 'resolved':
|
|
347
|
+
console.log(chalk_1.default.green(` ✓ Incident resolved successfully`));
|
|
348
|
+
break;
|
|
349
|
+
case 'failed':
|
|
350
|
+
console.log(chalk_1.default.red(` ✗ Incident resolution failed`));
|
|
351
|
+
console.log(chalk_1.default.gray(` • Review context: cloudops incident ${incident.incident_id} context`));
|
|
352
|
+
console.log(chalk_1.default.gray(` • Check diagnosis: cloudops incident ${incident.incident_id} diagnose`));
|
|
353
|
+
break;
|
|
354
|
+
default:
|
|
355
|
+
console.log(chalk_1.default.gray(` • Check status: cloudops incident ${incident.incident_id} status`));
|
|
356
|
+
}
|
|
357
|
+
console.log();
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Handle API errors consistently
|
|
361
|
+
*/
|
|
362
|
+
function handleAPIError(error, action) {
|
|
363
|
+
if (error instanceof api_client_1.APIError) {
|
|
364
|
+
console.error(chalk_1.default.red(`\n✗ Failed to ${action}:`));
|
|
365
|
+
console.error(chalk_1.default.red(` ${error.message}`));
|
|
366
|
+
if (error.statusCode === 401) {
|
|
367
|
+
console.error(chalk_1.default.yellow('\nAuthentication required. Please run: cloudops init'));
|
|
368
|
+
}
|
|
369
|
+
else if (error.statusCode === 403) {
|
|
370
|
+
console.error(chalk_1.default.yellow('\nAccess denied. Check your permissions.'));
|
|
371
|
+
}
|
|
372
|
+
else if (error.statusCode === 404) {
|
|
373
|
+
console.error(chalk_1.default.yellow('\nIncident not found. Check the incident ID.'));
|
|
374
|
+
}
|
|
375
|
+
else if (error.details) {
|
|
376
|
+
console.error(chalk_1.default.gray('\nDetails:'), error.details);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
console.error(chalk_1.default.red(`\n✗ Unexpected error:`));
|
|
381
|
+
console.error(chalk_1.default.red(` ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
382
|
+
}
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Get incident diagnosis
|
|
387
|
+
* Displays AI-generated root cause analysis with evidence
|
|
388
|
+
*/
|
|
389
|
+
async function incidentDiagnoseCommand(incidentId, options) {
|
|
390
|
+
if (!incidentId || incidentId.trim().length === 0) {
|
|
391
|
+
console.error(chalk_1.default.red('Error: Incident ID is required'));
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
console.log(chalk_1.default.bold.blue(`\n🔍 Fetching incident diagnosis...\n`));
|
|
395
|
+
try {
|
|
396
|
+
const apiClient = (0, api_client_1.createAPIClient)();
|
|
397
|
+
// Build request body
|
|
398
|
+
const requestBody = {};
|
|
399
|
+
if (options.useProfile) {
|
|
400
|
+
requestBody.use_profile = options.useProfile;
|
|
401
|
+
}
|
|
402
|
+
// Call diagnose endpoint
|
|
403
|
+
const response = await apiClient.post(`/api/v1/incidents/${incidentId}/diagnose`, requestBody);
|
|
404
|
+
const diagnosis = response.diagnosis;
|
|
405
|
+
// Display diagnosis header
|
|
406
|
+
console.log(chalk_1.default.green('✓ Diagnosis completed\n'));
|
|
407
|
+
// Display metadata
|
|
408
|
+
const metadataTable = new cli_table3_1.default({
|
|
409
|
+
colWidths: [25, 55],
|
|
410
|
+
wordWrap: true,
|
|
411
|
+
});
|
|
412
|
+
metadataTable.push([chalk_1.default.bold('Generated At'), new Date(diagnosis.generated_at).toLocaleString()], [chalk_1.default.bold('Model Used'), diagnosis.model_used], [chalk_1.default.bold('Overall Confidence'), formatConfidence(diagnosis.confidence)], [chalk_1.default.bold('Root Causes Found'), diagnosis.root_causes.length.toString()]);
|
|
413
|
+
console.log(metadataTable.toString());
|
|
414
|
+
console.log();
|
|
415
|
+
// Display root causes
|
|
416
|
+
if (diagnosis.root_causes.length === 0) {
|
|
417
|
+
console.log(chalk_1.default.yellow('⚠ No root causes identified'));
|
|
418
|
+
console.log(chalk_1.default.gray(' The AI was unable to determine a probable root cause.'));
|
|
419
|
+
console.log(chalk_1.default.gray(' Try gathering more context or asking specific questions.'));
|
|
420
|
+
console.log();
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
console.log(chalk_1.default.bold('Root Cause Analysis:\n'));
|
|
424
|
+
diagnosis.root_causes.forEach((rootCause, index) => {
|
|
425
|
+
// Root cause header
|
|
426
|
+
console.log(chalk_1.default.bold(`${index + 1}. `) +
|
|
427
|
+
chalk_1.default.cyan(rootCause.description) +
|
|
428
|
+
chalk_1.default.gray(` (${formatConfidence(rootCause.confidence)})`));
|
|
429
|
+
console.log();
|
|
430
|
+
// Reasoning
|
|
431
|
+
console.log(chalk_1.default.bold(' Reasoning:'));
|
|
432
|
+
console.log(chalk_1.default.gray(` ${rootCause.reasoning}`));
|
|
433
|
+
console.log();
|
|
434
|
+
// Evidence
|
|
435
|
+
if (rootCause.evidence.length > 0) {
|
|
436
|
+
console.log(chalk_1.default.bold(' Evidence:'));
|
|
437
|
+
rootCause.evidence.forEach((evidence, evidenceIndex) => {
|
|
438
|
+
const evidenceIcon = getEvidenceIcon(evidence.type);
|
|
439
|
+
console.log(chalk_1.default.gray(` ${evidenceIcon} [${evidence.type.toUpperCase()}] `) +
|
|
440
|
+
chalk_1.default.white(evidence.source));
|
|
441
|
+
console.log(chalk_1.default.gray(` Time: ${new Date(evidence.timestamp).toLocaleString()}`));
|
|
442
|
+
console.log(chalk_1.default.gray(` Relevance: ${evidence.relevance}`));
|
|
443
|
+
// Display evidence data summary
|
|
444
|
+
if (evidence.data) {
|
|
445
|
+
const dataSummary = formatEvidenceData(evidence.data, evidence.type);
|
|
446
|
+
if (dataSummary) {
|
|
447
|
+
console.log(chalk_1.default.gray(` Data: ${dataSummary}`));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (evidenceIndex < rootCause.evidence.length - 1) {
|
|
451
|
+
console.log();
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
console.log();
|
|
455
|
+
}
|
|
456
|
+
// Separator between root causes
|
|
457
|
+
if (index < diagnosis.root_causes.length - 1) {
|
|
458
|
+
console.log(chalk_1.default.gray(' ' + '─'.repeat(70)));
|
|
459
|
+
console.log();
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
// Show next steps
|
|
463
|
+
console.log(chalk_1.default.bold('Next Steps:'));
|
|
464
|
+
console.log(chalk_1.default.gray(` • Ask follow-up questions: cloudops incident ${incidentId} ask "<question>"`));
|
|
465
|
+
console.log(chalk_1.default.gray(` • Generate remediation plan: cloudops incident ${incidentId} remediate`));
|
|
466
|
+
console.log();
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
handleAPIError(error, 'fetch incident diagnosis');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Format confidence score as percentage with color
|
|
474
|
+
*/
|
|
475
|
+
function formatConfidence(confidence) {
|
|
476
|
+
const percentage = (confidence * 100).toFixed(0);
|
|
477
|
+
const percentageNum = parseInt(percentage);
|
|
478
|
+
if (percentageNum >= 80) {
|
|
479
|
+
return chalk_1.default.green(`${percentage}% confidence`);
|
|
480
|
+
}
|
|
481
|
+
else if (percentageNum >= 60) {
|
|
482
|
+
return chalk_1.default.yellow(`${percentage}% confidence`);
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
return chalk_1.default.red(`${percentage}% confidence`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Get icon for evidence type
|
|
490
|
+
*/
|
|
491
|
+
function getEvidenceIcon(type) {
|
|
492
|
+
const icons = {
|
|
493
|
+
metric: '📊',
|
|
494
|
+
log: '📝',
|
|
495
|
+
event: '⚡',
|
|
496
|
+
topology: '🗺️',
|
|
497
|
+
};
|
|
498
|
+
return icons[type] || '•';
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Format evidence data for display
|
|
502
|
+
*/
|
|
503
|
+
function formatEvidenceData(data, type) {
|
|
504
|
+
if (!data || typeof data !== 'object') {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
const dataObj = data;
|
|
509
|
+
switch (type) {
|
|
510
|
+
case 'metric':
|
|
511
|
+
if (dataObj.value !== undefined) {
|
|
512
|
+
return `value=${dataObj.value}${dataObj.unit ? ' ' + dataObj.unit : ''}`;
|
|
513
|
+
}
|
|
514
|
+
if (dataObj.min !== undefined && dataObj.max !== undefined) {
|
|
515
|
+
return `range=${dataObj.min}-${dataObj.max}${dataObj.unit ? ' ' + dataObj.unit : ''}`;
|
|
516
|
+
}
|
|
517
|
+
break;
|
|
518
|
+
case 'log':
|
|
519
|
+
if (dataObj.message) {
|
|
520
|
+
const message = dataObj.message.toString();
|
|
521
|
+
return message.length > 80 ? message.substring(0, 77) + '...' : message;
|
|
522
|
+
}
|
|
523
|
+
break;
|
|
524
|
+
case 'event':
|
|
525
|
+
if (dataObj.event_type) {
|
|
526
|
+
return `type=${dataObj.event_type}`;
|
|
527
|
+
}
|
|
528
|
+
break;
|
|
529
|
+
case 'topology':
|
|
530
|
+
if (dataObj.resource_type) {
|
|
531
|
+
return `resource=${dataObj.resource_type}`;
|
|
532
|
+
}
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
// Fallback: show first few keys
|
|
536
|
+
const keys = Object.keys(dataObj).slice(0, 3);
|
|
537
|
+
if (keys.length > 0) {
|
|
538
|
+
return keys.map((k) => `${k}=${JSON.stringify(dataObj[k])}`).join(', ');
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
// Ignore formatting errors
|
|
543
|
+
}
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Ask a follow-up question about an incident
|
|
548
|
+
* Displays AI response based on existing context and conversation history
|
|
549
|
+
*/
|
|
550
|
+
async function incidentAskCommand(incidentId, question, options) {
|
|
551
|
+
if (!incidentId || incidentId.trim().length === 0) {
|
|
552
|
+
console.error(chalk_1.default.red('Error: Incident ID is required'));
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
if (!question || question.trim().length === 0) {
|
|
556
|
+
console.error(chalk_1.default.red('Error: Question is required'));
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
console.log(chalk_1.default.bold.blue(`\n💬 Asking question about incident...\n`));
|
|
560
|
+
console.log(chalk_1.default.gray(`Question: ${question}`));
|
|
561
|
+
console.log();
|
|
562
|
+
try {
|
|
563
|
+
const apiClient = (0, api_client_1.createAPIClient)();
|
|
564
|
+
// Build request body
|
|
565
|
+
const requestBody = {
|
|
566
|
+
question,
|
|
567
|
+
};
|
|
568
|
+
if (options.useProfile) {
|
|
569
|
+
requestBody.use_profile = options.useProfile;
|
|
570
|
+
}
|
|
571
|
+
// Call ask endpoint
|
|
572
|
+
const response = await apiClient.post(`/api/v1/incidents/${incidentId}/ask`, requestBody);
|
|
573
|
+
// Display response
|
|
574
|
+
console.log(chalk_1.default.green('✓ Response received\n'));
|
|
575
|
+
// Display metadata
|
|
576
|
+
const metadataTable = new cli_table3_1.default({
|
|
577
|
+
colWidths: [25, 55],
|
|
578
|
+
wordWrap: true,
|
|
579
|
+
});
|
|
580
|
+
metadataTable.push([chalk_1.default.bold('Model Used'), response.model_used], [chalk_1.default.bold('Timestamp'), new Date(response.timestamp).toLocaleString()]);
|
|
581
|
+
console.log(metadataTable.toString());
|
|
582
|
+
console.log();
|
|
583
|
+
// Display answer
|
|
584
|
+
console.log(chalk_1.default.bold('Answer:'));
|
|
585
|
+
console.log();
|
|
586
|
+
console.log(chalk_1.default.white(response.answer));
|
|
587
|
+
console.log();
|
|
588
|
+
// Show next steps
|
|
589
|
+
console.log(chalk_1.default.bold('Next Steps:'));
|
|
590
|
+
console.log(chalk_1.default.gray(` • Ask another question: cloudops incident ${incidentId} ask "<question>"`));
|
|
591
|
+
console.log(chalk_1.default.gray(` • Generate remediation plan: cloudops incident ${incidentId} remediate`));
|
|
592
|
+
console.log();
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
handleAPIError(error, 'ask question');
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Generate remediation plan for an incident
|
|
600
|
+
* Displays remediation plan with risk assessment
|
|
601
|
+
*/
|
|
602
|
+
async function incidentRemediateCommand(incidentId, options) {
|
|
603
|
+
if (!incidentId || incidentId.trim().length === 0) {
|
|
604
|
+
console.error(chalk_1.default.red('Error: Incident ID is required'));
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
console.log(chalk_1.default.bold.blue(`\n🔧 Generating remediation plan...\n`));
|
|
608
|
+
try {
|
|
609
|
+
const apiClient = (0, api_client_1.createAPIClient)();
|
|
610
|
+
// Build request body
|
|
611
|
+
const requestBody = {};
|
|
612
|
+
if (options.useProfile) {
|
|
613
|
+
requestBody.use_profile = options.useProfile;
|
|
614
|
+
}
|
|
615
|
+
if (options.iacTool) {
|
|
616
|
+
requestBody.iac_tool = options.iacTool;
|
|
617
|
+
}
|
|
618
|
+
// Call remediate endpoint
|
|
619
|
+
const response = await apiClient.post(`/api/v1/incidents/${incidentId}/remediate`, requestBody);
|
|
620
|
+
const plan = response.plan;
|
|
621
|
+
// Display plan header
|
|
622
|
+
console.log(chalk_1.default.green('✓ Remediation plan generated\n'));
|
|
623
|
+
// Display plan metadata
|
|
624
|
+
const metadataTable = new cli_table3_1.default({
|
|
625
|
+
colWidths: [25, 55],
|
|
626
|
+
wordWrap: true,
|
|
627
|
+
});
|
|
628
|
+
metadataTable.push([chalk_1.default.bold('Plan ID'), chalk_1.default.cyan(plan.plan_id)], [chalk_1.default.bold('Generated At'), new Date(plan.generated_at).toLocaleString()], [chalk_1.default.bold('Total Steps'), plan.steps.length.toString()], [chalk_1.default.bold('Estimated Duration'), plan.estimated_duration], [chalk_1.default.bold('Total Risk Score'), formatRiskScore(plan.total_risk_score)], [chalk_1.default.bold('Requires Approval'), plan.requires_approval ? chalk_1.default.yellow('YES') : chalk_1.default.green('NO')]);
|
|
629
|
+
console.log(metadataTable.toString());
|
|
630
|
+
console.log();
|
|
631
|
+
// Display approval rules if required
|
|
632
|
+
if (plan.requires_approval && plan.approval_rules.length > 0) {
|
|
633
|
+
console.log(chalk_1.default.bold('Approval Requirements:\n'));
|
|
634
|
+
plan.approval_rules.forEach((rule) => {
|
|
635
|
+
console.log(chalk_1.default.yellow(` Level ${rule.level}:`));
|
|
636
|
+
console.log(chalk_1.default.gray(` Approvers: ${rule.approvers.join(', ')}`));
|
|
637
|
+
console.log(chalk_1.default.gray(` Timeout: ${rule.timeout}`));
|
|
638
|
+
if (rule.escalation_policy) {
|
|
639
|
+
console.log(chalk_1.default.gray(` Escalation: ${rule.escalation_policy}`));
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
console.log();
|
|
643
|
+
}
|
|
644
|
+
// Display remediation steps
|
|
645
|
+
if (plan.steps.length === 0) {
|
|
646
|
+
console.log(chalk_1.default.yellow('⚠ No remediation steps generated'));
|
|
647
|
+
console.log(chalk_1.default.gray(' The AI was unable to generate a remediation plan.'));
|
|
648
|
+
console.log();
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
console.log(chalk_1.default.bold('Remediation Steps:\n'));
|
|
652
|
+
plan.steps.forEach((step) => {
|
|
653
|
+
// Step header
|
|
654
|
+
console.log(chalk_1.default.bold(`Step ${step.step_number}: `) +
|
|
655
|
+
chalk_1.default.cyan(step.action) +
|
|
656
|
+
chalk_1.default.gray(` [${formatRiskLevel(step.risk_level)}]`));
|
|
657
|
+
console.log();
|
|
658
|
+
// Description
|
|
659
|
+
console.log(chalk_1.default.gray(` ${step.description}`));
|
|
660
|
+
console.log();
|
|
661
|
+
// Risk assessment
|
|
662
|
+
console.log(chalk_1.default.bold(' Risk Assessment:'));
|
|
663
|
+
console.log(chalk_1.default.gray(` Risk Level: ${formatRiskLevel(step.risk_level)}`));
|
|
664
|
+
console.log(chalk_1.default.gray(` Blast Radius: ${step.blast_radius.scope.replace(/_/g, ' ')}`));
|
|
665
|
+
console.log(chalk_1.default.gray(` Affected Resources: ${step.blast_radius.affected_resources.length}`));
|
|
666
|
+
if (step.blast_radius.affected_users > 0) {
|
|
667
|
+
console.log(chalk_1.default.gray(` Affected Users: ${step.blast_radius.affected_users}`));
|
|
668
|
+
}
|
|
669
|
+
console.log(chalk_1.default.gray(` Downtime Risk: ${step.blast_radius.downtime_risk ? chalk_1.default.red('YES') : chalk_1.default.green('NO')}`));
|
|
670
|
+
console.log();
|
|
671
|
+
// IaC details
|
|
672
|
+
console.log(chalk_1.default.bold(' Infrastructure as Code:'));
|
|
673
|
+
console.log(chalk_1.default.gray(` Tool: ${step.iac_tool.toUpperCase()}`));
|
|
674
|
+
console.log(chalk_1.default.gray(` Code Preview:`));
|
|
675
|
+
const codeLines = step.iac_code.split('\n').slice(0, 5);
|
|
676
|
+
codeLines.forEach((line) => {
|
|
677
|
+
console.log(chalk_1.default.gray(` ${line}`));
|
|
678
|
+
});
|
|
679
|
+
if (step.iac_code.split('\n').length > 5) {
|
|
680
|
+
console.log(chalk_1.default.gray(` ... (${step.iac_code.split('\n').length - 5} more lines)`));
|
|
681
|
+
}
|
|
682
|
+
console.log();
|
|
683
|
+
// Verification steps
|
|
684
|
+
if (step.verification_steps.length > 0) {
|
|
685
|
+
console.log(chalk_1.default.bold(' Verification Steps:'));
|
|
686
|
+
step.verification_steps.forEach((verifyStep, idx) => {
|
|
687
|
+
console.log(chalk_1.default.gray(` ${idx + 1}. ${verifyStep}`));
|
|
688
|
+
});
|
|
689
|
+
console.log();
|
|
690
|
+
}
|
|
691
|
+
// Rollback plan
|
|
692
|
+
console.log(chalk_1.default.bold(' Rollback Plan:'));
|
|
693
|
+
console.log(chalk_1.default.gray(` ${step.rollback_plan}`));
|
|
694
|
+
console.log();
|
|
695
|
+
// Estimated duration
|
|
696
|
+
console.log(chalk_1.default.bold(' Estimated Duration:'), chalk_1.default.gray(step.estimated_duration));
|
|
697
|
+
console.log();
|
|
698
|
+
// Separator between steps
|
|
699
|
+
if (step.step_number < plan.steps.length) {
|
|
700
|
+
console.log(chalk_1.default.gray(' ' + '─'.repeat(70)));
|
|
701
|
+
console.log();
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
// Show next steps
|
|
705
|
+
console.log(chalk_1.default.bold('Next Steps:'));
|
|
706
|
+
console.log(chalk_1.default.gray(` • Review plan: cloudops incident ${incidentId} plan show`));
|
|
707
|
+
console.log(chalk_1.default.gray(` • Edit plan: cloudops incident ${incidentId} plan edit`));
|
|
708
|
+
console.log(chalk_1.default.gray(` • Execute plan: cloudops incident ${incidentId} execute`));
|
|
709
|
+
console.log();
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
handleAPIError(error, 'generate remediation plan');
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Show remediation plan for an incident
|
|
717
|
+
*/
|
|
718
|
+
async function incidentPlanShowCommand(incidentId, _options) {
|
|
719
|
+
if (!incidentId || incidentId.trim().length === 0) {
|
|
720
|
+
console.error(chalk_1.default.red('Error: Incident ID is required'));
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
console.log(chalk_1.default.bold.blue(`\n📋 Fetching remediation plan...\n`));
|
|
724
|
+
try {
|
|
725
|
+
const apiClient = (0, api_client_1.createAPIClient)();
|
|
726
|
+
// Call plan get endpoint
|
|
727
|
+
const response = await apiClient.get(`/api/v1/incidents/${incidentId}/plan`);
|
|
728
|
+
const plan = response.plan;
|
|
729
|
+
// Display plan (reuse the display logic from remediate command)
|
|
730
|
+
console.log(chalk_1.default.green('✓ Remediation plan retrieved\n'));
|
|
731
|
+
// Display plan metadata
|
|
732
|
+
const metadataTable = new cli_table3_1.default({
|
|
733
|
+
colWidths: [25, 55],
|
|
734
|
+
wordWrap: true,
|
|
735
|
+
});
|
|
736
|
+
metadataTable.push([chalk_1.default.bold('Plan ID'), chalk_1.default.cyan(plan.plan_id)], [chalk_1.default.bold('Generated At'), new Date(plan.generated_at).toLocaleString()], [chalk_1.default.bold('Total Steps'), plan.steps.length.toString()], [chalk_1.default.bold('Estimated Duration'), plan.estimated_duration], [chalk_1.default.bold('Total Risk Score'), formatRiskScore(plan.total_risk_score)], [chalk_1.default.bold('Requires Approval'), plan.requires_approval ? chalk_1.default.yellow('YES') : chalk_1.default.green('NO')]);
|
|
737
|
+
console.log(metadataTable.toString());
|
|
738
|
+
console.log();
|
|
739
|
+
// Display approval rules if required
|
|
740
|
+
if (plan.requires_approval && plan.approval_rules.length > 0) {
|
|
741
|
+
console.log(chalk_1.default.bold('Approval Requirements:\n'));
|
|
742
|
+
plan.approval_rules.forEach((rule) => {
|
|
743
|
+
console.log(chalk_1.default.yellow(` Level ${rule.level}:`));
|
|
744
|
+
console.log(chalk_1.default.gray(` Approvers: ${rule.approvers.join(', ')}`));
|
|
745
|
+
console.log(chalk_1.default.gray(` Timeout: ${rule.timeout}`));
|
|
746
|
+
if (rule.escalation_policy) {
|
|
747
|
+
console.log(chalk_1.default.gray(` Escalation: ${rule.escalation_policy}`));
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
console.log();
|
|
751
|
+
}
|
|
752
|
+
// Display remediation steps summary
|
|
753
|
+
if (plan.steps.length === 0) {
|
|
754
|
+
console.log(chalk_1.default.yellow('⚠ No remediation steps in plan'));
|
|
755
|
+
console.log();
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
console.log(chalk_1.default.bold('Remediation Steps Summary:\n'));
|
|
759
|
+
const stepsTable = new cli_table3_1.default({
|
|
760
|
+
head: [
|
|
761
|
+
chalk_1.default.bold('Step'),
|
|
762
|
+
chalk_1.default.bold('Action'),
|
|
763
|
+
chalk_1.default.bold('Risk'),
|
|
764
|
+
chalk_1.default.bold('Duration'),
|
|
765
|
+
],
|
|
766
|
+
colWidths: [8, 35, 15, 15],
|
|
767
|
+
wordWrap: true,
|
|
768
|
+
});
|
|
769
|
+
plan.steps.forEach((step) => {
|
|
770
|
+
stepsTable.push([
|
|
771
|
+
step.step_number.toString(),
|
|
772
|
+
step.action,
|
|
773
|
+
formatRiskLevel(step.risk_level),
|
|
774
|
+
step.estimated_duration,
|
|
775
|
+
]);
|
|
776
|
+
});
|
|
777
|
+
console.log(stepsTable.toString());
|
|
778
|
+
console.log();
|
|
779
|
+
// Show next steps
|
|
780
|
+
console.log(chalk_1.default.bold('Next Steps:'));
|
|
781
|
+
console.log(chalk_1.default.gray(` • Edit plan: cloudops incident ${incidentId} plan edit`));
|
|
782
|
+
console.log(chalk_1.default.gray(` • Execute plan: cloudops incident ${incidentId} execute`));
|
|
783
|
+
console.log();
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
handleAPIError(error, 'fetch remediation plan');
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Edit remediation plan for an incident
|
|
791
|
+
* Opens the plan in the default editor
|
|
792
|
+
*/
|
|
793
|
+
async function incidentPlanEditCommand(incidentId, _options) {
|
|
794
|
+
if (!incidentId || incidentId.trim().length === 0) {
|
|
795
|
+
console.error(chalk_1.default.red('Error: Incident ID is required'));
|
|
796
|
+
process.exit(1);
|
|
797
|
+
}
|
|
798
|
+
console.log(chalk_1.default.bold.blue(`\n✏️ Editing remediation plan...\n`));
|
|
799
|
+
try {
|
|
800
|
+
const apiClient = (0, api_client_1.createAPIClient)();
|
|
801
|
+
// Fetch current plan
|
|
802
|
+
const response = await apiClient.get(`/api/v1/incidents/${incidentId}/plan`);
|
|
803
|
+
const plan = response.plan;
|
|
804
|
+
// Create temporary file
|
|
805
|
+
const tmpDir = path.join(process.cwd(), '.cloudops-tmp');
|
|
806
|
+
if (!fs.existsSync(tmpDir)) {
|
|
807
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
808
|
+
}
|
|
809
|
+
const tmpFile = path.join(tmpDir, `plan-${incidentId}.json`);
|
|
810
|
+
fs.writeFileSync(tmpFile, JSON.stringify(plan, null, 2), 'utf-8');
|
|
811
|
+
console.log(chalk_1.default.green('✓ Plan saved to temporary file\n'));
|
|
812
|
+
console.log(chalk_1.default.bold('File:'), chalk_1.default.cyan(tmpFile));
|
|
813
|
+
console.log();
|
|
814
|
+
console.log(chalk_1.default.yellow('⚠ Manual editing not yet implemented'));
|
|
815
|
+
console.log(chalk_1.default.gray(' This feature will open the plan in your default editor.'));
|
|
816
|
+
console.log(chalk_1.default.gray(' After editing, the plan will be validated and saved.'));
|
|
817
|
+
console.log();
|
|
818
|
+
console.log(chalk_1.default.bold('Current Plan:'));
|
|
819
|
+
console.log(chalk_1.default.gray(` Plan ID: ${plan.plan_id}`));
|
|
820
|
+
console.log(chalk_1.default.gray(` Steps: ${plan.steps.length}`));
|
|
821
|
+
console.log(chalk_1.default.gray(` Risk Score: ${plan.total_risk_score}`));
|
|
822
|
+
console.log();
|
|
823
|
+
// TODO: Implement actual editing workflow
|
|
824
|
+
// 1. Open file in editor (using $EDITOR or default)
|
|
825
|
+
// 2. Wait for editor to close
|
|
826
|
+
// 3. Read modified file
|
|
827
|
+
// 4. Validate changes
|
|
828
|
+
// 5. Send PUT request to update plan
|
|
829
|
+
// 6. Display confirmation
|
|
830
|
+
console.log(chalk_1.default.bold('Next Steps:'));
|
|
831
|
+
console.log(chalk_1.default.gray(` • View plan: cloudops incident ${incidentId} plan show`));
|
|
832
|
+
console.log(chalk_1.default.gray(` • Execute plan: cloudops incident ${incidentId} execute`));
|
|
833
|
+
console.log();
|
|
834
|
+
}
|
|
835
|
+
catch (error) {
|
|
836
|
+
handleAPIError(error, 'edit remediation plan');
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Execute remediation plan for an incident
|
|
841
|
+
* Handles approval workflow and displays PR URL and pipeline status
|
|
842
|
+
*/
|
|
843
|
+
async function incidentExecuteCommand(incidentId, options) {
|
|
844
|
+
if (!incidentId || incidentId.trim().length === 0) {
|
|
845
|
+
console.error(chalk_1.default.red('Error: Incident ID is required'));
|
|
846
|
+
process.exit(1);
|
|
847
|
+
}
|
|
848
|
+
console.log(chalk_1.default.bold.blue(`\n🚀 Executing remediation plan...\n`));
|
|
849
|
+
try {
|
|
850
|
+
const apiClient = (0, api_client_1.createAPIClient)();
|
|
851
|
+
// First, get the plan to show what will be executed
|
|
852
|
+
const planResponse = await apiClient.get(`/api/v1/incidents/${incidentId}/plan`);
|
|
853
|
+
const plan = planResponse.plan;
|
|
854
|
+
// Display execution summary
|
|
855
|
+
console.log(chalk_1.default.bold('Execution Summary:\n'));
|
|
856
|
+
const summaryTable = new cli_table3_1.default({
|
|
857
|
+
colWidths: [25, 55],
|
|
858
|
+
wordWrap: true,
|
|
859
|
+
});
|
|
860
|
+
summaryTable.push([chalk_1.default.bold('Plan ID'), chalk_1.default.cyan(plan.plan_id)], [chalk_1.default.bold('Total Steps'), plan.steps.length.toString()], [chalk_1.default.bold('Estimated Duration'), plan.estimated_duration], [chalk_1.default.bold('Total Risk Score'), formatRiskScore(plan.total_risk_score)], [chalk_1.default.bold('Requires Approval'), plan.requires_approval ? chalk_1.default.yellow('YES') : chalk_1.default.green('NO')]);
|
|
861
|
+
console.log(summaryTable.toString());
|
|
862
|
+
console.log();
|
|
863
|
+
// Show approval requirements if needed
|
|
864
|
+
if (plan.requires_approval && !options.skipApproval) {
|
|
865
|
+
console.log(chalk_1.default.yellow('⚠ This plan requires approval before execution\n'));
|
|
866
|
+
plan.approval_rules.forEach((rule) => {
|
|
867
|
+
console.log(chalk_1.default.bold(` Approval Level ${rule.level}:`));
|
|
868
|
+
console.log(chalk_1.default.gray(` Approvers: ${rule.approvers.join(', ')}`));
|
|
869
|
+
console.log(chalk_1.default.gray(` Timeout: ${rule.timeout}`));
|
|
870
|
+
});
|
|
871
|
+
console.log();
|
|
872
|
+
}
|
|
873
|
+
// Build request body
|
|
874
|
+
const requestBody = {
|
|
875
|
+
plan_id: plan.plan_id,
|
|
876
|
+
};
|
|
877
|
+
if (options.skipApproval) {
|
|
878
|
+
requestBody.skip_approval = true;
|
|
879
|
+
}
|
|
880
|
+
// Call execute endpoint
|
|
881
|
+
const executeResponse = await apiClient.post(`/api/v1/incidents/${incidentId}/execute`, requestBody);
|
|
882
|
+
// Display execution status
|
|
883
|
+
console.log(chalk_1.default.green('✓ Execution initiated\n'));
|
|
884
|
+
const statusTable = new cli_table3_1.default({
|
|
885
|
+
colWidths: [25, 55],
|
|
886
|
+
wordWrap: true,
|
|
887
|
+
});
|
|
888
|
+
statusTable.push([chalk_1.default.bold('Execution ID'), chalk_1.default.cyan(executeResponse.execution_id)], [chalk_1.default.bold('Status'), formatExecutionStatus(executeResponse.status)]);
|
|
889
|
+
if (executeResponse.pr_url) {
|
|
890
|
+
statusTable.push([chalk_1.default.bold('Pull Request'), chalk_1.default.cyan(executeResponse.pr_url)]);
|
|
891
|
+
}
|
|
892
|
+
if (executeResponse.pipeline_url) {
|
|
893
|
+
statusTable.push([chalk_1.default.bold('Pipeline'), chalk_1.default.cyan(executeResponse.pipeline_url)]);
|
|
894
|
+
}
|
|
895
|
+
console.log(statusTable.toString());
|
|
896
|
+
console.log();
|
|
897
|
+
// Show status-specific messages
|
|
898
|
+
switch (executeResponse.status) {
|
|
899
|
+
case 'pending_approval':
|
|
900
|
+
console.log(chalk_1.default.yellow('⏳ Waiting for approval'));
|
|
901
|
+
console.log(chalk_1.default.gray(' Approval requests have been sent to designated approvers.'));
|
|
902
|
+
console.log(chalk_1.default.gray(' You will be notified once the plan is approved.'));
|
|
903
|
+
break;
|
|
904
|
+
case 'approved':
|
|
905
|
+
console.log(chalk_1.default.green('✓ Plan approved'));
|
|
906
|
+
console.log(chalk_1.default.gray(' Execution will begin shortly via GitOps workflow.'));
|
|
907
|
+
break;
|
|
908
|
+
case 'executing':
|
|
909
|
+
console.log(chalk_1.default.blue('⚙️ Execution in progress'));
|
|
910
|
+
console.log(chalk_1.default.gray(' Changes are being applied via GitOps workflow.'));
|
|
911
|
+
if (executeResponse.pr_url) {
|
|
912
|
+
console.log(chalk_1.default.gray(` Monitor PR: ${executeResponse.pr_url}`));
|
|
913
|
+
}
|
|
914
|
+
if (executeResponse.pipeline_url) {
|
|
915
|
+
console.log(chalk_1.default.gray(` Monitor pipeline: ${executeResponse.pipeline_url}`));
|
|
916
|
+
}
|
|
917
|
+
break;
|
|
918
|
+
case 'completed':
|
|
919
|
+
console.log(chalk_1.default.green('✓ Execution completed successfully'));
|
|
920
|
+
break;
|
|
921
|
+
case 'failed':
|
|
922
|
+
console.log(chalk_1.default.red('✗ Execution failed'));
|
|
923
|
+
console.log(chalk_1.default.gray(' Check the pipeline logs for details.'));
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
console.log();
|
|
927
|
+
// Show next steps
|
|
928
|
+
console.log(chalk_1.default.bold('Next Steps:'));
|
|
929
|
+
console.log(chalk_1.default.gray(` • Check status: cloudops incident ${incidentId} status`));
|
|
930
|
+
if (executeResponse.pr_url) {
|
|
931
|
+
console.log(chalk_1.default.gray(` • Review PR: ${executeResponse.pr_url}`));
|
|
932
|
+
}
|
|
933
|
+
if (executeResponse.pipeline_url) {
|
|
934
|
+
console.log(chalk_1.default.gray(` • Monitor pipeline: ${executeResponse.pipeline_url}`));
|
|
935
|
+
}
|
|
936
|
+
console.log();
|
|
937
|
+
}
|
|
938
|
+
catch (error) {
|
|
939
|
+
handleAPIError(error, 'execute remediation plan');
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Format risk score with color
|
|
944
|
+
*/
|
|
945
|
+
function formatRiskScore(score) {
|
|
946
|
+
if (score >= 80) {
|
|
947
|
+
return chalk_1.default.red.bold(`${score} (CRITICAL)`);
|
|
948
|
+
}
|
|
949
|
+
else if (score >= 60) {
|
|
950
|
+
return chalk_1.default.red(`${score} (HIGH)`);
|
|
951
|
+
}
|
|
952
|
+
else if (score >= 40) {
|
|
953
|
+
return chalk_1.default.yellow(`${score} (MEDIUM)`);
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
return chalk_1.default.green(`${score} (LOW)`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Format risk level with color
|
|
961
|
+
*/
|
|
962
|
+
function formatRiskLevel(level) {
|
|
963
|
+
const levelColors = {
|
|
964
|
+
low: chalk_1.default.green,
|
|
965
|
+
medium: chalk_1.default.yellow,
|
|
966
|
+
high: chalk_1.default.red,
|
|
967
|
+
critical: chalk_1.default.red.bold,
|
|
968
|
+
};
|
|
969
|
+
const colorFn = levelColors[level] || chalk_1.default.white;
|
|
970
|
+
return colorFn(level.toUpperCase());
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Format execution status with color
|
|
974
|
+
*/
|
|
975
|
+
function formatExecutionStatus(status) {
|
|
976
|
+
const statusColors = {
|
|
977
|
+
pending_approval: chalk_1.default.yellow,
|
|
978
|
+
approved: chalk_1.default.green,
|
|
979
|
+
executing: chalk_1.default.blue,
|
|
980
|
+
completed: chalk_1.default.green,
|
|
981
|
+
failed: chalk_1.default.red,
|
|
982
|
+
};
|
|
983
|
+
const colorFn = statusColors[status] || chalk_1.default.white;
|
|
984
|
+
return colorFn(status.replace(/_/g, ' ').toUpperCase());
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* List all incidents with optional filtering
|
|
988
|
+
* Displays incidents in a table format
|
|
989
|
+
*/
|
|
990
|
+
async function incidentListCommand(options) {
|
|
991
|
+
console.log(chalk_1.default.blue('\n📋 Fetching incidents...\n'));
|
|
992
|
+
try {
|
|
993
|
+
const apiClient = (0, api_client_1.createAPIClient)();
|
|
994
|
+
// Build query parameters
|
|
995
|
+
const params = new URLSearchParams();
|
|
996
|
+
if (options.status) {
|
|
997
|
+
params.append('status', options.status);
|
|
998
|
+
}
|
|
999
|
+
if (options.severity) {
|
|
1000
|
+
params.append('severity', options.severity);
|
|
1001
|
+
}
|
|
1002
|
+
const queryString = params.toString();
|
|
1003
|
+
const url = queryString ? `/api/v1/incidents?${queryString}` : '/api/v1/incidents';
|
|
1004
|
+
const response = await apiClient.get(url);
|
|
1005
|
+
if (!response.incidents || response.incidents.length === 0) {
|
|
1006
|
+
console.log(chalk_1.default.yellow('No incidents found.'));
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
console.log(chalk_1.default.green(`✓ Found ${response.incidents.length} incident(s)\n`));
|
|
1010
|
+
// Display incidents in a table
|
|
1011
|
+
const table = new cli_table3_1.default({
|
|
1012
|
+
head: ['ID', 'Description', 'Status', 'Severity', 'Created'],
|
|
1013
|
+
colWidths: [30, 50, 20, 15, 25],
|
|
1014
|
+
wordWrap: true,
|
|
1015
|
+
});
|
|
1016
|
+
response.incidents.forEach((incident) => {
|
|
1017
|
+
table.push([
|
|
1018
|
+
chalk_1.default.cyan(incident.incident_id),
|
|
1019
|
+
incident.description.substring(0, 47) + (incident.description.length > 47 ? '...' : ''),
|
|
1020
|
+
formatStatus(incident.status),
|
|
1021
|
+
formatSeverity(incident.severity),
|
|
1022
|
+
new Date(incident.created_at).toLocaleString(),
|
|
1023
|
+
]);
|
|
1024
|
+
});
|
|
1025
|
+
console.log(table.toString());
|
|
1026
|
+
console.log();
|
|
1027
|
+
}
|
|
1028
|
+
catch (error) {
|
|
1029
|
+
handleAPIError(error, 'list incidents');
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
//# sourceMappingURL=incident.js.map
|