securenow 5.18.0 → 6.0.1

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.
Files changed (85) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +40 -239
  3. package/cli.js +455 -415
  4. package/console-instrumentation.js +136 -147
  5. package/docs/ALL-FRAMEWORKS-QUICKSTART.md +455 -1339
  6. package/docs/ARCHITECTURE.md +3 -3
  7. package/docs/AUTO-BODY-CAPTURE.md +1 -1
  8. package/docs/AUTO-SETUP.md +4 -4
  9. package/docs/AUTOMATIC-IP-CAPTURE.md +5 -5
  10. package/docs/BODY-CAPTURE-QUICKSTART.md +2 -2
  11. package/docs/CHANGELOG-NEXTJS.md +35 -1
  12. package/docs/CUSTOMER-GUIDE.md +16 -16
  13. package/docs/EASIEST-SETUP.md +5 -5
  14. package/docs/ENVIRONMENT-VARIABLES.md +652 -880
  15. package/docs/EXPRESS-BODY-CAPTURE.md +12 -13
  16. package/docs/EXPRESS-SETUP-GUIDE.md +720 -719
  17. package/docs/INDEX.md +4 -22
  18. package/docs/LOGGING-GUIDE.md +708 -701
  19. package/docs/LOGGING-QUICKSTART.md +255 -234
  20. package/docs/NEXTJS-BODY-CAPTURE.md +2 -2
  21. package/docs/NEXTJS-GUIDE.md +14 -14
  22. package/docs/NEXTJS-QUICKSTART.md +1 -1
  23. package/docs/NEXTJS-WRAPPER-APPROACH.md +1 -1
  24. package/docs/QUICKSTART-BODY-CAPTURE.md +2 -2
  25. package/docs/REDACTION-EXAMPLES.md +1 -1
  26. package/docs/REQUEST-BODY-CAPTURE.md +10 -19
  27. package/docs/VERCEL-OTEL-MIGRATION.md +3 -3
  28. package/examples/README.md +6 -6
  29. package/examples/instrumentation-with-auto-capture.ts +1 -1
  30. package/examples/nextjs-env-example.txt +2 -2
  31. package/examples/nextjs-instrumentation.js +1 -1
  32. package/examples/nextjs-instrumentation.ts +1 -1
  33. package/examples/nextjs-with-logging-example.md +6 -6
  34. package/examples/nextjs-with-options.ts +1 -1
  35. package/examples/test-nextjs-setup.js +1 -1
  36. package/nextjs-auto-capture.js +207 -199
  37. package/nextjs-middleware.js +181 -186
  38. package/nextjs-webpack-config.js +53 -88
  39. package/nextjs-wrapper.js +158 -158
  40. package/nextjs.d.ts +1 -1
  41. package/nextjs.js +198 -186
  42. package/package.json +45 -67
  43. package/postinstall.js +6 -6
  44. package/register.d.ts +1 -1
  45. package/register.js +4 -39
  46. package/tracing.d.ts +1 -2
  47. package/tracing.js +26 -286
  48. package/web-vite.mjs +156 -239
  49. package/CONSUMING-APPS-GUIDE.md +0 -455
  50. package/NPM_README.md +0 -1933
  51. package/SKILL-API.md +0 -600
  52. package/SKILL-CLI.md +0 -409
  53. package/cidr.js +0 -83
  54. package/cli/apps.js +0 -585
  55. package/cli/auth.js +0 -280
  56. package/cli/client.js +0 -115
  57. package/cli/config.js +0 -173
  58. package/cli/firewall.js +0 -100
  59. package/cli/fp.js +0 -638
  60. package/cli/init.js +0 -201
  61. package/cli/monitor.js +0 -440
  62. package/cli/run.js +0 -133
  63. package/cli/security.js +0 -1064
  64. package/cli/ui.js +0 -386
  65. package/docs/API-KEYS-GUIDE.md +0 -233
  66. package/docs/AUTO-SETUP-SUMMARY.md +0 -331
  67. package/docs/BODY-CAPTURE-FIX.md +0 -261
  68. package/docs/COMPLETION-REPORT.md +0 -408
  69. package/docs/FINAL-SOLUTION.md +0 -335
  70. package/docs/FIREWALL-GUIDE.md +0 -426
  71. package/docs/IMPLEMENTATION-SUMMARY.md +0 -410
  72. package/docs/NEXTJS-BODY-CAPTURE-COMPARISON.md +0 -323
  73. package/docs/NEXTJS-SETUP-COMPLETE.md +0 -795
  74. package/docs/NUXT-GUIDE.md +0 -166
  75. package/docs/SOLUTION-SUMMARY.md +0 -312
  76. package/firewall-cloud.js +0 -212
  77. package/firewall-iptables.js +0 -139
  78. package/firewall-only.js +0 -38
  79. package/firewall-tcp.js +0 -74
  80. package/firewall.js +0 -720
  81. package/free-trial-banner.js +0 -174
  82. package/nuxt-server-plugin.mjs +0 -423
  83. package/nuxt.d.ts +0 -60
  84. package/nuxt.mjs +0 -75
  85. package/resolve-ip.js +0 -77
package/cli/apps.js DELETED
@@ -1,585 +0,0 @@
1
- 'use strict';
2
-
3
- const { api, requireAuth } = require('./client');
4
- const config = require('./config');
5
- const ui = require('./ui');
6
-
7
- const FREE_TRIAL_URL = 'https://freetrial.securenow.ai:4318';
8
- const DOMAIN_REGEX = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
9
-
10
- function instanceUrl(inst) {
11
- if (!inst) return FREE_TRIAL_URL;
12
- return `${inst.protocol || 'https'}://${inst.host}:4318`;
13
- }
14
-
15
- async function fetchInstanceMap() {
16
- try {
17
- const data = await api.get('/instances');
18
- const instances = data.instances || [];
19
- const map = {};
20
- for (const inst of instances) {
21
- map[inst._id] = inst;
22
- }
23
- return map;
24
- } catch {
25
- return {};
26
- }
27
- }
28
-
29
- async function list(args, flags) {
30
- requireAuth();
31
- const s = ui.spinner('Fetching applications');
32
-
33
- try {
34
- const [appData, instMap] = await Promise.all([
35
- api.get('/applications'),
36
- fetchInstanceMap(),
37
- ]);
38
- const apps = appData.applications || [];
39
- s.stop(`Found ${apps.length} application${apps.length !== 1 ? 's' : ''}`);
40
- console.log('');
41
-
42
- if (flags.json) {
43
- const enriched = apps.map(app => ({
44
- ...app,
45
- instanceUrl: instanceUrl(app.instanceId ? instMap[app.instanceId] : null),
46
- }));
47
- ui.json(enriched);
48
- return;
49
- }
50
-
51
- const defaultApp = config.getDefaultApp();
52
- const rows = apps.map(app => {
53
- const inst = app.instanceId ? instMap[app.instanceId] : null;
54
- const url = instanceUrl(inst);
55
- const instLabel = inst ? `${inst.name} ${ui.c.dim(url)}` : `${ui.c.green('Free Trial')} ${ui.c.dim(url)}`;
56
- return [
57
- app.name + (app.key === defaultApp ? ui.c.cyan(' (default)') : ''),
58
- ui.c.dim(app.key),
59
- instLabel,
60
- ui.timeAgo(app.createdAt),
61
- ];
62
- });
63
-
64
- ui.table(['Name', 'Key', 'Instance', 'Created'], rows);
65
- console.log('');
66
-
67
- if (apps.length > 0) {
68
- console.log(` ${ui.c.bold('Add to your .env:')}`);
69
- const first = apps.find(a => a.key === defaultApp) || apps[0];
70
- const firstInst = first.instanceId ? instMap[first.instanceId] : null;
71
- console.log(` SECURENOW_APPID=${first.key}`);
72
- console.log(` SECURENOW_INSTANCE=${instanceUrl(firstInst)}`);
73
- console.log('');
74
- }
75
-
76
- if (!defaultApp && apps.length > 0) {
77
- ui.info(`Tip: Set a default app with ${ui.c.bold('securenow config set defaultApp <key>')}`);
78
- console.log('');
79
- }
80
- } catch (err) {
81
- s.fail('Failed to fetch applications');
82
- throw err;
83
- }
84
- }
85
-
86
- async function create(args, flags) {
87
- requireAuth();
88
-
89
- let name = args[0];
90
- if (!name) {
91
- name = await ui.prompt('Application name');
92
- if (!name) {
93
- ui.error('Application name is required');
94
- process.exit(1);
95
- }
96
- }
97
-
98
- const body = { name };
99
- if (flags.hosts) {
100
- body.hosts = flags.hosts.split(',').map(h => h.trim());
101
- }
102
-
103
- if (flags.instance) {
104
- body.instanceId = flags.instance;
105
- } else if (process.stdin.isTTY) {
106
- body.instanceId = await pickInstance();
107
- }
108
-
109
- const selectedInstanceId = body.instanceId;
110
-
111
- const s = ui.spinner(`Creating application "${name}"`);
112
- try {
113
- const result = await api.post('/applications', body);
114
- const app = result.application || result;
115
- s.stop(`Application created`);
116
-
117
- let inst = null;
118
- if (selectedInstanceId) {
119
- try {
120
- const instData = await api.get(`/instances/${selectedInstanceId}`);
121
- inst = instData.instance || null;
122
- } catch {}
123
- }
124
- const envUrl = instanceUrl(inst);
125
-
126
- console.log('');
127
- ui.keyValue([
128
- ['Name', app.name],
129
- ['Key', ui.c.green(ui.c.bold(app.key))],
130
- ['ID', ui.c.dim(app._id)],
131
- ['Instance', inst ? `${inst.name} ${ui.c.dim(`(${envUrl})`)}` : `${ui.c.green('Free Trial')} ${ui.c.dim(`(${envUrl})`)}`],
132
- ['Hosts', app.hosts?.length ? app.hosts.join(', ') : ui.c.dim('none')],
133
- ]);
134
-
135
- console.log('');
136
- console.log(` ${ui.c.bold('Add to your .env.local:')}`);
137
- console.log('');
138
- console.log(` SECURENOW_APPID=${app.key}`);
139
- console.log(` SECURENOW_INSTANCE=${envUrl}`);
140
- console.log('');
141
- ui.info(`Set as default: ${ui.c.bold(`securenow config set defaultApp ${app.key}`)}`);
142
- console.log('');
143
-
144
- if (flags.json) {
145
- ui.json({ ...app, instanceUrl: envUrl });
146
- }
147
-
148
- if (app.hosts && app.hosts.length > 0) {
149
- await offerSubdomainDiscovery(app.hosts, selectedInstanceId, flags);
150
- }
151
- } catch (err) {
152
- s.fail('Failed to create application');
153
- throw err;
154
- }
155
- }
156
-
157
- async function pickInstance() {
158
- const s = ui.spinner('Loading instances');
159
- let instances = [];
160
- try {
161
- const data = await api.get('/instances');
162
- instances = data.instances || [];
163
- s.stop('Instances loaded');
164
- } catch {
165
- s.stop('Could not load instances');
166
- return null;
167
- }
168
-
169
- const FREE_TRIAL_LABEL = `${ui.c.green('Free Trial')} ${ui.c.dim('— SecureNow managed instance (no setup needed)')}`;
170
-
171
- const choices = [{ label: FREE_TRIAL_LABEL, value: null }];
172
-
173
- for (const inst of instances) {
174
- const status = inst.status === 'active' ? ui.c.green('●') : ui.c.red('●');
175
- const apps = inst.linkedApps ? ui.c.dim(` (${inst.linkedApps} app${inst.linkedApps !== 1 ? 's' : ''})`) : '';
176
- choices.push({
177
- label: `${status} ${inst.name}${apps} ${ui.c.dim(`[${inst._id}]`)}`,
178
- value: inst._id,
179
- });
180
- }
181
-
182
- const appUrl = config.getAppUrl();
183
- choices.push({
184
- label: `${ui.c.cyan('+')} Create a new instance ${ui.c.dim('(opens browser)')}`,
185
- value: '__new__',
186
- });
187
-
188
- const selected = await ui.select('Which instance should this app use?', choices);
189
-
190
- if (selected === '__new__') {
191
- const url = `${appUrl}/dashboard/settings/instances`;
192
- ui.info(`Opening ${ui.c.underline(url)}`);
193
- openBrowser(url);
194
- ui.info('Create your instance in the browser, then run this command again.');
195
- process.exit(0);
196
- }
197
-
198
- return selected;
199
- }
200
-
201
- function openBrowser(url) {
202
- const { execFileSync } = require('child_process');
203
- try {
204
- if (process.platform === 'darwin') execFileSync('open', [url], { stdio: 'ignore' });
205
- else if (process.platform === 'win32') execFileSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore' });
206
- else execFileSync('xdg-open', [url], { stdio: 'ignore' });
207
- } catch {
208
- ui.warn(`Could not open browser. Visit: ${url}`);
209
- }
210
- }
211
-
212
- async function info(args, flags) {
213
- requireAuth();
214
-
215
- const id = args[0];
216
- if (!id) {
217
- ui.error('Application ID is required. Usage: securenow apps info <id>');
218
- process.exit(1);
219
- }
220
-
221
- const s = ui.spinner('Fetching application details');
222
- try {
223
- const data = await api.get(`/applications/${id}`);
224
- const app = data.application || data;
225
-
226
- let inst = null;
227
- if (app.instanceId) {
228
- try {
229
- const instData = await api.get(`/instances/${app.instanceId}`);
230
- inst = instData.instance || null;
231
- } catch {}
232
- }
233
- const envUrl = instanceUrl(inst);
234
- s.stop('Application details loaded');
235
-
236
- if (flags.json) {
237
- ui.json({ ...app, instanceUrl: envUrl });
238
- return;
239
- }
240
-
241
- console.log('');
242
- ui.heading(app.name);
243
- console.log('');
244
- ui.keyValue([
245
- ['Key', ui.c.bold(app.key)],
246
- ['ID', ui.c.dim(app._id)],
247
- ['Instance', inst ? `${inst.name} ${ui.c.dim(`(${envUrl})`)}` : `${ui.c.green('Free Trial')} ${ui.c.dim(`(${envUrl})`)}`],
248
- ['Hosts', app.hosts?.length ? app.hosts.join(', ') : ui.c.dim('none')],
249
- ['Created', app.createdAt ? new Date(app.createdAt).toLocaleString() : '—'],
250
- ['Updated', app.updatedAt ? new Date(app.updatedAt).toLocaleString() : '—'],
251
- ]);
252
-
253
- console.log('');
254
- console.log(` ${ui.c.bold('Environment variables:')}`);
255
- console.log(` SECURENOW_APPID=${app.key}`);
256
- console.log(` SECURENOW_INSTANCE=${envUrl}`);
257
- console.log('');
258
- } catch (err) {
259
- s.fail('Failed to fetch application');
260
- throw err;
261
- }
262
- }
263
-
264
- async function remove(args, flags) {
265
- requireAuth();
266
-
267
- const id = args[0];
268
- if (!id) {
269
- ui.error('Application ID is required. Usage: securenow apps delete <id>');
270
- process.exit(1);
271
- }
272
-
273
- if (!flags.force && !flags.yes) {
274
- const ok = await ui.confirm('Are you sure you want to delete this application?');
275
- if (!ok) {
276
- ui.info('Cancelled');
277
- return;
278
- }
279
- }
280
-
281
- const s = ui.spinner('Deleting application');
282
- try {
283
- await api.delete(`/applications/${id}`);
284
- s.stop('Application deleted');
285
- } catch (err) {
286
- s.fail('Failed to delete application');
287
- throw err;
288
- }
289
- }
290
-
291
- async function setDefault(args) {
292
- const key = args[0];
293
- if (!key) {
294
- ui.error('App key is required. Usage: securenow apps default <key>');
295
- process.exit(1);
296
- }
297
- config.setConfigValue('defaultApp', key);
298
- ui.success(`Default application set to ${ui.c.bold(key)}`);
299
- }
300
-
301
- function extractRootDomains(hosts) {
302
- const domains = new Set();
303
- for (const host of hosts || []) {
304
- let cleaned = host.trim().toLowerCase();
305
- try {
306
- if (cleaned.startsWith('http://') || cleaned.startsWith('https://')) {
307
- cleaned = new URL(cleaned).hostname;
308
- }
309
- } catch {}
310
- if (DOMAIN_REGEX.test(cleaned)) {
311
- const parts = cleaned.split('.');
312
- const root = parts.length > 2 ? parts.slice(-2).join('.') : cleaned;
313
- domains.add(root);
314
- }
315
- }
316
- return [...domains];
317
- }
318
-
319
- async function offerSubdomainDiscovery(hosts, instanceId, flags) {
320
- if (!process.stdin.isTTY) return;
321
-
322
- const domains = extractRootDomains(hosts);
323
- if (domains.length === 0) return;
324
-
325
- const shouldDiscover = await ui.confirm(
326
- `Domain${domains.length > 1 ? 's' : ''} detected (${domains.join(', ')}). Discover subdomains and add them as apps?`
327
- );
328
- if (!shouldDiscover) return;
329
-
330
- await discoverAndAdd(domains, instanceId, flags);
331
- }
332
-
333
- async function discoverAndAdd(domains, instanceId, flags) {
334
- const s = ui.spinner(`Scanning ${domains.length} domain${domains.length > 1 ? 's' : ''} for subdomains`);
335
-
336
- let allSubdomains = [];
337
- try {
338
- for (const domain of domains) {
339
- s.update(`Scanning ${domain}...`);
340
- const result = await api.get(`/subdomains`, { query: { domain } });
341
- if (result.success && result.subdomains) {
342
- allSubdomains.push(...result.subdomains);
343
- }
344
- }
345
- s.stop(`Found ${allSubdomains.length} subdomain${allSubdomains.length !== 1 ? 's' : ''}`);
346
- } catch (err) {
347
- s.fail('Failed to discover subdomains');
348
- throw err;
349
- }
350
-
351
- if (allSubdomains.length === 0) {
352
- ui.info('No subdomains found.');
353
- return;
354
- }
355
-
356
- // Filter out subdomains already registered as apps
357
- let existingHosts;
358
- try {
359
- const appData = await api.get('/applications');
360
- const apps = appData.applications || [];
361
- existingHosts = new Set(apps.flatMap(a => (a.hosts || []).map(h => h.toLowerCase())));
362
- } catch {
363
- existingHosts = new Set();
364
- }
365
-
366
- const newSubdomains = allSubdomains.filter(
367
- (s) => !existingHosts.has(s.subdomain.toLowerCase())
368
- );
369
-
370
- if (newSubdomains.length === 0) {
371
- ui.info('All discovered subdomains are already registered as apps.');
372
- return;
373
- }
374
-
375
- console.log('');
376
- ui.info(`${newSubdomains.length} new subdomain${newSubdomains.length !== 1 ? 's' : ''} found (${allSubdomains.length - newSubdomains.length} already tracked):`);
377
-
378
- if (flags && flags.json) {
379
- ui.json(newSubdomains);
380
- return;
381
- }
382
-
383
- const choices = newSubdomains.map((sub) => ({
384
- label: sub.subdomain,
385
- detail: `IP: ${sub.ip || 'none'}${sub.cloudflare ? ' · Cloudflare' : ''}`,
386
- value: sub.subdomain,
387
- }));
388
-
389
- const selected = await ui.multiSelect(
390
- 'Which subdomains should be added as monitored apps?',
391
- choices
392
- );
393
-
394
- if (selected.length === 0) {
395
- ui.info('No subdomains selected.');
396
- return;
397
- }
398
-
399
- const createSpinner = ui.spinner(`Creating ${selected.length} application${selected.length !== 1 ? 's' : ''}`);
400
- try {
401
- const result = await api.post('/applications/bulk', {
402
- subdomains: selected,
403
- instanceId: instanceId || null,
404
- });
405
- const count = result.count || selected.length;
406
- createSpinner.stop(`Created ${count} application${count !== 1 ? 's' : ''}`);
407
-
408
- console.log('');
409
- if (result.applications) {
410
- const rows = result.applications.map((app) => [
411
- app.name,
412
- ui.c.dim(app.key),
413
- app.hosts?.join(', ') || '',
414
- ]);
415
- ui.table(['Name', 'Key', 'Hosts'], rows);
416
- }
417
- console.log('');
418
- } catch (err) {
419
- createSpinner.fail('Failed to create applications');
420
- throw err;
421
- }
422
- }
423
-
424
- async function discover(args, flags) {
425
- requireAuth();
426
-
427
- const appId = args[0];
428
- let domains = [];
429
- let instanceId = null;
430
-
431
- if (appId) {
432
- const s = ui.spinner('Fetching application');
433
- try {
434
- const data = await api.get(`/applications/${appId}`);
435
- const app = data.application || data;
436
- s.stop(`Application: ${app.name}`);
437
- domains = extractRootDomains(app.hosts);
438
- instanceId = app.instanceId;
439
- } catch (err) {
440
- s.fail('Failed to fetch application');
441
- throw err;
442
- }
443
- } else if (flags.domain) {
444
- domains = flags.domain.split(',').map(d => d.trim());
445
- } else {
446
- const domain = await ui.prompt('Domain to scan (e.g. example.com)');
447
- if (!domain) {
448
- ui.error('Domain is required');
449
- process.exit(1);
450
- }
451
- domains = domain.split(',').map(d => d.trim());
452
- }
453
-
454
- if (domains.length === 0) {
455
- ui.error('No domains found in application hosts. Specify a domain with --domain');
456
- process.exit(1);
457
- }
458
-
459
- if (flags.instance) {
460
- instanceId = flags.instance;
461
- }
462
-
463
- await discoverAndAdd(domains, instanceId, flags);
464
- }
465
-
466
- async function scan(args, flags) {
467
- requireAuth();
468
- const s = ui.spinner('Running subdomain discovery across all applications');
469
-
470
- try {
471
- const appData = await api.get('/applications');
472
- const apps = appData.applications || [];
473
-
474
- const allDomains = new Set();
475
- let firstInstanceId = null;
476
- for (const app of apps) {
477
- const roots = extractRootDomains(app.hosts);
478
- for (const r of roots) allDomains.add(r);
479
- if (!firstInstanceId && app.instanceId) firstInstanceId = app.instanceId;
480
- }
481
-
482
- if (allDomains.size === 0) {
483
- s.stop('No domains found across your applications');
484
- ui.info('Add domains to your application hosts first, then run this command again.');
485
- return;
486
- }
487
-
488
- s.update(`Found ${allDomains.size} unique domain${allDomains.size !== 1 ? 's' : ''}, scanning...`);
489
-
490
- const existingHosts = new Set(
491
- apps.flatMap((a) => (a.hosts || []).map((h) => h.toLowerCase()))
492
- );
493
-
494
- let totalNew = 0;
495
- let totalCreated = 0;
496
- const allNew = [];
497
-
498
- for (const domain of allDomains) {
499
- s.update(`Scanning ${domain}...`);
500
- try {
501
- const result = await api.get('/subdomains', { query: { domain } });
502
- if (result.success && result.subdomains) {
503
- const newSubs = result.subdomains.filter(
504
- (sub) => !existingHosts.has(sub.subdomain.toLowerCase())
505
- );
506
- totalNew += newSubs.length;
507
- allNew.push(...newSubs);
508
- for (const sub of newSubs) {
509
- existingHosts.add(sub.subdomain.toLowerCase());
510
- }
511
- }
512
- } catch {
513
- ui.warn(`Failed to scan ${domain}`);
514
- }
515
- }
516
-
517
- s.stop(`Scanned ${allDomains.size} domain${allDomains.size !== 1 ? 's' : ''}, found ${totalNew} new subdomain${totalNew !== 1 ? 's' : ''}`);
518
-
519
- if (totalNew === 0) {
520
- ui.success('All subdomains are already tracked. Nothing to add.');
521
- return;
522
- }
523
-
524
- if (flags.yes || flags.force) {
525
- const createSpinner = ui.spinner(`Creating ${totalNew} application${totalNew !== 1 ? 's' : ''}`);
526
- try {
527
- const subdomainNames = allNew.map((s) => s.subdomain);
528
- const result = await api.post('/applications/bulk', {
529
- subdomains: subdomainNames,
530
- instanceId: firstInstanceId || null,
531
- });
532
- totalCreated = result.count || subdomainNames.length;
533
- createSpinner.stop(`Created ${totalCreated} application${totalCreated !== 1 ? 's' : ''}`);
534
- } catch (err) {
535
- createSpinner.fail('Failed to create applications');
536
- throw err;
537
- }
538
- } else if (process.stdin.isTTY) {
539
- const choices = allNew.map((sub) => ({
540
- label: sub.subdomain,
541
- detail: `IP: ${sub.ip || 'none'}${sub.cloudflare ? ' · Cloudflare' : ''}`,
542
- value: sub.subdomain,
543
- }));
544
-
545
- const selected = await ui.multiSelect(
546
- 'Which subdomains should be added as monitored apps?',
547
- choices
548
- );
549
-
550
- if (selected.length === 0) {
551
- ui.info('No subdomains selected.');
552
- return;
553
- }
554
-
555
- const createSpinner = ui.spinner(`Creating ${selected.length} application${selected.length !== 1 ? 's' : ''}`);
556
- try {
557
- const result = await api.post('/applications/bulk', {
558
- subdomains: selected,
559
- instanceId: firstInstanceId || null,
560
- });
561
- totalCreated = result.count || selected.length;
562
- createSpinner.stop(`Created ${totalCreated} application${totalCreated !== 1 ? 's' : ''}`);
563
- } catch (err) {
564
- createSpinner.fail('Failed to create applications');
565
- throw err;
566
- }
567
- } else {
568
- console.log('');
569
- for (const sub of allNew) {
570
- console.log(` ${sub.subdomain} ${ui.c.dim(`(IP: ${sub.ip || 'none'})`)}`);
571
- }
572
- console.log('');
573
- ui.info(`Run with --yes to auto-create all ${totalNew} apps.`);
574
- }
575
-
576
- if (flags.json) {
577
- ui.json({ domainsScanned: allDomains.size, newSubdomains: totalNew, appsCreated: totalCreated });
578
- }
579
- } catch (err) {
580
- s.fail('Subdomain scan failed');
581
- throw err;
582
- }
583
- }
584
-
585
- module.exports = { list, create, info, remove, setDefault, discover, scan };