securenow 8.0.3 → 8.2.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/cli.js CHANGED
@@ -1,44 +1,44 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const ui = require('./cli/ui');
5
-
6
- // ── Argument Parser ──
7
-
8
- function parseArgs(argv) {
9
- const positional = [];
10
- const flags = {};
11
-
12
- for (let i = 0; i < argv.length; i++) {
13
- const arg = argv[i];
14
- if (arg.startsWith('--')) {
15
- const eqIdx = arg.indexOf('=');
16
- if (eqIdx !== -1) {
17
- flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
18
- } else {
19
- const next = argv[i + 1];
20
- if (next && !next.startsWith('-')) {
21
- flags[arg.slice(2)] = next;
22
- i++;
23
- } else {
24
- flags[arg.slice(2)] = true;
25
- }
26
- }
27
- } else if (arg.startsWith('-') && arg.length === 2) {
28
- const shortMap = { f: 'force', y: 'yes', v: 'verbose', j: 'json' };
29
- const long = shortMap[arg[1]] || arg[1];
30
- flags[long] = true;
31
- } else {
32
- positional.push(arg);
33
- }
34
- }
35
-
36
- return { positional, flags };
37
- }
38
-
39
- // ── Command Registry ──
40
-
41
- const COMMANDS = {
2
+ 'use strict';
3
+
4
+ const ui = require('./cli/ui');
5
+
6
+ // ── Argument Parser ──
7
+
8
+ function parseArgs(argv) {
9
+ const positional = [];
10
+ const flags = {};
11
+
12
+ for (let i = 0; i < argv.length; i++) {
13
+ const arg = argv[i];
14
+ if (arg.startsWith('--')) {
15
+ const eqIdx = arg.indexOf('=');
16
+ if (eqIdx !== -1) {
17
+ flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
18
+ } else {
19
+ const next = argv[i + 1];
20
+ if (next && !next.startsWith('-')) {
21
+ flags[arg.slice(2)] = next;
22
+ i++;
23
+ } else {
24
+ flags[arg.slice(2)] = true;
25
+ }
26
+ }
27
+ } else if (arg.startsWith('-') && arg.length === 2) {
28
+ const shortMap = { f: 'force', y: 'yes', v: 'verbose', j: 'json' };
29
+ const long = shortMap[arg[1]] || arg[1];
30
+ flags[long] = true;
31
+ } else {
32
+ positional.push(arg);
33
+ }
34
+ }
35
+
36
+ return { positional, flags };
37
+ }
38
+
39
+ // ── Command Registry ──
40
+
41
+ const COMMANDS = {
42
42
  init: {
43
43
  desc: 'Set up SecureNow in the current project (instrumentation + runtime credentials)',
44
44
  usage: 'securenow init [--env local] [--key <API_KEY>] [--ts|--js] [--src|--root] [--force]',
@@ -46,21 +46,21 @@ const COMMANDS = {
46
46
  env: 'Deployment environment to write into .securenow/runtime.json (default: local)',
47
47
  environment: 'Alias for --env',
48
48
  key: 'Firewall API key to write to .securenow/runtime.json',
49
- 'api-key': 'Alias for --key',
50
- typescript: 'Force TypeScript',
51
- javascript: 'Force JavaScript',
52
- src: 'Create in src/',
53
- root: 'Create in root',
54
- force: 'Overwrite existing',
55
- },
56
- run: (a, f) => require('./cli/init').init(a, f),
57
- },
49
+ 'api-key': 'Alias for --key',
50
+ typescript: 'Force TypeScript',
51
+ javascript: 'Force JavaScript',
52
+ src: 'Create in src/',
53
+ root: 'Create in root',
54
+ force: 'Overwrite existing',
55
+ },
56
+ run: (a, f) => require('./cli/init').init(a, f),
57
+ },
58
58
  login: {
59
59
  desc: 'Onboard SecureNow admin auth and app runtime config',
60
60
  usage: 'securenow login [--token <TOKEN>] [--global]',
61
- flags: {
62
- token: 'Authenticate with a token directly',
63
- global: 'Save credentials to ~/.securenow/ (shared across all projects)',
61
+ flags: {
62
+ token: 'Authenticate with a token directly',
63
+ global: 'Save credentials to ~/.securenow/ (shared across all projects)',
64
64
  },
65
65
  run: (a, f) => require('./cli/auth').login(a, f),
66
66
  },
@@ -107,8 +107,8 @@ const COMMANDS = {
107
107
  },
108
108
  whoami: {
109
109
  desc: 'Show admin auth and SDK runtime status',
110
- usage: 'securenow whoami',
111
- run: () => require('./cli/auth').whoami(),
110
+ usage: 'securenow whoami',
111
+ run: () => require('./cli/auth').whoami(),
112
112
  },
113
113
  'api-key': {
114
114
  desc: 'Manage the runtime API key stored in runtime credentials',
@@ -128,21 +128,21 @@ const COMMANDS = {
128
128
  set: {
129
129
  desc: 'Save an API key (snk_live_...) to runtime credentials',
130
130
  usage: 'securenow api-key set <snk_live_...> [--global]',
131
- flags: { global: 'Save to ~/.securenow/ instead of project-local' },
132
- run: (a, f) => require('./cli/apiKey').set(a, f),
133
- },
134
- clear: {
135
- desc: 'Remove the stored API key',
136
- usage: 'securenow api-key clear [--global]',
137
- flags: { global: 'Clear from ~/.securenow/ instead of project-local' },
138
- run: (a, f) => require('./cli/apiKey').clear(a, f),
139
- },
140
- show: {
141
- desc: 'Print the masked API key currently in use',
142
- usage: 'securenow api-key show',
143
- run: () => require('./cli/apiKey').show(),
144
- },
145
- },
131
+ flags: { global: 'Save to ~/.securenow/ instead of project-local' },
132
+ run: (a, f) => require('./cli/apiKey').set(a, f),
133
+ },
134
+ clear: {
135
+ desc: 'Remove the stored API key',
136
+ usage: 'securenow api-key clear [--global]',
137
+ flags: { global: 'Clear from ~/.securenow/ instead of project-local' },
138
+ run: (a, f) => require('./cli/apiKey').clear(a, f),
139
+ },
140
+ show: {
141
+ desc: 'Print the masked API key currently in use',
142
+ usage: 'securenow api-key show',
143
+ run: () => require('./cli/apiKey').show(),
144
+ },
145
+ },
146
146
  defaultSub: 'show',
147
147
  },
148
148
  credentials: {
@@ -164,47 +164,47 @@ const COMMANDS = {
164
164
  },
165
165
  defaultSub: 'runtime',
166
166
  },
167
- apps: {
168
- desc: 'Manage applications',
169
- usage: 'securenow apps <subcommand> [options]',
170
- sub: {
171
- list: { desc: 'List all applications', run: (a, f) => require('./cli/apps').list(a, f) },
172
- create: { desc: 'Create a new application (interactive instance picker)', usage: 'securenow apps create <name> [--hosts host1,host2] [--instance <id>]', run: (a, f) => require('./cli/apps').create(a, f) },
173
- info: { desc: 'Show application details', usage: 'securenow apps info <id>', run: (a, f) => require('./cli/apps').info(a, f) },
174
- delete: { desc: 'Delete an application', usage: 'securenow apps delete <id> [--force]', run: (a, f) => require('./cli/apps').remove(a, f) },
175
- default: { desc: 'Set default application', usage: 'securenow apps default <key>', run: (a, f) => require('./cli/apps').setDefault(a, f) },
176
- discover: { desc: 'Discover subdomains and add as apps', usage: 'securenow apps discover [appId] [--domain example.com]', run: (a, f) => require('./cli/apps').discover(a, f) },
177
- scan: { desc: 'Scan all app domains for new subdomains', usage: 'securenow apps scan [--yes]', run: (a, f) => require('./cli/apps').scan(a, f) },
178
- },
179
- defaultSub: 'list',
180
- },
181
- traces: {
182
- desc: 'View and analyze traces',
183
- usage: 'securenow traces <subcommand> [options]',
184
- sub: {
167
+ apps: {
168
+ desc: 'Manage applications',
169
+ usage: 'securenow apps <subcommand> [options]',
170
+ sub: {
171
+ list: { desc: 'List all applications', run: (a, f) => require('./cli/apps').list(a, f) },
172
+ create: { desc: 'Create a new application (interactive instance picker)', usage: 'securenow apps create <name> [--hosts host1,host2] [--instance <id>]', run: (a, f) => require('./cli/apps').create(a, f) },
173
+ info: { desc: 'Show application details', usage: 'securenow apps info <id>', run: (a, f) => require('./cli/apps').info(a, f) },
174
+ delete: { desc: 'Delete an application', usage: 'securenow apps delete <id> [--force]', run: (a, f) => require('./cli/apps').remove(a, f) },
175
+ default: { desc: 'Set default application', usage: 'securenow apps default <key>', run: (a, f) => require('./cli/apps').setDefault(a, f) },
176
+ discover: { desc: 'Discover subdomains and add as apps', usage: 'securenow apps discover [appId] [--domain example.com]', run: (a, f) => require('./cli/apps').discover(a, f) },
177
+ scan: { desc: 'Scan all app domains for new subdomains', usage: 'securenow apps scan [--yes]', run: (a, f) => require('./cli/apps').scan(a, f) },
178
+ },
179
+ defaultSub: 'list',
180
+ },
181
+ traces: {
182
+ desc: 'View and analyze traces',
183
+ usage: 'securenow traces <subcommand> [options]',
184
+ sub: {
185
185
  list: { desc: 'List recent traces', flags: { app: 'App key', env: 'Environment (production, staging, preview, local, or all)', environment: 'Alias for --env', limit: 'Max results', start: 'Start time', end: 'End time' }, run: (a, f) => require('./cli/monitor').tracesList(a, f) },
186
186
  show: { desc: 'Show trace details', usage: 'securenow traces show <traceId>', flags: { app: 'App key', env: 'Environment', environment: 'Alias for --env' }, run: (a, f) => require('./cli/monitor').tracesShow(a, f) },
187
187
  analyze: { desc: 'AI-analyze a trace', usage: 'securenow traces analyze <traceId>', flags: { app: 'App key', env: 'Environment', environment: 'Alias for --env' }, run: (a, f) => require('./cli/monitor').tracesAnalyze(a, f) },
188
- },
189
- defaultSub: 'list',
190
- },
191
- logs: {
192
- desc: 'View application logs',
193
- usage: 'securenow logs [options]',
194
- sub: {
188
+ },
189
+ defaultSub: 'list',
190
+ },
191
+ logs: {
192
+ desc: 'View application logs',
193
+ usage: 'securenow logs [options]',
194
+ sub: {
195
195
  list: { desc: 'List recent logs', flags: { app: 'App key', env: 'Environment (production, staging, preview, local, or all)', environment: 'Alias for --env', limit: 'Max results (default 200)', since: 'Time window (e.g. 30m, 6h, 2d)', minutes: 'Time window in minutes (alias for --since Nm)', level: 'Filter by level (error, warn, info, debug)', start: 'Start time (ISO 8601)', end: 'End time (ISO 8601)' }, run: (a, f) => require('./cli/monitor').logsList(a, f) },
196
196
  trace: { desc: 'Show logs for a trace', usage: 'securenow logs trace <traceId>', flags: { app: 'App key', env: 'Environment', environment: 'Alias for --env' }, run: (a, f) => require('./cli/monitor').logsTrace(a, f) },
197
- },
198
- defaultSub: 'list',
199
- },
197
+ },
198
+ defaultSub: 'list',
199
+ },
200
200
  notifications: {
201
201
  desc: 'Manage notifications',
202
202
  usage: 'securenow notifications <subcommand> [options]',
203
- sub: {
204
- list: { desc: 'List notifications', flags: { limit: 'Max results', page: 'Page number' }, run: (a, f) => require('./cli/monitor').notificationsList(a, f) },
205
- read: { desc: 'Mark notification as read', usage: 'securenow notifications read <id>', run: (a, f) => require('./cli/monitor').notificationsRead(a, f) },
206
- 'read-all': { desc: 'Mark all as read', run: () => require('./cli/monitor').notificationsReadAll() },
207
- unread: { desc: 'Show unread count', run: () => require('./cli/monitor').notificationsUnread() },
203
+ sub: {
204
+ list: { desc: 'List notifications', flags: { limit: 'Max results', page: 'Page number' }, run: (a, f) => require('./cli/monitor').notificationsList(a, f) },
205
+ read: { desc: 'Mark notification as read', usage: 'securenow notifications read <id>', run: (a, f) => require('./cli/monitor').notificationsRead(a, f) },
206
+ 'read-all': { desc: 'Mark all as read', run: () => require('./cli/monitor').notificationsReadAll() },
207
+ unread: { desc: 'Show unread count', run: () => require('./cli/monitor').notificationsUnread() },
208
208
  },
209
209
  defaultSub: 'list',
210
210
  },
@@ -253,20 +253,32 @@ const COMMANDS = {
253
253
  alerts: {
254
254
  desc: 'Manage alerting',
255
255
  usage: 'securenow alerts <subcommand> [options]',
256
- sub: {
256
+ sub: {
257
257
  rules: {
258
- desc: 'List, show, update, test, or tune alert rules',
258
+ desc: 'Create, list, show, update, test, or tune alert rules',
259
259
  flags: {
260
260
  json: 'Output as JSON',
261
- 'applications-all': 'With update: scope rule to all apps',
261
+ name: 'With create: rule name',
262
+ description: 'With create: rule description',
263
+ nlp: 'With create: plain-English intent stored on the query',
264
+ text: 'Alias for --nlp',
265
+ category: 'With create: query category (default: custom)',
266
+ severity: 'With create: critical | high | medium | low',
267
+ schedule: 'With create: cron expression (default */15 * * * *)',
268
+ 'throttle-minutes': 'With create: notification throttle window in minutes',
269
+ 'no-throttle': 'With create: disable throttling',
270
+ 'execution-mode': 'With create: scheduled | instant | hybrid',
271
+ channel: 'With create: comma-separated alert channel ids (default: in-app)',
272
+ 'query-mapping-id': 'With create: reuse an existing saved query instead of --sql',
273
+ 'applications-all': 'With create/update: scope rule to all apps',
262
274
  'no-applications-all': 'With update: scope to explicit --apps list',
263
- apps: 'Comma-separated app keys (with update)',
275
+ apps: 'Comma-separated app keys (with create/update)',
264
276
  app: 'Application key for rule tests',
265
277
  mode: 'Rule test mode: dry_run or live',
266
278
  wait: 'Wait for rule test completion',
267
- sql: 'Candidate/replacement SQL, @file, or - for stdin',
279
+ sql: 'Detection/candidate/replacement SQL, @file, or - for stdin',
268
280
  query: 'Alias for --sql',
269
- file: 'Read candidate/replacement SQL from a file',
281
+ file: 'Read SQL from a file',
270
282
  reason: 'Audit reason for a write',
271
283
  'apply-globally': 'Required for system query tuning',
272
284
  'reactivate-paused': 'Reactivate paused system copies after tuning',
@@ -278,35 +290,35 @@ const COMMANDS = {
278
290
  },
279
291
  run: (a, f) => require('./cli/security').alertRulesRoute(a, f),
280
292
  },
281
- channels: { desc: 'List alert channels', run: (a, f) => require('./cli/security').alertChannelsList(a, f) },
282
- history: { desc: 'View alert history', flags: { limit: 'Max results' }, run: (a, f) => require('./cli/security').alertHistoryList(a, f) },
283
- },
284
- defaultSub: 'rules',
285
- },
286
- fp: {
287
- desc: 'Manage false-positive exclusion rules',
288
- usage: 'securenow fp <subcommand> [options]',
289
- flags: { json: 'Output as JSON', conditions: 'JSON array of conditions', 'match-mode': 'Match mode: all (AND) or any (OR)', 'rule-scope': 'Scope: this_rule | specific_rules | all_existing | any_rule', 'target-rules': 'Comma-separated rule IDs (when --rule-scope specific_rules)', 'path-safe': 'Add path_safe_values condition (standard|strict)', 'query-safe': 'Add query_safe_values condition (standard|strict)', 'query-keys': 'Allowed query param names (comma-separated)', 'ua-safe': 'Add ua_safe_values condition (standard|strict)', 'headers-safe': 'Add headers_safe_values condition (standard|strict)', 'headers-keys': 'Allowed header names (comma-separated)' },
290
- sub: {
291
- list: { desc: 'List all exclusion rules', run: (a, f) => require('./cli/fp').list(a, f) },
292
- show: { desc: 'Show exclusion rule details', usage: 'securenow fp show <id>', run: (a, f) => require('./cli/fp').show(a, f) },
293
- create: { desc: 'Create an exclusion rule', usage: 'securenow fp create [--conditions \'[...]\'] [--path /api/event] [--method POST] [--path-safe standard] [--ua-safe standard] [--headers-safe standard] [--query-keys page,limit] [--headers-keys host,content-type] [--reason "..."] [--rule-scope any_rule|specific_rules|all_existing] [--target-rules id1,id2]', run: (a, f) => require('./cli/fp').create(a, f) },
294
- edit: { desc: 'Edit an exclusion rule', usage: 'securenow fp edit <id> [--active true/false] [--conditions \'[...]\']', run: (a, f) => require('./cli/fp').edit(a, f) },
295
- delete: { desc: 'Delete an exclusion rule', usage: 'securenow fp delete <id> [--yes]', run: (a, f) => require('./cli/fp').remove(a, f) },
296
- 'test-body': { desc: 'Test a request body against conditions', usage: 'securenow fp test-body <body|@file> --conditions \'[...]\'', run: (a, f) => require('./cli/fp').testBody(a, f) },
297
- 'dry-run': { desc: 'Dry-run conditions against live traces (last 3 days)', usage: 'securenow fp dry-run --conditions \'[...]\'', run: (a, f) => require('./cli/fp').dryRun(a, f) },
298
- 'ai-fill': { desc: 'AI-generate exclusion conditions', usage: 'securenow fp ai-fill [--description "..."] [--context \'{"method":"POST",...}\']', run: (a, f) => require('./cli/fp').aiFill(a, f) },
299
- mark: { desc: 'Mark an IP as false positive on a notification', usage: 'securenow fp mark <notification-id> <ip> [--conditions \'[...]\'] [--reason "..."] [--rule-scope this_rule|specific_rules|all_existing|any_rule] [--target-rules id1,id2]', run: (a, f) => require('./cli/fp').mark(a, f) },
300
- },
301
- defaultSub: 'list',
302
- },
293
+ channels: { desc: 'List alert channels', run: (a, f) => require('./cli/security').alertChannelsList(a, f) },
294
+ history: { desc: 'View alert history', flags: { limit: 'Max results' }, run: (a, f) => require('./cli/security').alertHistoryList(a, f) },
295
+ },
296
+ defaultSub: 'rules',
297
+ },
298
+ fp: {
299
+ desc: 'Manage false-positive exclusion rules',
300
+ usage: 'securenow fp <subcommand> [options]',
301
+ flags: { json: 'Output as JSON', conditions: 'JSON array of conditions', 'match-mode': 'Match mode: all (AND) or any (OR)', 'rule-scope': 'Scope: this_rule | specific_rules | all_existing | any_rule', 'target-rules': 'Comma-separated rule IDs (when --rule-scope specific_rules)', 'path-safe': 'Add path_safe_values condition (standard|strict)', 'query-safe': 'Add query_safe_values condition (standard|strict)', 'query-keys': 'Allowed query param names (comma-separated)', 'ua-safe': 'Add ua_safe_values condition (standard|strict)', 'headers-safe': 'Add headers_safe_values condition (standard|strict)', 'headers-keys': 'Allowed header names (comma-separated)' },
302
+ sub: {
303
+ list: { desc: 'List all exclusion rules', run: (a, f) => require('./cli/fp').list(a, f) },
304
+ show: { desc: 'Show exclusion rule details', usage: 'securenow fp show <id>', run: (a, f) => require('./cli/fp').show(a, f) },
305
+ create: { desc: 'Create an exclusion rule', usage: 'securenow fp create [--conditions \'[...]\'] [--path /api/event] [--method POST] [--path-safe standard] [--ua-safe standard] [--headers-safe standard] [--query-keys page,limit] [--headers-keys host,content-type] [--reason "..."] [--rule-scope any_rule|specific_rules|all_existing] [--target-rules id1,id2]', run: (a, f) => require('./cli/fp').create(a, f) },
306
+ edit: { desc: 'Edit an exclusion rule', usage: 'securenow fp edit <id> [--active true/false] [--conditions \'[...]\']', run: (a, f) => require('./cli/fp').edit(a, f) },
307
+ delete: { desc: 'Delete an exclusion rule', usage: 'securenow fp delete <id> [--yes]', run: (a, f) => require('./cli/fp').remove(a, f) },
308
+ 'test-body': { desc: 'Test a request body against conditions', usage: 'securenow fp test-body <body|@file> --conditions \'[...]\'', run: (a, f) => require('./cli/fp').testBody(a, f) },
309
+ 'dry-run': { desc: 'Dry-run conditions against live traces (last 3 days)', usage: 'securenow fp dry-run --conditions \'[...]\'', run: (a, f) => require('./cli/fp').dryRun(a, f) },
310
+ 'ai-fill': { desc: 'AI-generate exclusion conditions', usage: 'securenow fp ai-fill [--description "..."] [--context \'{"method":"POST",...}\']', run: (a, f) => require('./cli/fp').aiFill(a, f) },
311
+ mark: { desc: 'Mark an IP as false positive on a notification', usage: 'securenow fp mark <notification-id> <ip> [--conditions \'[...]\'] [--reason "..."] [--rule-scope this_rule|specific_rules|all_existing|any_rule] [--target-rules id1,id2]', run: (a, f) => require('./cli/fp').mark(a, f) },
312
+ },
313
+ defaultSub: 'list',
314
+ },
303
315
  firewall: {
304
316
  desc: 'Firewall status, per-app toggle, and IP testing',
305
317
  usage: 'securenow firewall <subcommand> [options]',
306
318
  flags: { app: 'App key (defaults to logged-in app)', json: 'Output as JSON' },
307
- sub: {
319
+ sub: {
308
320
  status: { desc: 'Show firewall status, layers, and blocklist info', flags: { env: 'Environment (default: production)', environment: 'Alias for --env' }, run: (a, f) => require('./cli/firewall').status(a, f) },
309
- apps: { desc: 'List apps with their firewall on/off state', run: (a, f) => require('./cli/firewall').appsList(a, f) },
321
+ apps: { desc: 'List apps with their firewall on/off state', run: (a, f) => require('./cli/firewall').appsList(a, f) },
310
322
  enable: { desc: 'Turn the firewall ON for an app environment', usage: 'securenow firewall enable [--app <key>] [--env production]', flags: { app: 'App key (defaults to logged-in app)', env: 'Environment (default: production)', environment: 'Alias for --env' }, run: (a, f) => require('./cli/firewall').enable(a, f) },
311
323
  disable: { desc: 'Turn the firewall OFF for an app environment', usage: 'securenow firewall disable [--app <key>] [--env local]', flags: { app: 'App key (defaults to logged-in app)', env: 'Environment (default: production)', environment: 'Alias for --env' }, run: (a, f) => require('./cli/firewall').disable(a, f) },
312
324
  'test-ip': { desc: 'Check if an IP would be blocked', usage: 'securenow firewall test-ip <ip> [--path /admin/users] [--method GET] [--env production]', flags: { env: 'Environment (default: production)', environment: 'Alias for --env' }, run: (a, f) => require('./cli/firewall').testIp(a, f) },
@@ -423,8 +435,8 @@ const COMMANDS = {
423
435
  remove: { desc: 'Unblock an IP (compatibility alias)', usage: 'securenow blocklist remove <id> [--reason <reason>]', run: (a, f) => require('./cli/security').blocklistRemove(a, f) },
424
436
  stats: { desc: 'Blocklist statistics', run: (a, f) => require('./cli/security').blocklistStats(a, f) },
425
437
  },
426
- defaultSub: 'list',
427
- },
438
+ defaultSub: 'list',
439
+ },
428
440
  allowlist: {
429
441
  desc: 'Manage IP allowlist (only allow listed IPs)',
430
442
  usage: 'securenow allowlist <subcommand> [options]',
@@ -432,11 +444,11 @@ const COMMANDS = {
432
444
  sub: {
433
445
  list: { desc: 'List allowed IPs', run: (a, f) => require('./cli/security').allowlistList(a, f) },
434
446
  add: { desc: 'Allow an IP', usage: 'securenow allowlist add <ip> [--app <key>] [--env local] [--label <label>] [--reason <reason>]', run: (a, f) => require('./cli/security').allowlistAdd(a, f) },
435
- remove: { desc: 'Remove an allowed IP', usage: 'securenow allowlist remove <id>', run: (a, f) => require('./cli/security').allowlistRemove(a, f) },
436
- stats: { desc: 'Allowlist statistics', run: (a, f) => require('./cli/security').allowlistStats(a, f) },
437
- },
438
- defaultSub: 'list',
439
- },
447
+ remove: { desc: 'Remove an allowed IP', usage: 'securenow allowlist remove <id>', run: (a, f) => require('./cli/security').allowlistRemove(a, f) },
448
+ stats: { desc: 'Allowlist statistics', run: (a, f) => require('./cli/security').allowlistStats(a, f) },
449
+ },
450
+ defaultSub: 'list',
451
+ },
440
452
  trusted: {
441
453
  desc: 'Manage trusted IPs',
442
454
  usage: 'securenow trusted <subcommand> [options]',
@@ -444,43 +456,43 @@ const COMMANDS = {
444
456
  sub: {
445
457
  list: { desc: 'List trusted IPs', run: (a, f) => require('./cli/security').trustedList(a, f) },
446
458
  add: { desc: 'Add trusted IP', usage: 'securenow trusted add <ip> [--app <key>] [--env local] [--label <label>]', run: (a, f) => require('./cli/security').trustedAdd(a, f) },
447
- remove: { desc: 'Remove trusted IP', usage: 'securenow trusted remove <id>', run: (a, f) => require('./cli/security').trustedRemove(a, f) },
448
- },
449
- defaultSub: 'list',
450
- },
451
- ip: {
452
- desc: 'IP intelligence lookup',
453
- usage: 'securenow ip <ip-address>',
454
- sub: {
455
- lookup: { desc: 'Look up IP intelligence', run: (a, f) => require('./cli/security').ipLookup(a, f) },
459
+ remove: { desc: 'Remove trusted IP', usage: 'securenow trusted remove <id>', run: (a, f) => require('./cli/security').trustedRemove(a, f) },
460
+ },
461
+ defaultSub: 'list',
462
+ },
463
+ ip: {
464
+ desc: 'IP intelligence lookup',
465
+ usage: 'securenow ip <ip-address>',
466
+ sub: {
467
+ lookup: { desc: 'Look up IP intelligence', run: (a, f) => require('./cli/security').ipLookup(a, f) },
456
468
  traces: { desc: 'Show traces for an IP', usage: 'securenow ip traces <ip> [--env production]', flags: { app: 'App key', env: 'Environment', environment: 'Alias for --env' }, run: (a, f) => require('./cli/security').ipTraces(a, f) },
457
- },
458
- defaultAction: (a, f) => require('./cli/security').ipLookup(a, f),
459
- },
460
- forensics: {
461
- desc: 'Run forensic queries (natural language → SQL)',
462
- usage: 'securenow forensics <query> [--app <key>]',
463
- sub: {
469
+ },
470
+ defaultAction: (a, f) => require('./cli/security').ipLookup(a, f),
471
+ },
472
+ forensics: {
473
+ desc: 'Run forensic queries (natural language → SQL)',
474
+ usage: 'securenow forensics <query> [--app <key>]',
475
+ sub: {
464
476
  query: { desc: 'Run a forensic query', flags: { app: 'App key to scope query', env: 'Environment (default: production)', environment: 'Alias for --env' }, run: (a, f) => require('./cli/security').forensicsQuery(a, f) },
465
477
  chat: { desc: 'Interactive forensics chat (scoped to an app)', usage: 'securenow forensics chat --app <key> [--env production]', flags: { app: 'App key to chat with', env: 'Environment (default: production)', environment: 'Alias for --env' }, run: (a, f) => require('./cli/security').forensicsChat(a, f) },
466
- library: { desc: 'View saved queries', run: (a, f) => require('./cli/security').forensicsLibrary(a, f) },
467
- },
468
- defaultAction: (a, f) => require('./cli/security').forensicsQuery(a, f),
469
- },
470
- instances: {
471
- desc: 'Manage ClickHouse instances',
472
- usage: 'securenow instances <subcommand> [options]',
473
- sub: {
474
- list: { desc: 'List instances', run: (a, f) => require('./cli/security').instancesList(a, f) },
475
- test: { desc: 'Test instance connection', usage: 'securenow instances test <id>', run: (a, f) => require('./cli/security').instancesTest(a, f) },
476
- },
477
- defaultSub: 'list',
478
- },
479
- analytics: {
480
- desc: 'View response analytics',
481
- usage: 'securenow analytics [--app <key>]',
482
- run: (a, f) => require('./cli/security').analytics(a, f),
483
- },
478
+ library: { desc: 'View saved queries', run: (a, f) => require('./cli/security').forensicsLibrary(a, f) },
479
+ },
480
+ defaultAction: (a, f) => require('./cli/security').forensicsQuery(a, f),
481
+ },
482
+ instances: {
483
+ desc: 'Manage ClickHouse instances',
484
+ usage: 'securenow instances <subcommand> [options]',
485
+ sub: {
486
+ list: { desc: 'List instances', run: (a, f) => require('./cli/security').instancesList(a, f) },
487
+ test: { desc: 'Test instance connection', usage: 'securenow instances test <id>', run: (a, f) => require('./cli/security').instancesTest(a, f) },
488
+ },
489
+ defaultSub: 'list',
490
+ },
491
+ analytics: {
492
+ desc: 'View response analytics',
493
+ usage: 'securenow analytics [--app <key>]',
494
+ run: (a, f) => require('./cli/security').analytics(a, f),
495
+ },
484
496
  status: {
485
497
  desc: 'Dashboard overview',
486
498
  usage: 'securenow status [--app <key>] [--env local|production|all]',
@@ -491,89 +503,89 @@ const COMMANDS = {
491
503
  },
492
504
  run: (a, f) => require('./cli/monitor').status(a, f),
493
505
  },
494
- config: {
495
- desc: 'Manage CLI configuration',
496
- usage: 'securenow config <set|get> [key] [value]',
497
- sub: {
498
- set: {
499
- desc: 'Set a config value',
500
- usage: 'securenow config set <key> <value>',
501
- run: (a) => {
502
- const conf = require('./cli/config');
503
- const [key, ...rest] = a;
504
- const value = rest.join(' ');
505
- if (!key || !value) { ui.error('Usage: securenow config set <key> <value>'); process.exit(1); }
506
- conf.setConfigValue(key, value);
507
- ui.success(`${key} = ${value}`);
508
- },
509
- },
510
- get: {
511
- desc: 'Get a config value',
512
- usage: 'securenow config get [key]',
513
- run: (a) => {
514
- const conf = require('./cli/config');
515
- const key = a[0];
516
- if (key) {
517
- const val = conf.getConfigValue(key);
518
- console.log(val != null ? val : ui.c.dim('(not set)'));
519
- } else {
520
- const all = conf.loadConfig();
521
- console.log('');
522
- ui.keyValue(Object.entries(all).map(([k, v]) => [k, v != null ? String(v) : ui.c.dim('(not set)')]));
523
- console.log('');
524
- }
525
- },
526
- },
527
- path: {
528
- desc: 'Show config file path',
529
- run: () => {
530
- const conf = require('./cli/config');
531
- console.log(`Config: ${conf.CONFIG_FILE}`);
532
- console.log(`Credentials: ${conf.CREDENTIALS_FILE}`);
533
- if (conf.hasLocalCredentials()) {
534
- console.log(`Local creds: ${conf.LOCAL_CREDENTIALS_FILE} ${require('./cli/ui').c.green('(active)')}`);
535
- }
536
- console.log(`Auth source: ${conf.getAuthSource()}`);
537
- },
538
- },
539
- },
540
- },
541
- run: {
542
- desc: 'Run a Node.js app with automatic OTel instrumentation',
543
- usage: 'securenow run [node-flags] [--firewall-only] <script> [app-args]',
544
- flags: {
545
- watch: 'Enable Node.js watch mode',
546
- inspect: 'Enable Node.js inspector',
547
- 'firewall-only': 'Preload firewall without OTel tracing overhead',
548
- },
549
- rawArgv: true,
550
- run: (rawArgs) => require('./cli/run').run(rawArgs),
551
- },
552
- redact: {
553
- desc: 'Redact sensitive fields from a JSON payload',
554
- usage: "securenow redact '<json>' [--fields f1,f2] [--json]",
555
- flags: { fields: 'Comma-separated extra field names to redact (added to defaults)' },
556
- run: (a, f) => require('./cli/utils').redact(a, f),
557
- },
558
- cidr: {
559
- desc: 'CIDR utilities (match and parse)',
560
- usage: 'securenow cidr <match|parse> ...',
561
- sub: {
562
- match: {
563
- desc: 'Check if an IP matches a CIDR list',
564
- usage: 'securenow cidr match <ip> <cidr1,cidr2,...>',
565
- run: (a, f) => require('./cli/utils').cidrMatch(a, f),
566
- },
567
- parse: {
568
- desc: 'Parse a CIDR and show network/broadcast/size',
569
- usage: 'securenow cidr parse <cidr>',
570
- run: (a, f) => require('./cli/utils').cidrParse(a, f),
571
- },
572
- },
573
- },
574
- log: {
575
- desc: 'Emit logs via OTLP (for scripts, cron, debugging)',
576
- usage: 'securenow log send <message> [--level info|warn|error] [--attrs k=v,k=v]',
506
+ config: {
507
+ desc: 'Manage CLI configuration',
508
+ usage: 'securenow config <set|get> [key] [value]',
509
+ sub: {
510
+ set: {
511
+ desc: 'Set a config value',
512
+ usage: 'securenow config set <key> <value>',
513
+ run: (a) => {
514
+ const conf = require('./cli/config');
515
+ const [key, ...rest] = a;
516
+ const value = rest.join(' ');
517
+ if (!key || !value) { ui.error('Usage: securenow config set <key> <value>'); process.exit(1); }
518
+ conf.setConfigValue(key, value);
519
+ ui.success(`${key} = ${value}`);
520
+ },
521
+ },
522
+ get: {
523
+ desc: 'Get a config value',
524
+ usage: 'securenow config get [key]',
525
+ run: (a) => {
526
+ const conf = require('./cli/config');
527
+ const key = a[0];
528
+ if (key) {
529
+ const val = conf.getConfigValue(key);
530
+ console.log(val != null ? val : ui.c.dim('(not set)'));
531
+ } else {
532
+ const all = conf.loadConfig();
533
+ console.log('');
534
+ ui.keyValue(Object.entries(all).map(([k, v]) => [k, v != null ? String(v) : ui.c.dim('(not set)')]));
535
+ console.log('');
536
+ }
537
+ },
538
+ },
539
+ path: {
540
+ desc: 'Show config file path',
541
+ run: () => {
542
+ const conf = require('./cli/config');
543
+ console.log(`Config: ${conf.CONFIG_FILE}`);
544
+ console.log(`Credentials: ${conf.CREDENTIALS_FILE}`);
545
+ if (conf.hasLocalCredentials()) {
546
+ console.log(`Local creds: ${conf.LOCAL_CREDENTIALS_FILE} ${require('./cli/ui').c.green('(active)')}`);
547
+ }
548
+ console.log(`Auth source: ${conf.getAuthSource()}`);
549
+ },
550
+ },
551
+ },
552
+ },
553
+ run: {
554
+ desc: 'Run a Node.js app with automatic OTel instrumentation',
555
+ usage: 'securenow run [node-flags] [--firewall-only] <script> [app-args]',
556
+ flags: {
557
+ watch: 'Enable Node.js watch mode',
558
+ inspect: 'Enable Node.js inspector',
559
+ 'firewall-only': 'Preload firewall without OTel tracing overhead',
560
+ },
561
+ rawArgv: true,
562
+ run: (rawArgs) => require('./cli/run').run(rawArgs),
563
+ },
564
+ redact: {
565
+ desc: 'Redact sensitive fields from a JSON payload',
566
+ usage: "securenow redact '<json>' [--fields f1,f2] [--json]",
567
+ flags: { fields: 'Comma-separated extra field names to redact (added to defaults)' },
568
+ run: (a, f) => require('./cli/utils').redact(a, f),
569
+ },
570
+ cidr: {
571
+ desc: 'CIDR utilities (match and parse)',
572
+ usage: 'securenow cidr <match|parse> ...',
573
+ sub: {
574
+ match: {
575
+ desc: 'Check if an IP matches a CIDR list',
576
+ usage: 'securenow cidr match <ip> <cidr1,cidr2,...>',
577
+ run: (a, f) => require('./cli/utils').cidrMatch(a, f),
578
+ },
579
+ parse: {
580
+ desc: 'Parse a CIDR and show network/broadcast/size',
581
+ usage: 'securenow cidr parse <cidr>',
582
+ run: (a, f) => require('./cli/utils').cidrParse(a, f),
583
+ },
584
+ },
585
+ },
586
+ log: {
587
+ desc: 'Emit logs via OTLP (for scripts, cron, debugging)',
588
+ usage: 'securenow log send <message> [--level info|warn|error] [--attrs k=v,k=v]',
577
589
  sub: {
578
590
  send: {
579
591
  desc: 'Send a single log record to the OTLP collector',
@@ -583,11 +595,32 @@ const COMMANDS = {
583
595
  level: 'Severity (trace|debug|info|warn|error|fatal)',
584
596
  attrs: 'Comma-separated key=value attributes',
585
597
  },
586
- run: (a, f) => require('./cli/diagnostics').logSend(a, f),
587
- },
588
- },
589
- defaultSub: 'send',
590
- },
598
+ run: (a, f) => require('./cli/diagnostics').logSend(a, f),
599
+ },
600
+ },
601
+ defaultSub: 'send',
602
+ },
603
+ event: {
604
+ desc: 'Emit a custom security event to SecureNow (for scripts, testing, non-JS apps)',
605
+ usage: 'securenow event send <type> [--user <id>] [--session <id>] [--ip <ip>] [--attrs k=v,k=v]',
606
+ sub: {
607
+ send: {
608
+ desc: 'Send a single custom event to the /v1/events ingest',
609
+ flags: {
610
+ env: 'Deployment environment for this event (defaults to credentials file)',
611
+ environment: 'Alias for --env',
612
+ user: 'End-user / account id (enduser.id)',
613
+ session: 'Session id (session.id)',
614
+ ip: 'End-user client IP',
615
+ 'user-agent': 'End-user agent string',
616
+ level: 'Severity (trace|debug|info|warn|error|fatal)',
617
+ attrs: 'Comma-separated key=value attributes',
618
+ },
619
+ run: (a, f) => require('./cli/diagnostics').eventSend(a, f),
620
+ },
621
+ },
622
+ defaultSub: 'send',
623
+ },
591
624
  'test-span': {
592
625
  desc: 'Emit a test span to verify collector connectivity',
593
626
  usage: 'securenow test-span [<span-name>] [--env local|production]',
@@ -597,11 +630,11 @@ const COMMANDS = {
597
630
  },
598
631
  run: (a, f) => require('./cli/diagnostics').testSpan(a, f),
599
632
  },
600
- doctor: {
601
- desc: 'Diagnose SecureNow configuration and collector connectivity',
602
- usage: 'securenow doctor [--json]',
603
- run: (a, f) => require('./cli/diagnostics').doctor(a, f),
604
- },
633
+ doctor: {
634
+ desc: 'Diagnose SecureNow configuration and collector connectivity',
635
+ usage: 'securenow doctor [--json]',
636
+ run: (a, f) => require('./cli/diagnostics').doctor(a, f),
637
+ },
605
638
  env: {
606
639
  desc: 'Show resolved SecureNow configuration (service name, endpoints, credentials)',
607
640
  usage: 'securenow env [--json]',
@@ -615,163 +648,163 @@ const COMMANDS = {
615
648
  version: {
616
649
  desc: 'Show CLI version',
617
650
  run: () => {
618
- try {
619
- const pkg = require('./package.json');
620
- console.log(`securenow v${pkg.version}`);
621
- } catch {
622
- console.log('securenow (version unknown)');
623
- }
624
- },
625
- },
626
- };
627
-
628
- // ── Help System ──
629
-
630
- function showBanner() {
631
- console.log('');
632
- console.log(` ${ui.c.bold(ui.c.cyan('SecureNow CLI'))} ${ui.c.dim('— Security observability from the terminal')}`);
633
- console.log('');
634
- }
635
-
636
- function showHelp(commandName) {
637
- if (commandName && COMMANDS[commandName]) {
638
- const cmd = COMMANDS[commandName];
639
- showBanner();
640
- console.log(` ${ui.c.bold(commandName)} — ${cmd.desc}`);
641
- console.log('');
642
- if (cmd.usage) {
643
- console.log(` ${ui.c.bold('USAGE')}`);
644
- console.log(` ${cmd.usage}`);
645
- console.log('');
646
- }
647
- if (cmd.sub) {
648
- console.log(` ${ui.c.bold('SUBCOMMANDS')}`);
649
- const entries = Object.entries(cmd.sub);
650
- const maxLen = entries.reduce((m, [k]) => Math.max(m, k.length), 0);
651
- for (const [name, sub] of entries) {
652
- console.log(` ${ui.c.cyan(name.padEnd(maxLen + 2))} ${sub.desc}`);
653
- }
654
- console.log('');
655
- }
656
- if (cmd.flags) {
657
- console.log(` ${ui.c.bold('FLAGS')}`);
658
- for (const [flag, desc] of Object.entries(cmd.flags)) {
659
- console.log(` --${ui.c.cyan(flag.padEnd(16))} ${desc}`);
660
- }
661
- console.log('');
662
- }
663
- console.log(` ${ui.c.bold('GLOBAL FLAGS')}`);
664
- console.log(` --${ui.c.cyan('json'.padEnd(16))} Output as JSON`);
665
- console.log(` --${ui.c.cyan('help'.padEnd(16))} Show help`);
666
- console.log('');
667
- return;
668
- }
669
-
670
- showBanner();
671
-
672
- const groups = {
673
- 'Run': ['run'],
651
+ try {
652
+ const pkg = require('./package.json');
653
+ console.log(`securenow v${pkg.version}`);
654
+ } catch {
655
+ console.log('securenow (version unknown)');
656
+ }
657
+ },
658
+ },
659
+ };
660
+
661
+ // ── Help System ──
662
+
663
+ function showBanner() {
664
+ console.log('');
665
+ console.log(` ${ui.c.bold(ui.c.cyan('SecureNow CLI'))} ${ui.c.dim('— Security observability from the terminal')}`);
666
+ console.log('');
667
+ }
668
+
669
+ function showHelp(commandName) {
670
+ if (commandName && COMMANDS[commandName]) {
671
+ const cmd = COMMANDS[commandName];
672
+ showBanner();
673
+ console.log(` ${ui.c.bold(commandName)} — ${cmd.desc}`);
674
+ console.log('');
675
+ if (cmd.usage) {
676
+ console.log(` ${ui.c.bold('USAGE')}`);
677
+ console.log(` ${cmd.usage}`);
678
+ console.log('');
679
+ }
680
+ if (cmd.sub) {
681
+ console.log(` ${ui.c.bold('SUBCOMMANDS')}`);
682
+ const entries = Object.entries(cmd.sub);
683
+ const maxLen = entries.reduce((m, [k]) => Math.max(m, k.length), 0);
684
+ for (const [name, sub] of entries) {
685
+ console.log(` ${ui.c.cyan(name.padEnd(maxLen + 2))} ${sub.desc}`);
686
+ }
687
+ console.log('');
688
+ }
689
+ if (cmd.flags) {
690
+ console.log(` ${ui.c.bold('FLAGS')}`);
691
+ for (const [flag, desc] of Object.entries(cmd.flags)) {
692
+ console.log(` --${ui.c.cyan(flag.padEnd(16))} ${desc}`);
693
+ }
694
+ console.log('');
695
+ }
696
+ console.log(` ${ui.c.bold('GLOBAL FLAGS')}`);
697
+ console.log(` --${ui.c.cyan('json'.padEnd(16))} Output as JSON`);
698
+ console.log(` --${ui.c.cyan('help'.padEnd(16))} Show help`);
699
+ console.log('');
700
+ return;
701
+ }
702
+
703
+ showBanner();
704
+
705
+ const groups = {
706
+ 'Run': ['run'],
674
707
  'Authentication': ['login', 'logout', 'whoami', 'credentials'],
675
- 'Applications': ['apps', 'init', 'status'],
676
- 'Observe': ['traces', 'logs', 'analytics'],
708
+ 'Applications': ['apps', 'init', 'status'],
709
+ 'Observe': ['traces', 'logs', 'analytics'],
677
710
  'Detect & Respond': ['human', 'notifications', 'alerts', 'fp'],
678
- 'Investigate': ['ip', 'forensics'],
711
+ 'Investigate': ['ip', 'forensics'],
679
712
  'Firewall': ['firewall'],
680
713
  'Remediation': ['automation', 'ratelimit', 'blocklist', 'allowlist', 'trusted'],
681
- 'Telemetry': ['log', 'test-span'],
714
+ 'Telemetry': ['log', 'event', 'test-span'],
682
715
  'Utilities': ['redact', 'cidr', 'doctor', 'env', 'mcp'],
683
- 'Settings': ['instances', 'config', 'version'],
684
- };
685
-
686
- for (const [group, cmds] of Object.entries(groups)) {
687
- console.log(` ${ui.c.bold(group)}`);
688
- for (const name of cmds) {
689
- const cmd = COMMANDS[name];
690
- if (cmd) {
691
- console.log(` ${ui.c.cyan(name.padEnd(18))} ${cmd.desc}`);
692
- }
693
- }
694
- console.log('');
695
- }
696
-
697
- console.log(` ${ui.c.bold('GLOBAL FLAGS')}`);
698
- console.log(` --${ui.c.cyan('json'.padEnd(16))} Output as JSON`);
699
- console.log(` --${ui.c.cyan('help'.padEnd(16))} Show help for a command`);
700
- console.log('');
701
- console.log(` ${ui.c.dim('Run')} securenow help <command> ${ui.c.dim('for detailed usage')}`);
702
- console.log('');
703
- }
704
-
705
- // ── Main Router ──
706
-
707
- async function main() {
708
- const { positional, flags } = parseArgs(process.argv.slice(2));
709
- const cmdName = positional[0] || 'help';
710
-
711
- if (cmdName === 'help' || flags.help) {
712
- showHelp(flags.help === true ? positional[0] : flags.help || positional[1] || (cmdName !== 'help' ? cmdName : null));
713
- return;
714
- }
715
-
716
- const cmd = COMMANDS[cmdName];
717
- if (!cmd) {
718
- // Auto-detect: if the first arg looks like a file path, treat it as `securenow run <file>`
719
- if (/\.(m?[jt]sx?|cjs)$/.test(cmdName) || cmdName.includes('/') || cmdName.includes('\\')) {
720
- return COMMANDS.run.run(process.argv.slice(2));
721
- }
722
- ui.error(`Unknown command: ${cmdName}`);
723
- ui.info('Run `securenow help` for a list of commands.');
724
- process.exit(1);
725
- }
726
-
727
- if (cmd.rawArgv) {
728
- // Pass raw argv (everything after the command name) so the command can
729
- // forward flags like --watch, --inspect verbatim to child processes.
730
- const cmdIdx = process.argv.indexOf(cmdName);
731
- const rawArgs = cmdIdx !== -1 ? process.argv.slice(cmdIdx + 1) : positional.slice(1);
732
- await cmd.run(rawArgs);
733
- return;
734
- }
735
-
736
- if (cmd.run && !cmd.sub) {
737
- await cmd.run(positional.slice(1), flags);
738
- return;
739
- }
740
-
741
- if (cmd.sub) {
742
- let subName = positional[1];
743
- let subArgs = positional.slice(2);
744
-
745
- if (subName && cmd.sub[subName]) {
746
- if (flags.help) {
747
- showHelp(cmdName);
748
- return;
749
- }
750
- await cmd.sub[subName].run(subArgs, flags);
751
- return;
752
- }
753
-
754
- if (cmd.defaultAction) {
755
- const allArgs = subName ? [subName, ...subArgs] : [];
756
- await cmd.defaultAction(allArgs, flags);
757
- return;
758
- }
759
-
760
- if (cmd.defaultSub) {
761
- const allArgs = subName ? [subName, ...subArgs] : [];
762
- await cmd.sub[cmd.defaultSub].run(allArgs, flags);
763
- return;
764
- }
765
-
766
- showHelp(cmdName);
767
- return;
768
- }
769
-
770
- if (cmd.run) {
771
- await cmd.run(positional.slice(1), flags);
772
- }
773
- }
774
-
716
+ 'Settings': ['instances', 'config', 'version'],
717
+ };
718
+
719
+ for (const [group, cmds] of Object.entries(groups)) {
720
+ console.log(` ${ui.c.bold(group)}`);
721
+ for (const name of cmds) {
722
+ const cmd = COMMANDS[name];
723
+ if (cmd) {
724
+ console.log(` ${ui.c.cyan(name.padEnd(18))} ${cmd.desc}`);
725
+ }
726
+ }
727
+ console.log('');
728
+ }
729
+
730
+ console.log(` ${ui.c.bold('GLOBAL FLAGS')}`);
731
+ console.log(` --${ui.c.cyan('json'.padEnd(16))} Output as JSON`);
732
+ console.log(` --${ui.c.cyan('help'.padEnd(16))} Show help for a command`);
733
+ console.log('');
734
+ console.log(` ${ui.c.dim('Run')} securenow help <command> ${ui.c.dim('for detailed usage')}`);
735
+ console.log('');
736
+ }
737
+
738
+ // ── Main Router ──
739
+
740
+ async function main() {
741
+ const { positional, flags } = parseArgs(process.argv.slice(2));
742
+ const cmdName = positional[0] || 'help';
743
+
744
+ if (cmdName === 'help' || flags.help) {
745
+ showHelp(flags.help === true ? positional[0] : flags.help || positional[1] || (cmdName !== 'help' ? cmdName : null));
746
+ return;
747
+ }
748
+
749
+ const cmd = COMMANDS[cmdName];
750
+ if (!cmd) {
751
+ // Auto-detect: if the first arg looks like a file path, treat it as `securenow run <file>`
752
+ if (/\.(m?[jt]sx?|cjs)$/.test(cmdName) || cmdName.includes('/') || cmdName.includes('\\')) {
753
+ return COMMANDS.run.run(process.argv.slice(2));
754
+ }
755
+ ui.error(`Unknown command: ${cmdName}`);
756
+ ui.info('Run `securenow help` for a list of commands.');
757
+ process.exit(1);
758
+ }
759
+
760
+ if (cmd.rawArgv) {
761
+ // Pass raw argv (everything after the command name) so the command can
762
+ // forward flags like --watch, --inspect verbatim to child processes.
763
+ const cmdIdx = process.argv.indexOf(cmdName);
764
+ const rawArgs = cmdIdx !== -1 ? process.argv.slice(cmdIdx + 1) : positional.slice(1);
765
+ await cmd.run(rawArgs);
766
+ return;
767
+ }
768
+
769
+ if (cmd.run && !cmd.sub) {
770
+ await cmd.run(positional.slice(1), flags);
771
+ return;
772
+ }
773
+
774
+ if (cmd.sub) {
775
+ let subName = positional[1];
776
+ let subArgs = positional.slice(2);
777
+
778
+ if (subName && cmd.sub[subName]) {
779
+ if (flags.help) {
780
+ showHelp(cmdName);
781
+ return;
782
+ }
783
+ await cmd.sub[subName].run(subArgs, flags);
784
+ return;
785
+ }
786
+
787
+ if (cmd.defaultAction) {
788
+ const allArgs = subName ? [subName, ...subArgs] : [];
789
+ await cmd.defaultAction(allArgs, flags);
790
+ return;
791
+ }
792
+
793
+ if (cmd.defaultSub) {
794
+ const allArgs = subName ? [subName, ...subArgs] : [];
795
+ await cmd.sub[cmd.defaultSub].run(allArgs, flags);
796
+ return;
797
+ }
798
+
799
+ showHelp(cmdName);
800
+ return;
801
+ }
802
+
803
+ if (cmd.run) {
804
+ await cmd.run(positional.slice(1), flags);
805
+ }
806
+ }
807
+
775
808
  main().catch((err) => {
776
809
  if (err.name !== 'CLIError') {
777
810
  ui.error(err.message || 'An unexpected error occurred');