gpxe 1.0.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/bin/index.js +8 -0
- package/package.json +36 -0
- package/src/api.js +39 -0
- package/src/billing.js +91 -0
- package/src/commands/alerts.js +66 -0
- package/src/commands/basic.js +110 -0
- package/src/commands/billing.js +118 -0
- package/src/commands/capacity.js +62 -0
- package/src/commands/changes.js +51 -0
- package/src/commands/config.js +72 -0
- package/src/commands/deploy.js +36 -0
- package/src/commands/incidents.js +144 -0
- package/src/commands/reserve.js +17 -0
- package/src/commands/runbooks.js +51 -0
- package/src/commands/slos.js +50 -0
- package/src/commands/status.js +116 -0
- package/src/commands/support.js +67 -0
- package/src/config.js +35 -0
- package/src/demo.js +211 -0
- package/src/main.js +89 -0
- package/src/util.js +110 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const { program } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const { resolveEndpoint, request } = require('../api');
|
|
4
|
+
const { printRequestError } = require('../util');
|
|
5
|
+
const { isDemo } = require('../demo');
|
|
6
|
+
|
|
7
|
+
const deployCommand = program.command('deploy').description('Deployment helpers.');
|
|
8
|
+
|
|
9
|
+
deployCommand
|
|
10
|
+
.command('rollback <version>')
|
|
11
|
+
.description('Request a rollback of a deploy.')
|
|
12
|
+
.option('--service <name>', 'Filter to a specific service')
|
|
13
|
+
.option('--endpoint <url>', 'Override endpoint')
|
|
14
|
+
.option('--json', 'Output raw JSON')
|
|
15
|
+
.option('--demo', 'Use demo data')
|
|
16
|
+
.action(async (version, opts) => {
|
|
17
|
+
const endpoint = resolveEndpoint(opts.endpoint);
|
|
18
|
+
const payload = { version, service: opts.service || null };
|
|
19
|
+
try {
|
|
20
|
+
const data = isDemo(opts)
|
|
21
|
+
? { status: 'queued', ...payload }
|
|
22
|
+
: (await request('post', endpoint, '/deploy/rollback', payload)).data || payload;
|
|
23
|
+
if (opts.json) {
|
|
24
|
+
console.log(JSON.stringify(data, null, 2));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
console.log(chalk.cyan('Rollback request'));
|
|
28
|
+
console.log(`Version: ${version}`);
|
|
29
|
+
if (payload.service) {
|
|
30
|
+
console.log(`Service: ${payload.service}`);
|
|
31
|
+
}
|
|
32
|
+
console.log(`Status: ${data.status || 'queued'}`);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
printRequestError(e, 'Failed to request rollback.', endpoint);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const { program } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const { resolveEndpoint, request } = require('../api');
|
|
4
|
+
const { toInt, printRequestError, shouldShowHints, wantsJSON } = require('../util');
|
|
5
|
+
const { isDemo, demoIncidentExplain } = require('../demo');
|
|
6
|
+
|
|
7
|
+
program.command('incident <title>')
|
|
8
|
+
.description('Declare an incident update.')
|
|
9
|
+
.option('--status <type>', 'critical, degraded', 'critical')
|
|
10
|
+
.option('--severity <level>', 'low, medium, high, critical', 'high')
|
|
11
|
+
.option('--service <name>', 'Service name')
|
|
12
|
+
.option('--summary <text>', 'Short summary for operators')
|
|
13
|
+
.option('--endpoint <url>', 'Override endpoint')
|
|
14
|
+
.action(async (title, opts) => {
|
|
15
|
+
const endpoint = resolveEndpoint(opts.endpoint);
|
|
16
|
+
try {
|
|
17
|
+
const payload = {
|
|
18
|
+
title,
|
|
19
|
+
status: opts.status,
|
|
20
|
+
severity: opts.severity,
|
|
21
|
+
service: opts.service,
|
|
22
|
+
summary: opts.summary
|
|
23
|
+
};
|
|
24
|
+
const response = await request('post', endpoint, '/incidents', payload);
|
|
25
|
+
const incident = response.data?.incident || null;
|
|
26
|
+
console.log(chalk.red.bold(`\n🚨 INCIDENT DECLARED: ${title}`));
|
|
27
|
+
if (incident?.id) {
|
|
28
|
+
console.log(chalk.white(`Incident ID: ${incident.id}`));
|
|
29
|
+
}
|
|
30
|
+
console.log(chalk.white('Public status page updated immediately.'));
|
|
31
|
+
if (incident?.id && shouldShowHints()) {
|
|
32
|
+
console.log(chalk.gray(`Tip: gpxe incidents explain ${incident.id}`));
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
printRequestError(e, 'Failed to report incident.', endpoint);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const incidentsCommand = program.command('incidents')
|
|
40
|
+
.description('Fetch the latest incident details.')
|
|
41
|
+
.option('--endpoint <url>', 'Override endpoint')
|
|
42
|
+
.option('--json', 'Output raw JSON')
|
|
43
|
+
.action(async (opts) => {
|
|
44
|
+
const endpoint = resolveEndpoint(opts.endpoint);
|
|
45
|
+
try {
|
|
46
|
+
const response = await request('get', endpoint, '/incidents');
|
|
47
|
+
const incident = response.data || null;
|
|
48
|
+
if (opts.json) {
|
|
49
|
+
console.log(JSON.stringify({ endpoint, incident }, null, 2));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!incident) {
|
|
53
|
+
console.log(chalk.green('No incidents reported.'));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
console.log(chalk.red(`Latest incident: ${incident.title || 'Untitled'}`));
|
|
57
|
+
console.log(`Status: ${incident.status || 'unknown'}`);
|
|
58
|
+
if (incident.severity) {
|
|
59
|
+
console.log(`Severity: ${incident.severity}`);
|
|
60
|
+
}
|
|
61
|
+
if (incident.service) {
|
|
62
|
+
console.log(`Service: ${incident.service}`);
|
|
63
|
+
}
|
|
64
|
+
console.log(`Created: ${incident.created_at || 'unknown'}`);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
printRequestError(e, 'Failed to fetch incidents.', endpoint);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
incidentsCommand
|
|
71
|
+
.command('explain <incident_id>')
|
|
72
|
+
.description('Explain the likely causes behind an incident.')
|
|
73
|
+
.option('--service <name>', 'Filter to a specific service')
|
|
74
|
+
.option('--since <window>', 'Lookback window (e.g. 24h)')
|
|
75
|
+
.option('--evidence <count>', 'Number of evidence items', '5')
|
|
76
|
+
.option('--endpoint <url>', 'Override endpoint')
|
|
77
|
+
.option('--json', 'Output raw JSON')
|
|
78
|
+
.option('--demo', 'Use demo data')
|
|
79
|
+
.option('--quiet', 'Suppress suggested actions')
|
|
80
|
+
.action(async (incidentId, opts) => {
|
|
81
|
+
const endpoint = resolveEndpoint(opts.endpoint);
|
|
82
|
+
const params = {};
|
|
83
|
+
if (opts.service) params.service = opts.service;
|
|
84
|
+
if (opts.since) params.since = opts.since;
|
|
85
|
+
if (opts.evidence) params.evidence = toInt(opts.evidence, 5);
|
|
86
|
+
if (opts.quiet) params.quiet = true;
|
|
87
|
+
try {
|
|
88
|
+
const data = isDemo(opts)
|
|
89
|
+
? demoIncidentExplain(incidentId)
|
|
90
|
+
: (await request('get', endpoint, `/incidents/${encodeURIComponent(incidentId)}/explain`, null, params))
|
|
91
|
+
.data || {};
|
|
92
|
+
if (opts.json || wantsJSON()) {
|
|
93
|
+
console.log(JSON.stringify(data, null, 2));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const incidentLabel = data.id || incidentId;
|
|
97
|
+
const severity = data.severity || 'unknown';
|
|
98
|
+
const status = data.status || 'unknown';
|
|
99
|
+
const summary = data.summary || 'No summary available.';
|
|
100
|
+
const confidence = Number.isFinite(data.confidence) ? data.confidence : null;
|
|
101
|
+
console.log(chalk.cyan(`Incident: ${incidentLabel} (Severity: ${severity}) Status: ${status}`));
|
|
102
|
+
console.log(`Summary: ${summary}`);
|
|
103
|
+
console.log(`Confidence: ${confidence !== null ? confidence : 'n/a'}`);
|
|
104
|
+
console.log('');
|
|
105
|
+
if (data.primary_cause) {
|
|
106
|
+
const cause = data.primary_cause;
|
|
107
|
+
const causeLabel = [cause.type, cause.id].filter(Boolean).join(':') || 'unknown';
|
|
108
|
+
const score = Number.isFinite(cause.score) ? cause.score : 'n/a';
|
|
109
|
+
console.log('Primary cause:');
|
|
110
|
+
console.log(`- ${causeLabel} (change score ${score})`);
|
|
111
|
+
} else {
|
|
112
|
+
console.log('Primary cause: not available.');
|
|
113
|
+
}
|
|
114
|
+
if (Array.isArray(data.signals) && data.signals.length > 0) {
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log('Contributing signals:');
|
|
117
|
+
data.signals.forEach((signal) => {
|
|
118
|
+
const name = signal.name || signal.metric || 'signal';
|
|
119
|
+
const delta = signal.delta ?? signal.change ?? 'n/a';
|
|
120
|
+
const service = signal.service ? ` (service/${signal.service})` : '';
|
|
121
|
+
console.log(`- ${name} ${delta}${service}`);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(data.evidence) && data.evidence.length > 0) {
|
|
125
|
+
console.log('');
|
|
126
|
+
console.log(`Evidence (top ${data.evidence.length}):`);
|
|
127
|
+
data.evidence.forEach((item, index) => {
|
|
128
|
+
const detail = item.detail || item.kind || 'evidence';
|
|
129
|
+
console.log(`${index + 1}) ${detail}`);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (!opts.quiet && Array.isArray(data.suggested_actions) && data.suggested_actions.length > 0) {
|
|
133
|
+
console.log('');
|
|
134
|
+
console.log('Suggested actions:');
|
|
135
|
+
data.suggested_actions.forEach((action) => {
|
|
136
|
+
const cmd = action.cmd ? ` (${action.cmd})` : '';
|
|
137
|
+
const reason = action.reason || 'Follow up';
|
|
138
|
+
console.log(`- ${reason}${cmd}`);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
} catch (e) {
|
|
142
|
+
printRequestError(e, 'Failed to explain incident.', endpoint);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const { program } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
program
|
|
6
|
+
.command('reserve')
|
|
7
|
+
.description('Reserve a spot in the private beta.')
|
|
8
|
+
.action(async () => {
|
|
9
|
+
// Generate a cryptographically strong random token
|
|
10
|
+
const randomBytes = crypto.randomBytes(4).toString('hex').toUpperCase(); // 8 chars
|
|
11
|
+
const token = `GP-${randomBytes.slice(0, 4)}-${randomBytes.slice(4)}`;
|
|
12
|
+
|
|
13
|
+
console.log(chalk.bold('\nâš¡ Gridpoint Analytics Private Beta Reservation âš¡\n'));
|
|
14
|
+
console.log('Use this token to claim your spot on the waitlist:');
|
|
15
|
+
console.log('\n' + chalk.bgBlue.white.bold(` ${token} `) + '\n');
|
|
16
|
+
console.log(chalk.gray('Enter this token at https://gridpointanalytics.com\n'));
|
|
17
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const { program } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const { resolveEndpoint, request } = require('../api');
|
|
4
|
+
const { printRequestError, shouldShowHints } = require('../util');
|
|
5
|
+
const { isDemo, demoRunbookSuggest } = require('../demo');
|
|
6
|
+
|
|
7
|
+
const runbooksCommand = program.command('runbooks').description('Runbook intelligence commands.');
|
|
8
|
+
|
|
9
|
+
runbooksCommand
|
|
10
|
+
.command('suggest <incident_id>')
|
|
11
|
+
.description('Suggest operator runbooks for an incident.')
|
|
12
|
+
.option('--service <name>', 'Filter to a specific service')
|
|
13
|
+
.option('--endpoint <url>', 'Override endpoint')
|
|
14
|
+
.option('--json', 'Output raw JSON')
|
|
15
|
+
.option('--demo', 'Use demo data')
|
|
16
|
+
.action(async (incidentId, opts) => {
|
|
17
|
+
const endpoint = resolveEndpoint(opts.endpoint);
|
|
18
|
+
const params = {};
|
|
19
|
+
if (opts.service) params.service = opts.service;
|
|
20
|
+
try {
|
|
21
|
+
const data = isDemo(opts)
|
|
22
|
+
? demoRunbookSuggest(incidentId, params.service)
|
|
23
|
+
: (await request('get', endpoint, `/runbooks/suggest/${encodeURIComponent(incidentId)}`, null, params))
|
|
24
|
+
.data || {};
|
|
25
|
+
if (opts.json) {
|
|
26
|
+
console.log(JSON.stringify(data, null, 2));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const incidentLabel = data.incident_id || incidentId;
|
|
30
|
+
console.log(chalk.cyan(`Runbook suggestions for ${incidentLabel}`));
|
|
31
|
+
if (data.title) {
|
|
32
|
+
console.log(`Incident: ${data.title}`);
|
|
33
|
+
}
|
|
34
|
+
const steps = Array.isArray(data.steps) ? data.steps : [];
|
|
35
|
+
if (steps.length === 0) {
|
|
36
|
+
console.log('No runbook steps available.');
|
|
37
|
+
if (shouldShowHints()) {
|
|
38
|
+
console.log(chalk.gray('Tip: store runbooks for this service or run with --demo.'));
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
console.log('');
|
|
43
|
+
steps.forEach((step, index) => {
|
|
44
|
+
console.log(`${index + 1}) ${step.title || 'Step'}`);
|
|
45
|
+
if (step.command) console.log(` ${step.command}`);
|
|
46
|
+
if (step.rationale) console.log(` ${step.rationale}`);
|
|
47
|
+
});
|
|
48
|
+
} catch (e) {
|
|
49
|
+
printRequestError(e, 'Failed to suggest runbooks.', endpoint);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const { program } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const { resolveEndpoint, request } = require('../api');
|
|
4
|
+
const { printRequestError, shouldShowHints } = require('../util');
|
|
5
|
+
const { isDemo, demoSloStatus } = require('../demo');
|
|
6
|
+
|
|
7
|
+
program.command('slos')
|
|
8
|
+
.description('Service-level objective commands.')
|
|
9
|
+
.command('status')
|
|
10
|
+
.description('Show current SLO status.')
|
|
11
|
+
.option('--project <name>', 'Filter to a project')
|
|
12
|
+
.option('--service <name>', 'Filter to a specific service')
|
|
13
|
+
.option('--endpoint <url>', 'Override endpoint')
|
|
14
|
+
.option('--json', 'Output raw JSON')
|
|
15
|
+
.option('--demo', 'Use demo data')
|
|
16
|
+
.action(async (opts) => {
|
|
17
|
+
const endpoint = resolveEndpoint(opts.endpoint);
|
|
18
|
+
const params = {};
|
|
19
|
+
if (opts.project) params.project = opts.project;
|
|
20
|
+
if (opts.service) params.service = opts.service;
|
|
21
|
+
try {
|
|
22
|
+
const data = isDemo(opts)
|
|
23
|
+
? demoSloStatus(opts.project)
|
|
24
|
+
: (await request('get', endpoint, '/slos/status', null, params)).data || {};
|
|
25
|
+
if (opts.json) {
|
|
26
|
+
console.log(JSON.stringify(data, null, 2));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const project = data.project || opts.project || 'default';
|
|
30
|
+
const slos = Array.isArray(data.slos) ? data.slos : [];
|
|
31
|
+
console.log(chalk.cyan(`SLO Status (project: ${project})`));
|
|
32
|
+
if (slos.length === 0) {
|
|
33
|
+
console.log('No SLOs found.');
|
|
34
|
+
if (shouldShowHints()) {
|
|
35
|
+
console.log(chalk.gray('Tip: ingest SLO snapshots or run with --demo.'));
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
slos.forEach((slo) => {
|
|
40
|
+
const service = slo.service || 'unknown';
|
|
41
|
+
const availability = Number.isFinite(slo.availability) ? `${slo.availability.toFixed(2)}%` : 'n/a';
|
|
42
|
+
const budget = Number.isFinite(slo.budget_left_pct) ? `${slo.budget_left_pct}%` : 'n/a';
|
|
43
|
+
const burn = Number.isFinite(slo.burn_rate) ? `${slo.burn_rate}x` : 'n/a';
|
|
44
|
+
const ttb = slo.time_to_breach || 'n/a';
|
|
45
|
+
console.log(`service/${service} avail: ${availability} budget left: ${budget} burn: ${burn} time-to-breach: ${ttb}`);
|
|
46
|
+
});
|
|
47
|
+
} catch (e) {
|
|
48
|
+
printRequestError(e, 'Failed to fetch SLO status.', endpoint);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const { program } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const { resolveEndpoint, request } = require('../api');
|
|
4
|
+
const { toInt, printRequestError, shouldShowHints } = require('../util');
|
|
5
|
+
|
|
6
|
+
program.command('status')
|
|
7
|
+
.description('Show a clear health summary.')
|
|
8
|
+
.option('--endpoint <url>', 'Override endpoint')
|
|
9
|
+
.option('--json', 'Output raw JSON')
|
|
10
|
+
.action(async (opts) => {
|
|
11
|
+
const endpoint = resolveEndpoint(opts.endpoint);
|
|
12
|
+
try {
|
|
13
|
+
const metricsResponse = await request('get', endpoint, '/metrics');
|
|
14
|
+
const metrics = Array.isArray(metricsResponse.data) ? metricsResponse.data : [];
|
|
15
|
+
let incident = null;
|
|
16
|
+
try {
|
|
17
|
+
const incidentResponse = await request('get', endpoint, '/incidents');
|
|
18
|
+
incident = incidentResponse.data || null;
|
|
19
|
+
} catch (e) {
|
|
20
|
+
incident = null;
|
|
21
|
+
}
|
|
22
|
+
const healthy = metrics.length > 0;
|
|
23
|
+
const healthLabel = healthy ? 'healthy' : 'degraded';
|
|
24
|
+
const statusLabel = healthy ? 'All systems operational' : 'Waiting for telemetry';
|
|
25
|
+
const payload = {
|
|
26
|
+
endpoint,
|
|
27
|
+
health: healthLabel,
|
|
28
|
+
status: statusLabel,
|
|
29
|
+
metrics: {
|
|
30
|
+
count: metrics.length,
|
|
31
|
+
latest: metrics[0] || null
|
|
32
|
+
},
|
|
33
|
+
incident
|
|
34
|
+
};
|
|
35
|
+
if (opts.json) {
|
|
36
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const status = await checkSystemStatus(opts.endpoint);
|
|
40
|
+
|
|
41
|
+
console.log(chalk.cyan('Gridpoint Analytics Status'));
|
|
42
|
+
console.log('---------------------------');
|
|
43
|
+
console.log(`System: ${status.healthy ? chalk.green('ONLINE') : chalk.red('OFFLINE')}`);
|
|
44
|
+
console.log(`Endpoint: ${endpoint}`);
|
|
45
|
+
console.log(`Health: ${healthy ? chalk.green('healthy') : chalk.yellow('degraded')}`);
|
|
46
|
+
console.log(`Status: ${statusLabel}`);
|
|
47
|
+
console.log(`Recent metrics: ${metrics.length}`);
|
|
48
|
+
if (metrics[0]) {
|
|
49
|
+
console.log(`Latest metric: ${metrics[0].timestamp || 'unknown'} (cpu ${metrics[0].cpu_time || 'n/a'}, status ${metrics[0].status || 'n/a'})`);
|
|
50
|
+
} else {
|
|
51
|
+
console.log('Next step: gpxe metrics --watch 10');
|
|
52
|
+
}
|
|
53
|
+
if (incident) {
|
|
54
|
+
console.log(chalk.red(`Incident: ${incident.title || 'Untitled'} (${incident.status || 'unknown'})`));
|
|
55
|
+
} else {
|
|
56
|
+
console.log('Incident: none reported');
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
printRequestError(e, 'Unable to fetch status from endpoint.', endpoint);
|
|
60
|
+
if (shouldShowHints()) {
|
|
61
|
+
console.log(chalk.gray('Tip: run "gpxe config" to confirm endpoint/token.'));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
program.command('metrics')
|
|
67
|
+
.description('List recent metrics or watch for new ones.')
|
|
68
|
+
.option('--endpoint <url>', 'Override endpoint')
|
|
69
|
+
.option('--limit <count>', 'Number of rows to show', '10')
|
|
70
|
+
.option('--watch <seconds>', 'Poll interval in seconds')
|
|
71
|
+
.option('--json', 'Output raw JSON')
|
|
72
|
+
.action(async (opts) => {
|
|
73
|
+
const endpoint = resolveEndpoint(opts.endpoint);
|
|
74
|
+
const limit = toInt(opts.limit, 10);
|
|
75
|
+
const intervalSeconds = opts.watch ? toInt(opts.watch, 0) : 0;
|
|
76
|
+
|
|
77
|
+
const runOnce = async () => {
|
|
78
|
+
try {
|
|
79
|
+
const response = await request('get', endpoint, '/metrics');
|
|
80
|
+
const metrics = Array.isArray(response.data) ? response.data : [];
|
|
81
|
+
const rows = metrics.slice(0, Math.max(limit, 0));
|
|
82
|
+
const payload = { endpoint, count: metrics.length, metrics: rows };
|
|
83
|
+
if (opts.json) {
|
|
84
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
console.log(chalk.cyan(`Fetched ${metrics.length} metrics from ${endpoint}`));
|
|
88
|
+
if (rows.length === 0) {
|
|
89
|
+
console.log('No metrics available.');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const hasService = rows.some((metric) => metric.service);
|
|
93
|
+
const hasLatency = rows.some((metric) => metric.p95_latency_ms !== undefined && metric.p95_latency_ms !== null);
|
|
94
|
+
const hasErrorRate = rows.some((metric) => metric.error_rate_pct !== undefined && metric.error_rate_pct !== null);
|
|
95
|
+
console.table(rows.map((metric) => {
|
|
96
|
+
const row = {
|
|
97
|
+
timestamp: metric.timestamp || 'unknown',
|
|
98
|
+
cpu_time: metric.cpu_time ?? 'n/a',
|
|
99
|
+
status: metric.status ?? 'n/a'
|
|
100
|
+
};
|
|
101
|
+
if (hasService) row.service = metric.service || 'n/a';
|
|
102
|
+
if (hasLatency) row.p95_latency_ms = metric.p95_latency_ms ?? 'n/a';
|
|
103
|
+
if (hasErrorRate) row.error_rate_pct = metric.error_rate_pct ?? 'n/a';
|
|
104
|
+
return row;
|
|
105
|
+
}));
|
|
106
|
+
} catch (e) {
|
|
107
|
+
printRequestError(e, 'Failed to fetch metrics.', endpoint);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
await runOnce();
|
|
112
|
+
if (intervalSeconds > 0) {
|
|
113
|
+
console.log(chalk.gray(`Watching metrics every ${intervalSeconds}s. Press Ctrl+C to stop.`));
|
|
114
|
+
setInterval(runOnce, intervalSeconds * 1000);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const { program } = require('commander');
|
|
2
|
+
const { prompt } = require('enquirer');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { resolveEndpoint, request } = require('../api');
|
|
5
|
+
const { printRequestError } = require('../util');
|
|
6
|
+
const { isDemo, demoSupportRequest } = require('../demo');
|
|
7
|
+
|
|
8
|
+
const supportCommand = program.command('support').description('Support request commands.');
|
|
9
|
+
|
|
10
|
+
supportCommand
|
|
11
|
+
.command('new')
|
|
12
|
+
.description('Open a new support request.')
|
|
13
|
+
.option('--topic <topic>', 'Support topic (billing, incident, access, etc.)', 'general')
|
|
14
|
+
.option('--subject <text>', 'Short subject line')
|
|
15
|
+
.option('--message <text>', 'Details for the support team')
|
|
16
|
+
.option('--email <email>', 'Contact email address')
|
|
17
|
+
.option('--endpoint <url>', 'Override endpoint')
|
|
18
|
+
.option('--json', 'Output raw JSON')
|
|
19
|
+
.option('--demo', 'Use demo data')
|
|
20
|
+
.action(async (opts) => {
|
|
21
|
+
let subject = opts.subject;
|
|
22
|
+
let message = opts.message;
|
|
23
|
+
if (!subject && process.stdin.isTTY) {
|
|
24
|
+
const answers = await prompt({
|
|
25
|
+
type: 'input',
|
|
26
|
+
name: 'subject',
|
|
27
|
+
message: 'Support request subject:'
|
|
28
|
+
});
|
|
29
|
+
subject = answers.subject;
|
|
30
|
+
}
|
|
31
|
+
if (!message && process.stdin.isTTY) {
|
|
32
|
+
const answers = await prompt({
|
|
33
|
+
type: 'input',
|
|
34
|
+
name: 'message',
|
|
35
|
+
message: 'What should we know?'
|
|
36
|
+
});
|
|
37
|
+
message = answers.message;
|
|
38
|
+
}
|
|
39
|
+
if (!subject && !message) {
|
|
40
|
+
console.log(chalk.red('Provide a subject or message to open a request.'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const endpoint = resolveEndpoint(opts.endpoint);
|
|
44
|
+
const payload = {
|
|
45
|
+
topic: opts.topic || 'general',
|
|
46
|
+
subject: subject || null,
|
|
47
|
+
message: message || null,
|
|
48
|
+
email: opts.email || null
|
|
49
|
+
};
|
|
50
|
+
try {
|
|
51
|
+
const data = isDemo(opts)
|
|
52
|
+
? demoSupportRequest(payload.topic, payload.subject, payload.message, payload.email)
|
|
53
|
+
: (await request('post', endpoint, '/support/requests', payload)).data || {};
|
|
54
|
+
if (opts.json) {
|
|
55
|
+
console.log(JSON.stringify(data, null, 2));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const requestData = data.request || payload;
|
|
59
|
+
console.log(chalk.cyan('Support request created'));
|
|
60
|
+
if (requestData.id) console.log(`ID: ${requestData.id}`);
|
|
61
|
+
console.log(`Topic: ${requestData.topic || payload.topic}`);
|
|
62
|
+
if (requestData.subject) console.log(`Subject: ${requestData.subject}`);
|
|
63
|
+
console.log(`Status: ${data.status || 'open'}`);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
printRequestError(e, 'Failed to open support request.', endpoint);
|
|
66
|
+
}
|
|
67
|
+
});
|
package/src/config.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const CONFIG_PATH = path.join(os.homedir(), '.gridpoint-cli.json');
|
|
6
|
+
const readConfig = () => {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
9
|
+
} catch (e) {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const writeConfig = (data) => {
|
|
14
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(data));
|
|
15
|
+
};
|
|
16
|
+
const config = {
|
|
17
|
+
set: (k, v) => {
|
|
18
|
+
const data = readConfig();
|
|
19
|
+
data[k] = v;
|
|
20
|
+
writeConfig(data);
|
|
21
|
+
},
|
|
22
|
+
get: (k) => {
|
|
23
|
+
const data = readConfig();
|
|
24
|
+
return data[k] ?? null;
|
|
25
|
+
},
|
|
26
|
+
unset: (k) => {
|
|
27
|
+
const data = readConfig();
|
|
28
|
+
if (!(k in data)) return false;
|
|
29
|
+
delete data[k];
|
|
30
|
+
writeConfig(data);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
module.exports = { config, CONFIG_PATH };
|