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 ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ const { run } = require('../src/main');
3
+
4
+ run(process.argv).catch((err) => {
5
+ console.error(err);
6
+ process.exitCode = 1;
7
+ });
8
+ module.exports = { run };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "gpxe",
3
+ "version": "1.0.0",
4
+ "description": "CLI-first telemetry for Gridpoint Analytics.",
5
+ "main": "bin/index.js",
6
+ "scripts": {
7
+ "test": "node test/cli.spec.js"
8
+ },
9
+ "keywords": [
10
+ "analytics",
11
+ "cli",
12
+ "cloudflare",
13
+ "observability",
14
+ "telemetry",
15
+ "gridpoint"
16
+ ],
17
+ "author": "",
18
+ "license": "ISC",
19
+ "type": "commonjs",
20
+ "dependencies": {
21
+ "axios": "^1.13.2",
22
+ "chalk": "^4.1.2",
23
+ "commander": "^14.0.2",
24
+ "conf": "^15.0.2",
25
+ "enquirer": "^2.4.1"
26
+ },
27
+ "bin": {
28
+ "gpxe": "bin/index.js"
29
+ },
30
+ "files": [
31
+ "bin/",
32
+ "src/",
33
+ "README.md",
34
+ "package.json"
35
+ ]
36
+ }
package/src/api.js ADDED
@@ -0,0 +1,39 @@
1
+ const axios = require('axios');
2
+ const { config } = require('./config');
3
+
4
+ const DEFAULT_ENDPOINT = "https://gridpoint-engine.awalker-3.workers.dev";
5
+
6
+ const normalizeEndpoint = (endpoint) => {
7
+ if (!endpoint) return endpoint;
8
+ return endpoint.replace(/\/+$/, '');
9
+ };
10
+
11
+ const resolveEndpoint = (explicitEndpoint) => {
12
+ return normalizeEndpoint(explicitEndpoint || process.env.GRIDPOINT_ENDPOINT || config.get('endpoint') || DEFAULT_ENDPOINT);
13
+ };
14
+
15
+ const resolveToken = () => process.env.GRIDPOINT_TOKEN || config.get('token');
16
+
17
+ const buildHeaders = () => {
18
+ const token = resolveToken();
19
+ return token ? { Authorization: `Bearer ${token}` } : {};
20
+ };
21
+
22
+ const request = async (method, endpoint, path, data, params) => {
23
+ return axios({
24
+ method,
25
+ url: `${endpoint}${path}`,
26
+ data,
27
+ params,
28
+ headers: { "Content-Type": "application/json", ...buildHeaders() },
29
+ timeout: 10000
30
+ });
31
+ };
32
+
33
+ module.exports = {
34
+ DEFAULT_ENDPOINT,
35
+ normalizeEndpoint,
36
+ resolveEndpoint,
37
+ resolveToken,
38
+ request
39
+ };
package/src/billing.js ADDED
@@ -0,0 +1,91 @@
1
+ const { config } = require('./config');
2
+ const { request, resolveEndpoint } = require('./api');
3
+ const { program } = require('commander');
4
+ const { getCommandPath } = require('./util');
5
+ const chalk = require('chalk');
6
+
7
+ let cachedBillingContext = null;
8
+ let billingContextPromise = null;
9
+
10
+ const resolveBillingContext = async (endpoint) => {
11
+ if (cachedBillingContext) return cachedBillingContext;
12
+ const accountId = process.env.GRIDPOINT_ACCOUNT_ID || config.get('account_id');
13
+ const workspaceId = process.env.GRIDPOINT_WORKSPACE_ID || config.get('workspace_id');
14
+ if (accountId || workspaceId) {
15
+ cachedBillingContext = { accountId: accountId || null, workspaceId: workspaceId || null };
16
+ return cachedBillingContext;
17
+ }
18
+ if (!endpoint) return null;
19
+ if (billingContextPromise) return billingContextPromise;
20
+ billingContextPromise = (async () => {
21
+ try {
22
+ const response = await request('get', endpoint, '/billing/account');
23
+ const account = response.data?.account || null;
24
+ if (account?.id || account?.workspace_id) {
25
+ cachedBillingContext = {
26
+ accountId: account.id || null,
27
+ workspaceId: account.workspace_id || null
28
+ };
29
+ }
30
+ return cachedBillingContext;
31
+ } catch (e) {
32
+ return null;
33
+ } finally {
34
+ billingContextPromise = null;
35
+ }
36
+ })();
37
+ return billingContextPromise;
38
+ };
39
+
40
+ const isMeteringEnabled = (opts) => {
41
+ if (opts?.demo) return false;
42
+ const demoEnv = ['1', 'true', 'yes', 'on'].includes((process.env.GRIDPOINT_DEMO || '').toLowerCase());
43
+ return !demoEnv;
44
+ };
45
+
46
+ const shouldMeterCommand = (commandPath) => {
47
+ if (!commandPath) return false;
48
+ const top = commandPath.split(' ')[0];
49
+ return !['login', 'init', 'config', 'billing', 'help'].includes(top);
50
+ };
51
+
52
+ const recordUsage = async (actionCommand) => {
53
+ const opts = typeof actionCommand?.opts === 'function' ? actionCommand.opts() : {};
54
+ if (!isMeteringEnabled(opts)) return;
55
+ const commandPath = getCommandPath(actionCommand);
56
+ if (!shouldMeterCommand(commandPath)) return;
57
+ const endpoint = resolveEndpoint(opts.endpoint);
58
+ const context = await resolveBillingContext(endpoint);
59
+ if (!context?.accountId && !context?.workspaceId) return;
60
+ const started = opts?._startTime;
61
+ const durationMs = Number.isFinite(started) ? Date.now() - started : null;
62
+ const payload = {
63
+ account_id: context?.accountId || null,
64
+ workspace_id: context?.workspaceId || null,
65
+ service: commandPath,
66
+ quantity: 1,
67
+ unit: 'commands',
68
+ source: 'cli',
69
+ metadata: {
70
+ command: commandPath,
71
+ duration_ms: Number.isFinite(durationMs) ? durationMs : null,
72
+ cli_version: program.version(),
73
+ platform: process.platform,
74
+ node_version: process.version
75
+ }
76
+ };
77
+ try {
78
+ await request('post', endpoint, '/billing/usage', payload);
79
+ } catch (e) {
80
+ if (showHints) {
81
+ console.log(chalk.gray('Tip: unable to record billing usage; check account/workspace id.'));
82
+ }
83
+ }
84
+ };
85
+
86
+ module.exports = {
87
+ resolveBillingContext,
88
+ isMeteringEnabled,
89
+ shouldMeterCommand,
90
+ recordUsage,
91
+ };
@@ -0,0 +1,66 @@
1
+ const { program } = require('commander');
2
+ const chalk = require('chalk');
3
+ const { resolveEndpoint, request } = require('../api');
4
+ const { toInt, printRequestError, shouldShowHints, formatPercent } = require('../util');
5
+ const { isDemo, demoAlertsTune } = require('../demo');
6
+
7
+ const alertsCommand = program.command('alerts').description('Alert intelligence commands.');
8
+
9
+ alertsCommand
10
+ .command('tune')
11
+ .description('Suggest adaptive alert thresholds.')
12
+ .option('--window <window>', 'Lookback window (e.g. 24h)', '24h')
13
+ .option('--service <name>', 'Filter to a specific service')
14
+ .option('--limit <count>', 'Max recommendations', '5')
15
+ .option('--endpoint <url>', 'Override endpoint')
16
+ .option('--json', 'Output raw JSON')
17
+ .option('--demo', 'Use demo data')
18
+ .action(async (opts) => {
19
+ const endpoint = resolveEndpoint(opts.endpoint);
20
+ const params = {
21
+ window: opts.window,
22
+ limit: toInt(opts.limit, 5)
23
+ };
24
+ if (opts.service) params.service = opts.service;
25
+ try {
26
+ const data = isDemo(opts)
27
+ ? demoAlertsTune(params.window, params.service)
28
+ : (await request('get', endpoint, '/alerts/tune', null, params)).data || {};
29
+ if (opts.json) {
30
+ console.log(JSON.stringify(data, null, 2));
31
+ return;
32
+ }
33
+ const windowLabel = data.window || opts.window;
34
+ const serviceLabel = data.service || opts.service || 'all services';
35
+ console.log(chalk.cyan(`Adaptive alerting (service/${serviceLabel}, window: ${windowLabel})`));
36
+ const summary = data.summary || {};
37
+ if (Number.isFinite(summary.noisy_alerts) || Number.isFinite(summary.total_alerts)) {
38
+ const noisy = Number.isFinite(summary.noisy_alerts) ? summary.noisy_alerts : 'n/a';
39
+ const total = Number.isFinite(summary.total_alerts) ? summary.total_alerts : 'n/a';
40
+ const pct = Number.isFinite(summary.noise_pct) ? `${summary.noise_pct}%` : 'n/a';
41
+ console.log(`Noisy alerts: ${noisy} / ${total} (${pct})`);
42
+ }
43
+ const recommendations = Array.isArray(data.recommendations) ? data.recommendations : [];
44
+ if (recommendations.length === 0) {
45
+ console.log('No tuning recommendations.');
46
+ if (shouldShowHints()) {
47
+ console.log(chalk.gray('Tip: ingest alert data or run with --demo.'));
48
+ }
49
+ return;
50
+ }
51
+ console.log('');
52
+ console.log('Recommendations:');
53
+ recommendations.slice(0, params.limit).forEach((rec, index) => {
54
+ const name = rec.alert || rec.name || 'alert';
55
+ const action = rec.action || 'adjust';
56
+ const current = rec.current || 'n/a';
57
+ const suggested = rec.suggested || 'n/a';
58
+ const reduction = Number.isFinite(rec.expected_noise_reduction_pct)
59
+ ? ` (${formatPercent(rec.expected_noise_reduction_pct)} noise reduction)`
60
+ : '';
61
+ console.log(`${index + 1}) ${name}: ${action} ${current} -> ${suggested}${reduction}`);
62
+ });
63
+ } catch (e) {
64
+ printRequestError(e, 'Failed to tune alerts.', endpoint);
65
+ }
66
+ });
@@ -0,0 +1,110 @@
1
+ const { program } = require('commander');
2
+ const { prompt } = require('enquirer');
3
+ const chalk = require('chalk');
4
+ const { config } = require('../config');
5
+ const { DEFAULT_ENDPOINT, normalizeEndpoint, resolveEndpoint, resolveToken, request } = require('../api');
6
+ const { shouldShowHints, printRequestError } = require('../util');
7
+
8
+ program.command('login')
9
+ .description('Save an API token so the CLI can fetch telemetry.')
10
+ .option('--token <token>', 'API token (or use the prompt)')
11
+ .action(async (opts) => {
12
+ let token = opts.token;
13
+ if (!token) {
14
+ const res = await prompt({ type: 'password', name: 'token', message: 'Cloudflare API token (read-only ok):' });
15
+ token = res.token;
16
+ }
17
+ if (!token) {
18
+ console.log(chalk.red('No token provided.'));
19
+ return;
20
+ }
21
+ config.set('token', token);
22
+ console.log(chalk.green('✔ Authenticated. Next: gpxe init')); if (shouldShowHints()) {
23
+ console.log(chalk.gray('Tip: set GRIDPOINT_TOKEN to override this device setting.'));
24
+ }
25
+ });
26
+
27
+ program.command('init')
28
+ .description('Store the Gridpoint engine endpoint for this device.')
29
+ .option('--endpoint <url>', 'Override endpoint')
30
+ .action(async (opts) => {
31
+ let endpoint = opts.endpoint || process.env.GRIDPOINT_ENDPOINT;
32
+ if (!endpoint) {
33
+ const res = await prompt({
34
+ type: 'input',
35
+ name: 'endpoint',
36
+ message: 'Gridpoint engine endpoint (press enter for default):',
37
+ initial: DEFAULT_ENDPOINT
38
+ });
39
+ endpoint = res.endpoint;
40
+ }
41
+ if (!endpoint) {
42
+ console.log(chalk.red('No endpoint provided.'));
43
+ return;
44
+ }
45
+ endpoint = normalizeEndpoint(endpoint);
46
+ config.set('endpoint', endpoint);
47
+ console.log(chalk.green(`✔ Linked to ${endpoint}. Next: gpxe status`)); if (shouldShowHints()) {
48
+ console.log(chalk.gray('Tip: set GRIDPOINT_ENDPOINT to override this device setting.'));
49
+ }
50
+ });
51
+
52
+ program.command('onboard')
53
+ .description('Guided onboarding for tokens, endpoints, and workspace billing.')
54
+ .option('--endpoint <url>', 'Override endpoint')
55
+ .option('--token <token>', 'API token')
56
+ .option('--workspace <id>', 'Workspace identifier')
57
+ .action(async (opts) => {
58
+ let token = opts.token || resolveToken();
59
+ if (!token) {
60
+ const res = await prompt({ type: 'password', name: 'token', message: 'API token:' });
61
+ token = res.token;
62
+ }
63
+ if (!token) {
64
+ console.log(chalk.red('No token provided.'));
65
+ return;
66
+ }
67
+ config.set('token', token);
68
+
69
+ let endpoint = normalizeEndpoint(opts.endpoint || process.env.GRIDPOINT_ENDPOINT || config.get('endpoint'));
70
+ if (!endpoint) {
71
+ const res = await prompt({
72
+ type: 'input',
73
+ name: 'endpoint',
74
+ message: 'Gridpoint engine endpoint:',
75
+ initial: DEFAULT_ENDPOINT
76
+ });
77
+ endpoint = normalizeEndpoint(res.endpoint);
78
+ }
79
+ if (!endpoint) {
80
+ console.log(chalk.red('No endpoint provided.'));
81
+ return;
82
+ }
83
+ config.set('endpoint', endpoint);
84
+
85
+ let workspaceId = opts.workspace || process.env.GRIDPOINT_WORKSPACE_ID || config.get('workspace_id');
86
+ if (!workspaceId) {
87
+ const res = await prompt({
88
+ type: 'input',
89
+ name: 'workspace',
90
+ message: 'Workspace id (used for billing attribution):'
91
+ });
92
+ workspaceId = res.workspace;
93
+ }
94
+ if (workspaceId) {
95
+ config.set('workspace_id', workspaceId);
96
+ }
97
+
98
+ console.log(chalk.green('✔ Workspace connected.'));
99
+ try {
100
+ const params = workspaceId ? { workspace_id: workspaceId } : undefined;
101
+ const data = (await request('get', endpoint, '/billing/account', null, params)).data || {};
102
+ const account = data.account || {};
103
+ console.log(`Plan: ${account.plan || 'starter'} (${account.status || 'active'})`);
104
+ if (account.portal_url) {
105
+ console.log(`Billing portal: ${account.portal_url}`);
106
+ }
107
+ } catch (e) {
108
+ printRequestError(e, 'Unable to load billing account.', endpoint);
109
+ }
110
+ console.log(chalk.gray('Next: gpxe status'));});
@@ -0,0 +1,118 @@
1
+ const { program } = require('commander');
2
+ const chalk = require('chalk');
3
+ const { resolveEndpoint, request } = require('../api');
4
+ const { parseQuantity, printRequestError, wantsJSON, formatCents } = require('../util');
5
+ const { isDemo, demoBillingStatus, demoBillingUsageReport } = require('../demo');
6
+
7
+ const billingCommand = program.command('billing').description('Billing and usage commands.');
8
+
9
+ billingCommand
10
+ .command('status')
11
+ .description('Show billing status, usage, and invoices.')
12
+ .option('--account <id>', 'Billing account id')
13
+ .option('--workspace <id>', 'Workspace id')
14
+ .option('--endpoint <url>', 'Override endpoint')
15
+ .option('--json', 'Output raw JSON')
16
+ .option('--demo', 'Use demo data')
17
+ .action(async (opts) => {
18
+ const endpoint = resolveEndpoint(opts.endpoint);
19
+ const params = {};
20
+ if (opts.account) params.account_id = opts.account;
21
+ if (opts.workspace) params.workspace_id = opts.workspace;
22
+ try {
23
+ const data = isDemo(opts)
24
+ ? demoBillingStatus()
25
+ : (await request('get', endpoint, '/billing/account', null, params)).data || {};
26
+ if (opts.json || wantsJSON()) {
27
+ console.log(JSON.stringify(data, null, 2));
28
+ return;
29
+ }
30
+ const account = data.account;
31
+ if (!account) {
32
+ console.log(chalk.yellow('No billing account on file.'));
33
+ return;
34
+ }
35
+ console.log(chalk.cyan('Billing account'));
36
+ console.log(`Account: ${account.id}`);
37
+ if (account.workspace_id) console.log(`Workspace: ${account.workspace_id}`);
38
+ console.log(`Plan: ${account.plan} (${account.status || 'active'})`);
39
+ if (account.usage_tier) console.log(`Usage tier: ${account.usage_tier}`);
40
+ if (account.period_start || account.period_end) {
41
+ console.log(`Period: ${account.period_start || 'n/a'} -> ${account.period_end || 'n/a'}`);
42
+ }
43
+ if (data.usage) {
44
+ const limit = account.usage_limit;
45
+ const total = data.usage.total ?? 0;
46
+ const usageLine = limit
47
+ ? `${total.toLocaleString('en-US')} / ${limit.toLocaleString('en-US')} ${data.usage.unit || ''}`
48
+ : `${total.toLocaleString('en-US')} ${data.usage.unit || ''}`;
49
+ console.log(`Usage (${data.usage.month}): ${usageLine.trim()}`);
50
+ }
51
+ if (account.payment_method?.brand || account.payment_method?.last4) {
52
+ console.log(
53
+ `Payment method: ${account.payment_method?.brand || 'Card'} ${account.payment_method?.last4 ? `•••• ${account.payment_method.last4}` : ''}`.trim()
54
+ );
55
+ }
56
+ if (account.next_invoice?.amount) {
57
+ console.log(
58
+ `Next invoice: ${formatCents(account.next_invoice.amount, account.next_invoice.currency)} due ${account.next_invoice.due_at || 'n/a'}`
59
+ );
60
+ }
61
+ if (Array.isArray(data.invoices) && data.invoices.length > 0) {
62
+ console.log('');
63
+ console.log('Recent invoices:');
64
+ data.invoices.slice(0, 5).forEach((invoice) => {
65
+ const amount = formatCents(invoice.amount_due, invoice.currency);
66
+ console.log(`- ${invoice.number || invoice.id} ${amount} (${invoice.status || 'unknown'})`);
67
+ });
68
+ }
69
+ } catch (e) {
70
+ printRequestError(e, 'Failed to fetch billing status.', endpoint);
71
+ }
72
+ });
73
+
74
+ const billingUsageCommand = billingCommand.command('usage').description('Usage reporting commands.');
75
+
76
+ billingUsageCommand
77
+ .command('report')
78
+ .description('Report metered usage for billing.')
79
+ .option('--count <count>', 'Usage count to report')
80
+ .option('--service <name>', 'Service name')
81
+ .option('--unit <unit>', 'Usage unit (events, metrics, traces)', 'events')
82
+ .option('--account <id>', 'Billing account id')
83
+ .option('--workspace <id>', 'Workspace id')
84
+ .option('--source <source>', 'Usage source tag', 'cli')
85
+ .option('--endpoint <url>', 'Override endpoint')
86
+ .option('--json', 'Output raw JSON')
87
+ .option('--demo', 'Use demo data')
88
+ .action(async (opts) => {
89
+ const quantity = parseQuantity(opts.count);
90
+ if (!Number.isFinite(quantity) || quantity <= 0) {
91
+ console.log(chalk.red('Provide a positive usage count via --count.'));
92
+ return;
93
+ }
94
+ const endpoint = resolveEndpoint(opts.endpoint);
95
+ const payload = {
96
+ account_id: opts.account || null,
97
+ workspace_id: opts.workspace || null,
98
+ service: opts.service || null,
99
+ quantity,
100
+ unit: opts.unit || 'events',
101
+ source: opts.source || 'cli'
102
+ };
103
+ try {
104
+ const data = isDemo(opts)
105
+ ? demoBillingUsageReport(payload)
106
+ : (await request('post', endpoint, '/billing/usage', payload)).data || {};
107
+ if (opts.json || wantsJSON()) {
108
+ console.log(JSON.stringify(data, null, 2));
109
+ return;
110
+ }
111
+ console.log(chalk.cyan('Usage recorded'));
112
+ console.log(`Count: ${quantity.toLocaleString('en-US')} ${payload.unit}`);
113
+ if (payload.service) console.log(`Service: ${payload.service}`);
114
+ if (payload.workspace_id) console.log(`Workspace: ${payload.workspace_id}`);
115
+ } catch (e) {
116
+ printRequestError(e, 'Failed to report usage.', endpoint);
117
+ }
118
+ });
@@ -0,0 +1,62 @@
1
+ const { program } = require('commander');
2
+ const chalk = require('chalk');
3
+ const { resolveEndpoint, request } = require('../api');
4
+ const { toInt, printRequestError, shouldShowHints, formatPercent, formatCurrency } = require('../util');
5
+ const { isDemo, demoCapacityForecast } = require('../demo');
6
+
7
+ const capacityCommand = program.command('capacity').description('Capacity and cost intelligence commands.');
8
+
9
+ capacityCommand
10
+ .command('forecast')
11
+ .description('Forecast capacity and cost trends.')
12
+ .option('--window <window>', 'Forecast window (e.g. 30d)', '30d')
13
+ .option('--service <name>', 'Filter to a specific service')
14
+ .option('--endpoint <url>', 'Override endpoint')
15
+ .option('--json', 'Output raw JSON')
16
+ .option('--demo', 'Use demo data')
17
+ .action(async (opts) => {
18
+ const endpoint = resolveEndpoint(opts.endpoint);
19
+ const params = { window: opts.window };
20
+ if (opts.service) params.service = opts.service;
21
+ try {
22
+ const data = isDemo(opts)
23
+ ? demoCapacityForecast(params.window, params.service)
24
+ : (await request('get', endpoint, '/capacity/forecast', null, params)).data || {};
25
+ if (opts.json) {
26
+ console.log(JSON.stringify(data, null, 2));
27
+ return;
28
+ }
29
+ const windowLabel = data.window || opts.window;
30
+ const serviceLabel = data.service || opts.service || 'all services';
31
+ console.log(chalk.cyan(`Capacity forecast (service/${serviceLabel}, window: ${windowLabel})`));
32
+ const saturation = data.saturation || {};
33
+ if (Number.isFinite(saturation.days) || Number.isFinite(saturation.utilization_pct)) {
34
+ const days = Number.isFinite(saturation.days) ? `${saturation.days}d` : 'n/a';
35
+ const util = formatPercent(saturation.utilization_pct);
36
+ console.log(`Saturation window: ${days} (util ${util})`);
37
+ }
38
+ const cost = data.cost || {};
39
+ if (Number.isFinite(cost.projected_monthly_usd) || Number.isFinite(cost.delta_pct)) {
40
+ const monthly = formatCurrency(cost.projected_monthly_usd);
41
+ const delta = Number.isFinite(cost.delta_pct) ? ` (${formatPercent(cost.delta_pct)})` : '';
42
+ console.log(`Projected cost: ${monthly}${delta}`);
43
+ }
44
+ const recommendations = Array.isArray(data.recommendations) ? data.recommendations : [];
45
+ if (recommendations.length === 0) {
46
+ console.log('No capacity recommendations.');
47
+ if (shouldShowHints()) {
48
+ console.log(chalk.gray('Tip: ingest capacity forecasts or run with --demo.'));
49
+ }
50
+ return;
51
+ }
52
+ console.log('');
53
+ console.log('Recommendations:');
54
+ recommendations.forEach((rec, index) => {
55
+ const label = rec.action || 'action';
56
+ const detail = rec.detail || rec.note || 'Review capacity plan.';
57
+ console.log(`${index + 1}) ${label}: ${detail}`);
58
+ });
59
+ } catch (e) {
60
+ printRequestError(e, 'Failed to forecast capacity.', endpoint);
61
+ }
62
+ });
@@ -0,0 +1,51 @@
1
+ const { program } = require('commander');
2
+ const chalk = require('chalk');
3
+ const { resolveEndpoint, request } = require('../api');
4
+ const { toInt, printRequestError, shouldShowHints, formatImpact } = require('../util');
5
+ const { isDemo, demoChangesCorrelate } = require('../demo');
6
+
7
+ program.command('changes')
8
+ .description('Change intelligence commands.')
9
+ .command('correlate')
10
+ .description('List the most correlated recent changes.')
11
+ .option('--window <window>', 'Lookback window (e.g. 24h)', '24h')
12
+ .option('--service <name>', 'Filter to a specific service')
13
+ .option('--limit <count>', 'Max results to return', '10')
14
+ .option('--endpoint <url>', 'Override endpoint')
15
+ .option('--json', 'Output raw JSON')
16
+ .option('--demo', 'Use demo data')
17
+ .action(async (opts) => {
18
+ const endpoint = resolveEndpoint(opts.endpoint);
19
+ const params = {
20
+ window: opts.window,
21
+ limit: toInt(opts.limit, 10)
22
+ };
23
+ if (opts.service) params.service = opts.service;
24
+ try {
25
+ const data = isDemo(opts)
26
+ ? demoChangesCorrelate(params.window)
27
+ : (await request('get', endpoint, '/changes/correlate', null, params)).data || {};
28
+ if (opts.json) {
29
+ console.log(JSON.stringify(data, null, 2));
30
+ return;
31
+ }
32
+ const windowLabel = data.window || opts.window;
33
+ const results = Array.isArray(data.results) ? data.results : [];
34
+ console.log(chalk.cyan(`Top correlated changes (window: ${windowLabel})`));
35
+ if (results.length === 0) {
36
+ console.log('No correlated changes found.');
37
+ if (shouldShowHints()) {
38
+ console.log(chalk.gray('Tip: ingest change events or run with --demo.'));
39
+ }
40
+ return;
41
+ }
42
+ results.slice(0, params.limit).forEach((entry, index) => {
43
+ const label = [entry.type, entry.id].filter(Boolean).join(':') || 'change';
44
+ const score = Number.isFinite(entry.score) ? entry.score : 'n/a';
45
+ const impactLabel = formatImpact(entry.impact);
46
+ console.log(`${index + 1}) ${label} score: ${score} ${impactLabel}`);
47
+ });
48
+ } catch (e) {
49
+ printRequestError(e, 'Failed to correlate changes.', endpoint);
50
+ }
51
+ });
@@ -0,0 +1,72 @@
1
+ const { program } = require('commander');
2
+ const chalk = require('chalk');
3
+ const { config, CONFIG_PATH } = require('../config');
4
+ const { resolveEndpoint, resolveToken } = require('../api');
5
+ const { maskToken, shouldShowHints } = require('../util');
6
+
7
+ const configCommand = program.command('config')
8
+ .description('Show the saved endpoint and token status.')
9
+ .option('--json', 'Output raw JSON')
10
+ .action((opts) => {
11
+ const endpoint = config.get('endpoint');
12
+ const resolvedEndpoint = resolveEndpoint();
13
+ const token = resolveToken();
14
+ const workspaceId = config.get('workspace_id');
15
+ const accountId = config.get('account_id');
16
+ const payload = {
17
+ endpoint: endpoint || null,
18
+ resolvedEndpoint,
19
+ token: maskToken(token),
20
+ tokenSet: Boolean(token),
21
+ workspace_id: workspaceId || null,
22
+ account_id: accountId || null,
23
+ configPath: CONFIG_PATH
24
+ };
25
+ if (opts.json) {
26
+ console.log(JSON.stringify(payload, null, 2));
27
+ return;
28
+ }
29
+ console.log(chalk.cyan('Configuration'));
30
+ console.log(`Endpoint (configured): ${payload.endpoint || 'not set'}`);
31
+ console.log(`Endpoint (resolved): ${payload.resolvedEndpoint}`);
32
+ console.log(`Token: ${payload.token || 'not set'}`);
33
+ console.log(`Workspace: ${payload.workspace_id || 'not set'}`);
34
+ console.log(`Account: ${payload.account_id || 'not set'}`);
35
+ if (shouldShowHints()) {
36
+ console.log(chalk.gray(`Config file: ${CONFIG_PATH}`));
37
+ }
38
+ });
39
+
40
+ configCommand
41
+ .command('set <key> <value>')
42
+ .description('Set a configuration value.')
43
+ .action((key, value) => {
44
+ config.set(key, value);
45
+ const message = key === 'token' ? `token set to ${maskToken(value)}` : `${key} set to ${value}`;
46
+ console.log(chalk.green(`✔ ${message}`));
47
+ });
48
+
49
+ configCommand
50
+ .command('get <key>')
51
+ .description('Get a configuration value.')
52
+ .action((key) => {
53
+ const value = config.get(key);
54
+ if (value === null) {
55
+ console.log(chalk.yellow(`${key} is not set`));
56
+ return;
57
+ }
58
+ const output = key === 'token' ? maskToken(value) : value;
59
+ console.log(output);
60
+ });
61
+
62
+ configCommand
63
+ .command('unset <key>')
64
+ .description('Remove a configuration value.')
65
+ .action((key) => {
66
+ const removed = config.unset(key);
67
+ if (!removed) {
68
+ console.log(chalk.yellow(`${key} was not set`));
69
+ return;
70
+ }
71
+ console.log(chalk.green(`✔ ${key} removed`));
72
+ });