securenow 8.11.2 → 8.11.3

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/cli/monitor.js CHANGED
@@ -1,10 +1,10 @@
1
- 'use strict';
2
-
1
+ 'use strict';
2
+
3
3
  const { api, requireAuth } = require('./client');
4
4
  const config = require('./config');
5
5
  const ui = require('./ui');
6
6
  const appConfig = require('../app-config');
7
-
7
+
8
8
  function resolveApp(flags) {
9
9
  return flags.app || config.getDefaultApp();
10
10
  }
@@ -18,105 +18,105 @@ function addEnvironment(query, flags) {
18
18
  if (environment) query.environment = environment;
19
19
  return query;
20
20
  }
21
-
22
- // ── Traces ──
23
-
24
- async function tracesList(args, flags) {
25
- requireAuth();
26
- const appKey = resolveApp(flags);
27
- if (!appKey) {
28
- ui.error('No app specified. Use --app <key> or set a default with `securenow config set defaultApp <key>`');
29
- process.exit(1);
30
- }
31
-
32
- const s = ui.spinner('Fetching traces');
33
- try {
21
+
22
+ // ── Traces ──
23
+
24
+ async function tracesList(args, flags) {
25
+ requireAuth();
26
+ const appKey = resolveApp(flags);
27
+ if (!appKey) {
28
+ ui.error('No app specified. Use --app <key> or set a default with `securenow config set defaultApp <key>`');
29
+ process.exit(1);
30
+ }
31
+
32
+ const s = ui.spinner('Fetching traces');
33
+ try {
34
34
  const query = {
35
35
  appKeys: appKey,
36
36
  limit: flags.limit || 20,
37
37
  };
38
38
  addEnvironment(query, flags);
39
- if (flags.start) query.from = flags.start;
40
- if (flags.end) query.to = flags.end;
41
-
42
- const data = await api.get('/traces', { query });
43
- const traces = data.traces || [];
44
- s.stop(`Found ${traces.length} trace${traces.length !== 1 ? 's' : ''}`);
45
-
46
- if (flags.json) { ui.json(traces); return; }
47
-
48
- console.log('');
49
- const rows = traces.map(t => [
39
+ if (flags.start) query.from = flags.start;
40
+ if (flags.end) query.to = flags.end;
41
+
42
+ const data = await api.get('/traces', { query });
43
+ const traces = data.traces || [];
44
+ s.stop(`Found ${traces.length} trace${traces.length !== 1 ? 's' : ''}`);
45
+
46
+ if (flags.json) { ui.json(traces); return; }
47
+
48
+ console.log('');
49
+ const rows = traces.map(t => [
50
50
  ui.c.dim(ui.truncate(t.traceID || t.traceId || t._id, 16)),
51
51
  t.environment || 'production',
52
- t.operationName || t.name || t.serviceName || '—',
53
- ui.httpStatusColor(t.statusCode || t.httpStatusCode || t.responseStatusCode || '—'),
54
- ui.durationColor(t.durationNano ? t.durationNano / 1e6 : t.duration),
55
- t.httpMethod || t.method || '—',
56
- ui.truncate(t.httpUrl || t.url || t.httpRoute || '', 40),
57
- ui.timeAgo(t.timestamp),
58
- ]);
59
-
52
+ t.operationName || t.name || t.serviceName || '—',
53
+ ui.httpStatusColor(t.statusCode || t.httpStatusCode || t.responseStatusCode || '—'),
54
+ ui.durationColor(t.durationNano ? t.durationNano / 1e6 : t.duration),
55
+ t.httpMethod || t.method || '—',
56
+ ui.truncate(t.httpUrl || t.url || t.httpRoute || '', 40),
57
+ ui.timeAgo(t.timestamp),
58
+ ]);
59
+
60
60
  ui.table(['Trace ID', 'Env', 'Operation', 'Status', 'Duration', 'Method', 'URL', 'Time'], rows);
61
- console.log('');
62
- } catch (err) {
63
- s.fail('Failed to fetch traces');
64
- throw err;
65
- }
66
- }
67
-
68
- async function tracesShow(args, flags) {
69
- requireAuth();
70
- const traceId = args[0];
71
- if (!traceId) {
72
- ui.error('Trace ID is required. Usage: securenow traces show <traceId>');
73
- process.exit(1);
74
- }
75
-
76
- const s = ui.spinner('Fetching trace details');
77
- try {
61
+ console.log('');
62
+ } catch (err) {
63
+ s.fail('Failed to fetch traces');
64
+ throw err;
65
+ }
66
+ }
67
+
68
+ async function tracesShow(args, flags) {
69
+ requireAuth();
70
+ const traceId = args[0];
71
+ if (!traceId) {
72
+ ui.error('Trace ID is required. Usage: securenow traces show <traceId>');
73
+ process.exit(1);
74
+ }
75
+
76
+ const s = ui.spinner('Fetching trace details');
77
+ try {
78
78
  const appKey = resolveApp(flags);
79
79
  const traceQuery = appKey ? { appKeys: appKey } : {};
80
80
  addEnvironment(traceQuery, flags);
81
81
  const data = await api.get(`/traces/${traceId}`, { query: traceQuery });
82
- s.stop('Trace loaded');
83
-
84
- if (flags.json) { ui.json(data); return; }
85
-
86
- const spans = data.spans || [];
87
- console.log('');
88
- ui.heading(`Trace ${data.traceId || traceId}`);
89
-
90
- if (spans.length) {
91
- ui.subheading(`Spans (${spans.length})`);
92
- console.log('');
93
- const rows = spans.map(span => [
94
- ui.c.dim(ui.truncate(span.spanID || span.spanId, 16)),
95
- span.operationName || span.name || '—',
96
- ui.httpStatusColor(span.statusCode || span.responseStatusCode || '—'),
97
- ui.durationColor(span.durationNano ? span.durationNano / 1e6 : span.duration),
98
- span.kind || '—',
99
- ]);
100
- ui.table(['Span ID', 'Operation', 'Status', 'Duration', 'Kind'], rows);
101
- } else {
102
- console.log('');
103
- ui.info('No spans found for this trace.');
104
- }
105
- console.log('');
106
- } catch (err) {
107
- s.fail('Failed to fetch trace');
108
- throw err;
109
- }
110
- }
111
-
112
- async function tracesAnalyze(args, flags) {
113
- requireAuth();
114
- const traceId = args[0];
115
- if (!traceId) {
116
- ui.error('Trace ID is required. Usage: securenow traces analyze <traceId>');
117
- process.exit(1);
118
- }
119
-
82
+ s.stop('Trace loaded');
83
+
84
+ if (flags.json) { ui.json(data); return; }
85
+
86
+ const spans = data.spans || [];
87
+ console.log('');
88
+ ui.heading(`Trace ${data.traceId || traceId}`);
89
+
90
+ if (spans.length) {
91
+ ui.subheading(`Spans (${spans.length})`);
92
+ console.log('');
93
+ const rows = spans.map(span => [
94
+ ui.c.dim(ui.truncate(span.spanID || span.spanId, 16)),
95
+ span.operationName || span.name || '—',
96
+ ui.httpStatusColor(span.statusCode || span.responseStatusCode || '—'),
97
+ ui.durationColor(span.durationNano ? span.durationNano / 1e6 : span.duration),
98
+ span.kind || '—',
99
+ ]);
100
+ ui.table(['Span ID', 'Operation', 'Status', 'Duration', 'Kind'], rows);
101
+ } else {
102
+ console.log('');
103
+ ui.info('No spans found for this trace.');
104
+ }
105
+ console.log('');
106
+ } catch (err) {
107
+ s.fail('Failed to fetch trace');
108
+ throw err;
109
+ }
110
+ }
111
+
112
+ async function tracesAnalyze(args, flags) {
113
+ requireAuth();
114
+ const traceId = args[0];
115
+ if (!traceId) {
116
+ ui.error('Trace ID is required. Usage: securenow traces analyze <traceId>');
117
+ process.exit(1);
118
+ }
119
+
120
120
  const s = ui.spinner('Analyzing trace with AI');
121
121
  try {
122
122
  const environment = resolveEnvironment(flags);
@@ -140,167 +140,167 @@ async function tracesAnalyze(args, flags) {
140
140
  };
141
141
  if (environment) body.environment = environment;
142
142
  let result = await api.post('/traces/analyze', body);
143
-
144
- if (result.analysisId && result.status === 'running') {
145
- const analysisId = result.analysisId;
146
- for (let i = 0; i < 120; i++) {
147
- await new Promise(r => setTimeout(r, 3000));
148
- result = await api.get(`/traces/analyze/${analysisId}`);
149
- if (result.status === 'completed' || result.status === 'failed') break;
150
- }
151
- if (result.status === 'failed') throw new Error(result.error || 'Analysis failed');
152
- if (result.status === 'running') throw new Error('Analysis timed out');
153
- }
154
-
155
- s.stop('Analysis complete');
156
-
157
- if (flags.json) { ui.json(result); return; }
158
-
159
- const analysis = result.analysis;
160
- console.log('');
161
- ui.heading('AI Trace Analysis');
162
- console.log('');
163
-
164
- if (typeof analysis === 'object' && analysis !== null) {
165
- if (analysis.summary) {
166
- ui.subheading('Summary');
167
- console.log(`\n ${analysis.summary}\n`);
168
- }
169
- if (analysis.riskLevel) {
170
- console.log(` ${ui.c.bold('Risk Level:')} ${ui.statusBadge(analysis.riskLevel)}\n`);
171
- }
172
- if (analysis.securityIssues?.length) {
173
- ui.subheading('Security Issues');
174
- console.log('');
175
- analysis.securityIssues.forEach((issue, i) => {
176
- console.log(` ${i + 1}. ${typeof issue === 'string' ? issue : issue.description || JSON.stringify(issue)}`);
177
- });
178
- console.log('');
179
- }
180
- if (analysis.recommendations?.length) {
181
- ui.subheading('Recommendations');
182
- console.log('');
183
- analysis.recommendations.forEach((rec, i) => {
184
- console.log(` ${i + 1}. ${typeof rec === 'string' ? rec : rec.description || JSON.stringify(rec)}`);
185
- });
186
- console.log('');
187
- }
188
- } else {
189
- console.log(analysis || JSON.stringify(result, null, 2));
190
- console.log('');
191
- }
192
- } catch (err) {
193
- s.fail('Analysis failed');
194
- throw err;
195
- }
196
- }
197
-
198
- // ── Logs ──
199
-
200
- // Parse a duration string like "6h", "30m", "2d", "90s" into minutes.
201
- // Returns null on unparseable input so the caller can error out clearly
202
- // instead of silently falling through to a default.
203
- function parseDurationToMinutes(value) {
204
- if (value == null || value === '') return null;
205
- const m = String(value).trim().match(/^(\d+)\s*(s|m|h|d)?$/i);
206
- if (!m) return null;
207
- const n = parseInt(m[1], 10);
208
- const unit = (m[2] || 'm').toLowerCase();
209
- if (unit === 's') return Math.max(1, Math.round(n / 60));
210
- if (unit === 'm') return n;
211
- if (unit === 'h') return n * 60;
212
- if (unit === 'd') return n * 60 * 24;
213
- return null;
214
- }
215
-
216
- const LOG_LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'WARNING', 'ERROR', 'FATAL'];
217
-
218
- async function logsList(args, flags) {
219
- requireAuth();
220
- const appKey = resolveApp(flags);
221
- if (!appKey) {
222
- ui.error('No app specified. Use --app <key> or set a default.');
223
- process.exit(1);
224
- }
225
-
226
- let minutes = 60;
227
- if (flags.since != null) {
228
- const parsed = parseDurationToMinutes(flags.since);
229
- if (parsed == null) {
230
- ui.error(`Invalid --since value "${flags.since}". Expected e.g. 30m, 6h, 2d.`);
231
- process.exit(1);
232
- }
233
- minutes = parsed;
234
- } else if (flags.minutes != null) {
235
- const n = parseInt(flags.minutes, 10);
236
- if (isNaN(n) || n <= 0) {
237
- ui.error(`Invalid --minutes value "${flags.minutes}".`);
238
- process.exit(1);
239
- }
240
- minutes = n;
241
- }
242
-
243
- let severity = null;
244
- if (flags.level) {
245
- severity = String(flags.level).toUpperCase();
246
- if (!LOG_LEVELS.includes(severity)) {
247
- ui.error(`Invalid --level "${flags.level}". Expected one of: ${LOG_LEVELS.join(', ').toLowerCase()}.`);
248
- process.exit(1);
249
- }
250
- }
251
-
252
- const s = ui.spinner('Fetching logs');
253
- try {
254
- const now = Date.now();
255
- const query = {
256
- appKeys: appKey,
257
- limit: flags.limit || 200,
258
- from: flags.start || new Date(now - minutes * 60 * 1000).toISOString(),
259
- to: flags.end || new Date(now).toISOString(),
143
+
144
+ if (result.analysisId && result.status === 'running') {
145
+ const analysisId = result.analysisId;
146
+ for (let i = 0; i < 120; i++) {
147
+ await new Promise(r => setTimeout(r, 3000));
148
+ result = await api.get(`/traces/analyze/${analysisId}`);
149
+ if (result.status === 'completed' || result.status === 'failed') break;
150
+ }
151
+ if (result.status === 'failed') throw new Error(result.error || 'Analysis failed');
152
+ if (result.status === 'running') throw new Error('Analysis timed out');
153
+ }
154
+
155
+ s.stop('Analysis complete');
156
+
157
+ if (flags.json) { ui.json(result); return; }
158
+
159
+ const analysis = result.analysis;
160
+ console.log('');
161
+ ui.heading('AI Trace Analysis');
162
+ console.log('');
163
+
164
+ if (typeof analysis === 'object' && analysis !== null) {
165
+ if (analysis.summary) {
166
+ ui.subheading('Summary');
167
+ console.log(`\n ${analysis.summary}\n`);
168
+ }
169
+ if (analysis.riskLevel) {
170
+ console.log(` ${ui.c.bold('Risk Level:')} ${ui.statusBadge(analysis.riskLevel)}\n`);
171
+ }
172
+ if (analysis.securityIssues?.length) {
173
+ ui.subheading('Security Issues');
174
+ console.log('');
175
+ analysis.securityIssues.forEach((issue, i) => {
176
+ console.log(` ${i + 1}. ${typeof issue === 'string' ? issue : issue.description || JSON.stringify(issue)}`);
177
+ });
178
+ console.log('');
179
+ }
180
+ if (analysis.recommendations?.length) {
181
+ ui.subheading('Recommendations');
182
+ console.log('');
183
+ analysis.recommendations.forEach((rec, i) => {
184
+ console.log(` ${i + 1}. ${typeof rec === 'string' ? rec : rec.description || JSON.stringify(rec)}`);
185
+ });
186
+ console.log('');
187
+ }
188
+ } else {
189
+ console.log(analysis || JSON.stringify(result, null, 2));
190
+ console.log('');
191
+ }
192
+ } catch (err) {
193
+ s.fail('Analysis failed');
194
+ throw err;
195
+ }
196
+ }
197
+
198
+ // ── Logs ──
199
+
200
+ // Parse a duration string like "6h", "30m", "2d", "90s" into minutes.
201
+ // Returns null on unparseable input so the caller can error out clearly
202
+ // instead of silently falling through to a default.
203
+ function parseDurationToMinutes(value) {
204
+ if (value == null || value === '') return null;
205
+ const m = String(value).trim().match(/^(\d+)\s*(s|m|h|d)?$/i);
206
+ if (!m) return null;
207
+ const n = parseInt(m[1], 10);
208
+ const unit = (m[2] || 'm').toLowerCase();
209
+ if (unit === 's') return Math.max(1, Math.round(n / 60));
210
+ if (unit === 'm') return n;
211
+ if (unit === 'h') return n * 60;
212
+ if (unit === 'd') return n * 60 * 24;
213
+ return null;
214
+ }
215
+
216
+ const LOG_LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'WARNING', 'ERROR', 'FATAL'];
217
+
218
+ async function logsList(args, flags) {
219
+ requireAuth();
220
+ const appKey = resolveApp(flags);
221
+ if (!appKey) {
222
+ ui.error('No app specified. Use --app <key> or set a default.');
223
+ process.exit(1);
224
+ }
225
+
226
+ let minutes = 60;
227
+ if (flags.since != null) {
228
+ const parsed = parseDurationToMinutes(flags.since);
229
+ if (parsed == null) {
230
+ ui.error(`Invalid --since value "${flags.since}". Expected e.g. 30m, 6h, 2d.`);
231
+ process.exit(1);
232
+ }
233
+ minutes = parsed;
234
+ } else if (flags.minutes != null) {
235
+ const n = parseInt(flags.minutes, 10);
236
+ if (isNaN(n) || n <= 0) {
237
+ ui.error(`Invalid --minutes value "${flags.minutes}".`);
238
+ process.exit(1);
239
+ }
240
+ minutes = n;
241
+ }
242
+
243
+ let severity = null;
244
+ if (flags.level) {
245
+ severity = String(flags.level).toUpperCase();
246
+ if (!LOG_LEVELS.includes(severity)) {
247
+ ui.error(`Invalid --level "${flags.level}". Expected one of: ${LOG_LEVELS.join(', ').toLowerCase()}.`);
248
+ process.exit(1);
249
+ }
250
+ }
251
+
252
+ const s = ui.spinner('Fetching logs');
253
+ try {
254
+ const now = Date.now();
255
+ const query = {
256
+ appKeys: appKey,
257
+ limit: flags.limit || 200,
258
+ from: flags.start || new Date(now - minutes * 60 * 1000).toISOString(),
259
+ to: flags.end || new Date(now).toISOString(),
260
260
  };
261
261
  if (severity) query.severity = severity;
262
262
  addEnvironment(query, flags);
263
263
 
264
264
  const data = await api.get('/logs', { query });
265
- const logs = data.logs || [];
266
- s.stop(`Found ${logs.length} log${logs.length !== 1 ? 's' : ''}`);
267
-
268
- if (flags.json) { ui.json(logs); return; }
269
-
270
- console.log('');
271
- for (const log of logs) {
272
- const level = (log.severityText || log.level || 'INFO').toUpperCase();
273
- const levelColor = {
274
- ERROR: ui.c.red, FATAL: ui.c.red, WARN: ui.c.yellow, WARNING: ui.c.yellow,
275
- INFO: ui.c.cyan, DEBUG: ui.c.dim, TRACE: ui.c.dim,
276
- }[level] || ui.c.white;
277
-
278
- const parsedTime = ui.parseTimestamp(log.timestamp);
279
- const time = ui.c.dim(parsedTime ? parsedTime.toLocaleTimeString() : '');
280
- const body = log.body || log.message || log.severityText || '';
281
-
265
+ const logs = data.logs || [];
266
+ s.stop(`Found ${logs.length} log${logs.length !== 1 ? 's' : ''}`);
267
+
268
+ if (flags.json) { ui.json(logs); return; }
269
+
270
+ console.log('');
271
+ for (const log of logs) {
272
+ const level = (log.severityText || log.level || 'INFO').toUpperCase();
273
+ const levelColor = {
274
+ ERROR: ui.c.red, FATAL: ui.c.red, WARN: ui.c.yellow, WARNING: ui.c.yellow,
275
+ INFO: ui.c.cyan, DEBUG: ui.c.dim, TRACE: ui.c.dim,
276
+ }[level] || ui.c.white;
277
+
278
+ const parsedTime = ui.parseTimestamp(log.timestamp);
279
+ const time = ui.c.dim(parsedTime ? parsedTime.toLocaleTimeString() : '');
280
+ const body = log.body || log.message || log.severityText || '';
281
+
282
282
  const env = log.environment ? ` ${ui.c.dim(`[${log.environment}]`)}` : '';
283
283
  console.log(` ${time} ${levelColor(level.padEnd(7))}${env} ${body}`);
284
-
285
- if (log.traceId && flags.verbose) {
286
- console.log(` ${ui.c.dim(` trace=${log.traceId} span=${log.spanId || ''}`)}`);
287
- }
288
- }
289
- console.log('');
290
- } catch (err) {
291
- s.fail('Failed to fetch logs');
292
- throw err;
293
- }
294
- }
295
-
296
- async function logsTrace(args, flags) {
297
- requireAuth();
298
- const traceId = args[0];
299
- if (!traceId) {
300
- ui.error('Trace ID required. Usage: securenow logs trace <traceId>');
301
- process.exit(1);
302
- }
303
-
284
+
285
+ if (log.traceId && flags.verbose) {
286
+ console.log(` ${ui.c.dim(` trace=${log.traceId} span=${log.spanId || ''}`)}`);
287
+ }
288
+ }
289
+ console.log('');
290
+ } catch (err) {
291
+ s.fail('Failed to fetch logs');
292
+ throw err;
293
+ }
294
+ }
295
+
296
+ async function logsTrace(args, flags) {
297
+ requireAuth();
298
+ const traceId = args[0];
299
+ if (!traceId) {
300
+ ui.error('Trace ID required. Usage: securenow logs trace <traceId>');
301
+ process.exit(1);
302
+ }
303
+
304
304
  const s = ui.spinner('Fetching logs for trace');
305
305
  try {
306
306
  const query = {};
@@ -308,144 +308,144 @@ async function logsTrace(args, flags) {
308
308
  if (appKey) query.appKeys = appKey;
309
309
  addEnvironment(query, flags);
310
310
  const data = await api.get(`/logs/trace/${traceId}`, { query });
311
- const logs = data.logs || [];
312
- s.stop(`Found ${logs.length} log${logs.length !== 1 ? 's' : ''}`);
313
-
314
- if (flags.json) { ui.json(logs); return; }
315
-
316
- console.log('');
317
- for (const log of logs) {
318
- const level = (log.severityText || log.level || 'INFO').toUpperCase();
319
- const levelColor = {
320
- ERROR: ui.c.red, FATAL: ui.c.red, WARN: ui.c.yellow, WARNING: ui.c.yellow,
321
- INFO: ui.c.cyan, DEBUG: ui.c.dim, TRACE: ui.c.dim,
322
- }[level] || ui.c.white;
323
-
324
- const parsedTime = ui.parseTimestamp(log.timestamp);
325
- const time = ui.c.dim(parsedTime ? parsedTime.toLocaleTimeString() : '');
311
+ const logs = data.logs || [];
312
+ s.stop(`Found ${logs.length} log${logs.length !== 1 ? 's' : ''}`);
313
+
314
+ if (flags.json) { ui.json(logs); return; }
315
+
316
+ console.log('');
317
+ for (const log of logs) {
318
+ const level = (log.severityText || log.level || 'INFO').toUpperCase();
319
+ const levelColor = {
320
+ ERROR: ui.c.red, FATAL: ui.c.red, WARN: ui.c.yellow, WARNING: ui.c.yellow,
321
+ INFO: ui.c.cyan, DEBUG: ui.c.dim, TRACE: ui.c.dim,
322
+ }[level] || ui.c.white;
323
+
324
+ const parsedTime = ui.parseTimestamp(log.timestamp);
325
+ const time = ui.c.dim(parsedTime ? parsedTime.toLocaleTimeString() : '');
326
326
  const env = log.environment ? ` ${ui.c.dim(`[${log.environment}]`)}` : '';
327
327
  console.log(` ${time} ${levelColor(level.padEnd(7))}${env} ${log.body || log.message || ''}`);
328
- }
329
- console.log('');
330
- } catch (err) {
331
- s.fail('Failed to fetch logs');
332
- throw err;
333
- }
334
- }
335
-
336
-
337
- // ── Notifications ──
338
-
339
- async function notificationsList(args, flags) {
340
- requireAuth();
341
- const s = ui.spinner('Fetching notifications');
342
- try {
343
- const query = { limit: flags.limit || 20, page: flags.page || 1 };
344
- const data = await api.get('/notifications', { query });
345
- const notifications = data.notifications || [];
346
- const pagination = data.pagination;
347
- s.stop(`Found ${notifications.length} notification${notifications.length !== 1 ? 's' : ''}${pagination ? ` (page ${pagination.page}/${pagination.totalPages})` : ''}`);
348
-
349
- if (flags.json) { ui.json(data); return; }
350
-
351
- console.log('');
352
- const rows = notifications.map(n => [
353
- ui.c.dim(ui.truncate(n._id, 12)),
354
- ui.statusBadge(n.read ? 'read' : 'unread'),
355
- ui.truncate(n.title || n.message || n.type || '', 50),
356
- n.ip || '—',
357
- n.severity ? ui.statusBadge(n.severity) : '—',
358
- ui.timeAgo(n.createdAt),
359
- ]);
360
-
361
- ui.table(['ID', 'Status', 'Title', 'IP', 'Severity', 'Time'], rows);
362
- console.log('');
363
- } catch (err) {
364
- s.fail('Failed to fetch notifications');
365
- throw err;
366
- }
367
- }
368
-
369
- async function notificationsRead(args, flags) {
370
- requireAuth();
371
- const id = args[0];
372
- if (!id) {
373
- ui.error('Notification ID required. Usage: securenow notifications read <id>');
374
- process.exit(1);
375
- }
376
-
377
- const s = ui.spinner('Marking as read');
378
- try {
379
- await api.put(`/notifications/${id}/read`);
380
- s.stop('Notification marked as read');
381
- } catch (err) {
382
- s.fail('Failed to mark notification');
383
- throw err;
384
- }
385
- }
386
-
387
- async function notificationsReadAll() {
388
- requireAuth();
389
- const s = ui.spinner('Marking all as read');
390
- try {
391
- await api.put('/notifications/read-all');
392
- s.stop('All notifications marked as read');
393
- } catch (err) {
394
- s.fail('Failed to mark notifications');
395
- throw err;
396
- }
397
- }
398
-
399
- async function notificationsUnread() {
400
- requireAuth();
401
- try {
402
- const data = await api.get('/notifications/unread-count');
403
- const count = data.count ?? 0;
404
- console.log(`\n ${ui.c.bold(String(count))} unread notification${count !== 1 ? 's' : ''}\n`);
405
- } catch (err) {
406
- throw err;
407
- }
408
- }
409
-
410
- // ── Status / Dashboard Overview ──
411
-
412
- async function status(args, flags) {
413
- requireAuth();
414
- const s = ui.spinner('Fetching dashboard overview');
415
- try {
416
- const [appsData, unreadData] = await Promise.all([
417
- api.get('/applications'),
418
- api.get('/notifications/unread-count').catch(() => ({ count: 0 })),
419
- ]);
420
-
421
- const apps = appsData.applications || [];
422
- s.stop('Dashboard loaded');
423
-
424
- console.log('');
425
- ui.heading('SecureNow Dashboard');
426
- console.log('');
427
-
428
- ui.keyValue([
429
- ['Applications', String(apps.length)],
430
- ['Unread Alerts', String(unreadData.count ?? 0)],
431
- ]);
432
-
433
- if (apps.length > 0) {
434
- ui.subheading('Applications');
435
- console.log('');
436
- const rows = apps.map(app => [
437
- app.name,
438
- ui.c.dim(app.key),
439
- app.hosts?.length ? app.hosts.join(', ') : ui.c.dim('—'),
440
- ]);
441
- ui.table(['Name', 'Key', 'Hosts'], rows);
442
- }
443
-
328
+ }
329
+ console.log('');
330
+ } catch (err) {
331
+ s.fail('Failed to fetch logs');
332
+ throw err;
333
+ }
334
+ }
335
+
336
+
337
+ // ── Notifications ──
338
+
339
+ async function notificationsList(args, flags) {
340
+ requireAuth();
341
+ const s = ui.spinner('Fetching notifications');
342
+ try {
343
+ const query = { limit: flags.limit || 20, page: flags.page || 1 };
344
+ const data = await api.get('/notifications', { query });
345
+ const notifications = data.notifications || [];
346
+ const pagination = data.pagination;
347
+ s.stop(`Found ${notifications.length} notification${notifications.length !== 1 ? 's' : ''}${pagination ? ` (page ${pagination.page}/${pagination.totalPages})` : ''}`);
348
+
349
+ if (flags.json) { ui.json(data); return; }
350
+
351
+ console.log('');
352
+ const rows = notifications.map(n => [
353
+ ui.c.dim(ui.truncate(n._id, 12)),
354
+ ui.statusBadge(n.read ? 'read' : 'unread'),
355
+ ui.truncate(n.title || n.message || n.type || '', 50),
356
+ n.ip || '—',
357
+ n.severity ? ui.statusBadge(n.severity) : '—',
358
+ ui.timeAgo(n.createdAt),
359
+ ]);
360
+
361
+ ui.table(['ID', 'Status', 'Title', 'IP', 'Severity', 'Time'], rows);
362
+ console.log('');
363
+ } catch (err) {
364
+ s.fail('Failed to fetch notifications');
365
+ throw err;
366
+ }
367
+ }
368
+
369
+ async function notificationsRead(args, flags) {
370
+ requireAuth();
371
+ const id = args[0];
372
+ if (!id) {
373
+ ui.error('Notification ID required. Usage: securenow notifications read <id>');
374
+ process.exit(1);
375
+ }
376
+
377
+ const s = ui.spinner('Marking as read');
378
+ try {
379
+ await api.put(`/notifications/${id}/read`);
380
+ s.stop('Notification marked as read');
381
+ } catch (err) {
382
+ s.fail('Failed to mark notification');
383
+ throw err;
384
+ }
385
+ }
386
+
387
+ async function notificationsReadAll() {
388
+ requireAuth();
389
+ const s = ui.spinner('Marking all as read');
390
+ try {
391
+ await api.put('/notifications/read-all');
392
+ s.stop('All notifications marked as read');
393
+ } catch (err) {
394
+ s.fail('Failed to mark notifications');
395
+ throw err;
396
+ }
397
+ }
398
+
399
+ async function notificationsUnread() {
400
+ requireAuth();
401
+ try {
402
+ const data = await api.get('/notifications/unread-count');
403
+ const count = data.count ?? 0;
404
+ console.log(`\n ${ui.c.bold(String(count))} unread notification${count !== 1 ? 's' : ''}\n`);
405
+ } catch (err) {
406
+ throw err;
407
+ }
408
+ }
409
+
410
+ // ── Status / Dashboard Overview ──
411
+
412
+ async function status(args, flags) {
413
+ requireAuth();
414
+ const s = ui.spinner('Fetching dashboard overview');
415
+ try {
416
+ const [appsData, unreadData] = await Promise.all([
417
+ api.get('/applications'),
418
+ api.get('/notifications/unread-count').catch(() => ({ count: 0 })),
419
+ ]);
420
+
421
+ const apps = appsData.applications || [];
422
+ s.stop('Dashboard loaded');
423
+
424
+ console.log('');
425
+ ui.heading('SecureNow Dashboard');
426
+ console.log('');
427
+
428
+ ui.keyValue([
429
+ ['Applications', String(apps.length)],
430
+ ['Unread Alerts', String(unreadData.count ?? 0)],
431
+ ]);
432
+
433
+ if (apps.length > 0) {
434
+ ui.subheading('Applications');
435
+ console.log('');
436
+ const rows = apps.map(app => [
437
+ app.name,
438
+ ui.c.dim(app.key),
439
+ app.hosts?.length ? app.hosts.join(', ') : ui.c.dim('—'),
440
+ ]);
441
+ ui.table(['Name', 'Key', 'Hosts'], rows);
442
+ }
443
+
444
444
  const appKey = resolveApp(flags);
445
445
  const requestedEnv = resolveEnvironment(flags) || appConfig.resolveDeploymentEnvironment() || 'production';
446
446
  const showAllEnvs = requestedEnv === 'all' || requestedEnv === '*';
447
447
  {
448
- try {
448
+ try {
449
449
  const protectionData = await api.get('/applications/protection-status');
450
450
  const statuses = protectionData.statuses;
451
451
  if (statuses && Object.keys(statuses).length > 0) {
@@ -480,25 +480,25 @@ async function status(args, flags) {
480
480
  }
481
481
 
482
482
  }
483
- } catch {}
484
- }
485
-
486
- console.log('');
487
- } catch (err) {
488
- s.fail('Failed to load dashboard');
489
- throw err;
490
- }
491
- }
492
-
493
- module.exports = {
494
- tracesList,
495
- tracesShow,
496
- tracesAnalyze,
497
- logsList,
498
- logsTrace,
499
- notificationsList,
500
- notificationsRead,
501
- notificationsReadAll,
502
- notificationsUnread,
503
- status,
504
- };
483
+ } catch {}
484
+ }
485
+
486
+ console.log('');
487
+ } catch (err) {
488
+ s.fail('Failed to load dashboard');
489
+ throw err;
490
+ }
491
+ }
492
+
493
+ module.exports = {
494
+ tracesList,
495
+ tracesShow,
496
+ tracesAnalyze,
497
+ logsList,
498
+ logsTrace,
499
+ notificationsList,
500
+ notificationsRead,
501
+ notificationsReadAll,
502
+ notificationsUnread,
503
+ status,
504
+ };