dbsafedump 0.0.1 → 1.0.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/bin/cli.js CHANGED
@@ -1 +1,719 @@
1
- console.log('DBSafeDump CLI placeholder');
1
+ const { Command } = require('commander');
2
+ const chalk = require('chalk');
3
+ const inquirer = require('inquirer');
4
+ const { loadConfig, saveConfig, getDefaultConfig, generateSalt, generateConfig, getDefaultConfigPath, updateConfigField, listProfiles, validateProfile } = require('../src/config');
5
+ const { runDump, runSchema, runImport, runScrub, runTestConnections } = require('../src/commands');
6
+ const { checkLicense, getTierInfo, getMaxRows, canScrub, canUseMultiProfile, canUseCI, canUseSmartDiscovery, shouldShowBadge } = require('../src/license');
7
+ const { activate, deactivate, isActivated, getLicenseInfo, getCachePath, removeAll, saveLicenseKey, loadCache, decrypt } = require('../src/license');
8
+ const { getFingerprintData } = require('../src/license');
9
+ const { discoverSensitiveColumns, applyDiscoveryToConfig } = require('../src/smartDiscovery');
10
+ const { createConnection, closeConnection, getTables } = require('../src/database');
11
+
12
+ let currentTier = 'free';
13
+
14
+ async function checkAndShowLicense() {
15
+ const result = await checkLicense();
16
+ currentTier = result.tier || 'free';
17
+
18
+ if (shouldShowBadge()) {
19
+ console.log(chalk.gray(`DBSafeDump FREE Edition - Row limit: ${getMaxRows()}/table`));
20
+ console.log(chalk.gray('Activate license: dbsafedump activate "LICENSE-KEY"'));
21
+ console.log(chalk.gray('Upgrade at: https://dbsafedump.com/pricing\n'));
22
+ }
23
+
24
+ return currentTier;
25
+ }
26
+
27
+ const program = new Command();
28
+
29
+ program
30
+ .name('dbsafedump')
31
+ .description('Safe database dump with anonymization')
32
+ .version('1.0.1');
33
+
34
+ program
35
+ .command('init')
36
+ .description('Initialize DBSafeDump configuration')
37
+ .option('-f, --force', 'Overwrite existing config')
38
+ .action(async (options) => {
39
+ const configPath = getDefaultConfigPath();
40
+ const fs = require('fs');
41
+
42
+ if (fs.existsSync(configPath) && !options.force) {
43
+ console.log(chalk.yellow(`Config already exists: ${configPath}`));
44
+ console.log(chalk.gray('Use --force to overwrite'));
45
+ console.log(chalk.gray('Running config wizard...\n'));
46
+ } else {
47
+ const config = generateConfig();
48
+ saveConfig(config);
49
+ console.log(chalk.green(`Configuration created: ${configPath}`));
50
+ console.log(chalk.gray('\nGenerated unique salt for deterministic masking.\n'));
51
+ }
52
+
53
+ const { runConfigWizard } = require('../src/commands/config');
54
+ await runConfigWizard();
55
+ });
56
+
57
+ program
58
+ .command('activate')
59
+ .description('Activate license on this machine')
60
+ .argument('<key>', 'License key')
61
+ .action(async (key) => {
62
+ console.log(chalk.blue(`\nDBSafeDump - License Activation\n`));
63
+
64
+ if (isActivated()) {
65
+ console.log(chalk.yellow('License already activated on this machine.'));
66
+ console.log(chalk.gray('Use: dbsafedump deactivate first\n'));
67
+ return;
68
+ }
69
+
70
+ console.log(chalk.gray('Connecting to license server...\n'));
71
+
72
+ try {
73
+ const result = await activate(key);
74
+
75
+ if (result.data && result.checksum) {
76
+ try {
77
+ const data = decrypt(result.data, result.checksum);
78
+
79
+ if (data.valid) {
80
+ saveLicenseKey(key);
81
+
82
+ console.log(chalk.green('License activated successfully!'));
83
+ console.log(chalk.gray(` Tier: ${data.tier.toUpperCase()}`));
84
+ console.log(chalk.gray(` Expires: ${data.expiresAt || 'N/A'}`));
85
+ console.log('');
86
+ } else {
87
+ console.log(chalk.red('License activation failed.'));
88
+ if (data.error) {
89
+ console.log(chalk.red(` Error: ${data.error}`));
90
+ }
91
+ console.log('');
92
+ console.log(chalk.gray(`[LOG] Activation failed: ${JSON.stringify(data)}\n`));
93
+ }
94
+ } catch (decryptError) {
95
+ console.log(chalk.red('Failed to verify license response.'));
96
+ console.log(chalk.red(` ${decryptError.message}`));
97
+ console.log(chalk.gray('\n This may indicate a server configuration issue.\n'));
98
+ console.log(chalk.gray(`[LOG] Decrypt error: ${decryptError.message}\n`));
99
+ }
100
+ } else {
101
+ console.log(chalk.red('Invalid response from license server.'));
102
+ console.log(chalk.gray(' Received:', JSON.stringify(result)));
103
+ console.log('');
104
+ console.log(chalk.gray(`[LOG] Invalid API response: ${JSON.stringify(result)}\n`));
105
+ }
106
+ } catch (error) {
107
+ console.log(chalk.red('Activation failed:'));
108
+ console.log(chalk.red(` ${error.message}`));
109
+ console.log(chalk.gray('\nMake sure you have an internet connection.\n'));
110
+ console.log(chalk.gray(`[LOG] Activation error: ${error.message}\n`));
111
+ }
112
+ });
113
+
114
+ program
115
+ .command('deactivate')
116
+ .description('Deactivate license (releases for another machine)')
117
+ .action(async () => {
118
+ console.log(chalk.blue(`\nDBSafeDump - License Deactivation\n`));
119
+
120
+ if (!isActivated()) {
121
+ console.log(chalk.yellow('No license activated on this machine.\n'));
122
+ return;
123
+ }
124
+
125
+ const licenseInfo = getLicenseInfo();
126
+ console.log(chalk.gray(`Current license: ${licenseInfo.key}`));
127
+ console.log(chalk.gray(`Tier: ${(licenseInfo.cached?.tier || 'unknown').toUpperCase()}\n`));
128
+
129
+ try {
130
+ const result = await deactivate(licenseInfo.key);
131
+
132
+ let reason = null;
133
+
134
+ if (result.data && result.checksum) {
135
+ try {
136
+ const data = decrypt(result.data, result.checksum);
137
+ reason = data.reason;
138
+ } catch (e) {
139
+ // Nie udało się odszyfrować
140
+ }
141
+ }
142
+
143
+ if (reason === 'license_deactivated') {
144
+ removeAll();
145
+ console.log(chalk.green('License deactivated successfully.'));
146
+ console.log(chalk.gray('You can now activate this license on another machine.\n'));
147
+ } else if (reason === 'fingerprint_mismatch') {
148
+ console.log(chalk.red('This license is registered to a different machine.'));
149
+ console.log(chalk.gray('Local license data removed.\n'));
150
+ removeAll();
151
+ } else if (reason === 'license_not_found') {
152
+ console.log(chalk.red('License not found on server.'));
153
+ console.log(chalk.gray('Local license data removed.\n'));
154
+ removeAll();
155
+ } else {
156
+ console.log(chalk.yellow('Warning: Could not deactivate on server.'));
157
+ console.log(chalk.gray('Removing local license data...\n'));
158
+ removeAll();
159
+ }
160
+ } catch (error) {
161
+ console.log(chalk.yellow('Warning: Could not connect to server.'));
162
+ console.log(chalk.gray('Removing local license data anyway...\n'));
163
+ removeAll();
164
+ console.log(chalk.green('Local license removed.'));
165
+ console.log(chalk.gray('Note: Server-side license may still be registered to this machine.\n'));
166
+ }
167
+ });
168
+
169
+ program
170
+ .command('license')
171
+ .description('Show current license status')
172
+ .action(async () => {
173
+ console.log(chalk.blue(`\nDBSafeDump - License Status\n`));
174
+
175
+ if (!isActivated()) {
176
+ console.log(chalk.yellow('No license activated.'));
177
+ console.log(chalk.gray('Usage: dbsafedump activate "LICENSE-KEY"\n'));
178
+ return;
179
+ }
180
+
181
+ const info = getLicenseInfo();
182
+ const cached = loadCache();
183
+
184
+ console.log(chalk.green('License is activated\n'));
185
+ console.log(chalk.gray(`Key: ${info.key}`));
186
+ console.log(chalk.gray(`Tier: ${(cached?.tier || 'unknown').toUpperCase()}`));
187
+ console.log(chalk.gray(`Valid: ${cached?.valid ? 'Yes' : 'Unknown'}`));
188
+
189
+ if (info.cacheAge) {
190
+ const ageMinutes = Math.floor(info.cacheAge / 60000);
191
+ console.log(chalk.gray(`Cache age: ${ageMinutes} minutes`));
192
+ }
193
+
194
+ console.log(chalk.gray(`\nStored in: ${getCachePath()}\n`));
195
+
196
+ await checkAndShowLicense();
197
+ });
198
+
199
+ program
200
+ .command('profiles')
201
+ .description('List available profiles (STARTER/PRO)')
202
+ .option('-c, --config <file>', 'Config file', getDefaultConfigPath())
203
+ .action(async (options) => {
204
+ const config = loadConfig(options.config);
205
+
206
+ if (!config) {
207
+ console.log(chalk.yellow('Config file not found. Running configuration wizard...\n'));
208
+ const runConfig = require('../src/commands/config');
209
+ await runConfig();
210
+ return;
211
+ }
212
+
213
+ await checkAndShowLicense();
214
+
215
+ if (!canUseMultiProfile()) {
216
+ console.log(chalk.red('\nMulti-profile requires STARTER or PRO license.'));
217
+ console.log(chalk.gray('Get your license at: https://dbsafedump.com/pricing\n'));
218
+ return;
219
+ }
220
+
221
+ const profiles = listProfiles(options.config);
222
+
223
+ if (profiles.length === 0) {
224
+ console.log(chalk.yellow('\nNo profiles defined in config.'));
225
+ console.log(chalk.gray('\nAdd profiles to config.yml:'));
226
+ console.log(chalk.gray(' profiles:'));
227
+ console.log(chalk.gray(' dev:'));
228
+ console.log(chalk.gray(' connections:'));
229
+ console.log(chalk.gray(' source: { host: localhost, ... }'));
230
+ console.log(chalk.gray('\nThen use: dbsafedump dump --profile dev\n'));
231
+ return;
232
+ }
233
+
234
+ console.log(chalk.blue(`\nAvailable profiles:\n`));
235
+ for (const name of profiles) {
236
+ const validation = validateProfile(config, name);
237
+ if (validation.valid) {
238
+ console.log(chalk.green(` ${name}`));
239
+ } else {
240
+ console.log(chalk.red(` ${name} (invalid: ${validation.error})`));
241
+ }
242
+ }
243
+ console.log('');
244
+ });
245
+
246
+ program
247
+ .command('generate-salt')
248
+ .description('Generate new salt for config (forces re-masking)')
249
+ .option('-c, --config <file>', 'Config file', getDefaultConfigPath())
250
+ .option('-y, --yes', 'Skip confirmation')
251
+ .action(async (options) => {
252
+ const configPath = options.config || getDefaultConfigPath();
253
+ const fs = require('fs');
254
+
255
+ const config = loadConfig(configPath);
256
+
257
+ if (!config) {
258
+ console.log(chalk.red('Config file not found'));
259
+ console.log(chalk.gray('Run: dbsafedump init first'));
260
+ return;
261
+ }
262
+
263
+ const oldSalt = config.salt;
264
+
265
+ if (!options.yes) {
266
+ console.log(chalk.yellow('Warning: Generating new salt will change all masked values!'));
267
+ console.log(chalk.yellow(' Existing masked data will no longer match.'));
268
+ console.log(chalk.gray(` Old salt: ${oldSalt ? oldSalt.substring(0, 8) + '...' : 'none'}\n`));
269
+
270
+ const { confirm } = await inquirer.prompt([
271
+ {
272
+ type: 'confirm',
273
+ name: 'confirm',
274
+ message: 'Generate new salt?',
275
+ default: false
276
+ }
277
+ ]);
278
+
279
+ if (!confirm) {
280
+ console.log(chalk.gray('Cancelled.\n'));
281
+ return;
282
+ }
283
+ }
284
+
285
+ const newSalt = generateSalt();
286
+ updateConfigField(configPath, 'salt', newSalt);
287
+
288
+ console.log(chalk.green('New salt generated!'));
289
+ console.log(chalk.gray(` Old salt: ${oldSalt ? oldSalt.substring(0, 16) + '...' : 'none'}`));
290
+ console.log(chalk.green(` New salt: ${newSalt.substring(0, 16)}...`));
291
+ console.log(chalk.yellow('\nWarning: Re-run dump/import to apply new masking!\n'));
292
+ });
293
+
294
+ async function runDumpWithSchema(config, options) {
295
+ const isCI = options.ci || options.yes;
296
+
297
+ if (!isCI) {
298
+ console.log(chalk.blue(`\nRunning schema check first...\n`));
299
+ await runSchema(config);
300
+
301
+ console.log(chalk.yellow(`\nProceed with dump?`));
302
+ const { confirm } = await inquirer.prompt([
303
+ {
304
+ type: 'confirm',
305
+ name: 'confirm',
306
+ message: 'Continue with dump?',
307
+ default: true
308
+ }
309
+ ]);
310
+
311
+ if (!confirm) {
312
+ console.log(chalk.gray('\nDump cancelled.\n'));
313
+ return;
314
+ }
315
+ }
316
+
317
+ if (isCI) {
318
+ console.log(chalk.gray(`\n[CI MODE] Running in automated mode...\n`));
319
+ }
320
+
321
+ await runDump(config, options);
322
+ }
323
+
324
+ program
325
+ .command('dump')
326
+ .description('Dump database with anonymization')
327
+ .option('-o, --output <file>', 'Output file', 'dump.sql')
328
+ .option('-c, --config <file>', 'Config file', getDefaultConfigPath())
329
+ .option('-p, --profile <name>', 'Use named profile (STARTER/PRO)')
330
+ .option('-y, --yes', 'Skip schema check and confirmation (for CI/CD)')
331
+ .option('--ci', 'CI/CD mode - no prompts (PRO)')
332
+ .action(async (options) => {
333
+ try {
334
+ const config = loadConfig(options.config, options.profile);
335
+
336
+ if (!config) {
337
+ console.log(chalk.red('Config file not found'));
338
+ console.log(chalk.gray('Run: dbsafedump init'));
339
+ return;
340
+ }
341
+
342
+ await checkAndShowLicense();
343
+
344
+ if (options.profile && !canUseMultiProfile()) {
345
+ console.log(chalk.red('\nProfiles require STARTER or PRO license.'));
346
+ console.log(chalk.gray('Get your license at: https://dbsafedump.com/pricing\n'));
347
+ return;
348
+ }
349
+
350
+ if (options.ci && !canUseCI()) {
351
+ console.log(chalk.red('\nCI/CD mode requires PRO license.'));
352
+ console.log(chalk.gray('Get your license at: https://dbsafedump.com/pricing\n'));
353
+ return;
354
+ }
355
+
356
+ if (!config.connections || !config.connections.source) {
357
+ console.log(chalk.red('Invalid config: missing source connection'));
358
+ return;
359
+ }
360
+
361
+ await runDumpWithSchema(config, options);
362
+ } catch (error) {
363
+ console.error(chalk.red(`\nError: ${error.message}`));
364
+ process.exit(1);
365
+ }
366
+ });
367
+
368
+ async function runImportWithSchema(config, options) {
369
+ const validModes = ['truncate', 'append'];
370
+ const isCI = options.ci || options.yes;
371
+
372
+ if (!isCI) {
373
+ console.log(chalk.blue(`\nRunning schema check first...\n`));
374
+ await runSchema(config);
375
+
376
+ console.log(chalk.yellow(`\nProceed with import?`));
377
+ const { confirm } = await inquirer.prompt([
378
+ {
379
+ type: 'confirm',
380
+ name: 'confirm',
381
+ message: 'Continue with import?',
382
+ default: true
383
+ }
384
+ ]);
385
+
386
+ if (!confirm) {
387
+ console.log(chalk.gray('\nImport cancelled.\n'));
388
+ return;
389
+ }
390
+
391
+ const { mode } = await inquirer.prompt([
392
+ {
393
+ type: 'list',
394
+ name: 'mode',
395
+ message: 'Select import mode:',
396
+ choices: [
397
+ { name: 'Truncate & Replace', value: 'truncate' },
398
+ { name: 'Append', value: 'append' }
399
+ ],
400
+ default: 'truncate'
401
+ }
402
+ ]);
403
+
404
+ options.mode = mode;
405
+ }
406
+
407
+ if (isCI) {
408
+ console.log(chalk.gray(`\n[CI MODE] Running in automated mode...\n`));
409
+ }
410
+
411
+ await runImport(config, options);
412
+ }
413
+
414
+ program
415
+ .command('import')
416
+ .description('Dump and import directly to target database (STARTER/PRO only)')
417
+ .option('-c, --config <file>', 'Config file', getDefaultConfigPath())
418
+ .option('-p, --profile <name>', 'Use named profile (STARTER/PRO)')
419
+ .option('-m, --mode <mode>', 'Import mode: truncate or append (skip prompt)', 'prompt')
420
+ .option('-y, --yes', 'Skip schema check and confirmation (for CI/CD)')
421
+ .option('--ci', 'CI/CD mode - no prompts (PRO)')
422
+ .action(async (options) => {
423
+ try {
424
+ const validModes = ['truncate', 'append', 'prompt'];
425
+ if (options.mode && !validModes.includes(options.mode)) {
426
+ console.log(chalk.red(`Invalid mode: ${options.mode}`));
427
+ console.log(chalk.gray('Valid modes: truncate, append'));
428
+ return;
429
+ }
430
+
431
+ if (options.mode === 'prompt') {
432
+ options.mode = undefined;
433
+ }
434
+
435
+ const config = loadConfig(options.config, options.profile);
436
+
437
+ if (!config) {
438
+ console.log(chalk.red('Config file not found'));
439
+ console.log(chalk.gray('Run: dbsafedump init'));
440
+ return;
441
+ }
442
+
443
+ await checkAndShowLicense();
444
+
445
+ if (options.profile && !canUseMultiProfile()) {
446
+ console.log(chalk.red('\nProfiles require STARTER or PRO license.'));
447
+ console.log(chalk.gray('Get your license at: https://dbsafedump.com/pricing\n'));
448
+ return;
449
+ }
450
+
451
+ if (options.ci && !canUseCI()) {
452
+ console.log(chalk.red('\nCI/CD mode requires PRO license.'));
453
+ console.log(chalk.gray('Get your license at: https://dbsafedump.com/pricing\n'));
454
+ return;
455
+ }
456
+
457
+ if (!canScrub()) {
458
+ console.log(chalk.red('\nImport/Scrub requires STARTER or PRO license.'));
459
+ console.log(chalk.gray('Get your license at: https://dbsafedump.com/pricing\n'));
460
+ return;
461
+ }
462
+
463
+ if (!config.connections || !config.connections.source || !config.connections.target) {
464
+ console.log(chalk.red('Invalid config: missing source or target connection'));
465
+ return;
466
+ }
467
+
468
+ await runImportWithSchema(config, options);
469
+ } catch (error) {
470
+ console.error(chalk.red(`\nError: ${error.message}`));
471
+ process.exit(1);
472
+ }
473
+ });
474
+
475
+ program
476
+ .command('schema')
477
+ .description('Show database schema (tables, keys, row counts)')
478
+ .option('-c, --config <file>', 'Config file', getDefaultConfigPath())
479
+ .action(async (options) => {
480
+ try {
481
+ const config = loadConfig(options.config);
482
+
483
+ if (!config) {
484
+ console.log(chalk.red('Config file not found'));
485
+ console.log(chalk.gray('Run: dbsafedump init'));
486
+ return;
487
+ }
488
+
489
+ if (!config.connections || !config.connections.source) {
490
+ console.log(chalk.red('Invalid config: missing source connection'));
491
+ return;
492
+ }
493
+
494
+ await checkAndShowLicense();
495
+ await runSchema(config);
496
+ } catch (error) {
497
+ console.error(chalk.red(`\nError: ${error.message}`));
498
+ process.exit(1);
499
+ }
500
+ });
501
+
502
+ program
503
+ .command('test-connection')
504
+ .description('Test database connections (source and target)')
505
+ .option('-c, --config <file>', 'Config file', getDefaultConfigPath())
506
+ .action(async (options) => {
507
+ try {
508
+ const config = loadConfig(options.config);
509
+
510
+ if (!config) {
511
+ console.log(chalk.red('Config file not found'));
512
+ console.log(chalk.gray('Run: dbsafedump init'));
513
+ return;
514
+ }
515
+
516
+ if (!config.connections || !config.connections.source) {
517
+ console.log(chalk.red('Invalid config: missing source connection'));
518
+ return;
519
+ }
520
+
521
+ await checkAndShowLicense();
522
+ await runTestConnections(config);
523
+ } catch (error) {
524
+ console.error(chalk.red(`\nError: ${error.message}`));
525
+ process.exit(1);
526
+ }
527
+ });
528
+
529
+ program
530
+ .command('config')
531
+ .description('Configure database connections interactively')
532
+ .action(async () => {
533
+ const runConfig = require('../src/commands/config');
534
+ await runConfig();
535
+ });
536
+
537
+ program
538
+ .command('discover')
539
+ .description('Analyze database and suggest masking rules (STARTER/PRO)')
540
+ .option('-c, --config <file>', 'Config file', getDefaultConfigPath())
541
+ .option('-p, --profile <name>', 'Use named profile (STARTER/PRO)')
542
+ .option('-o, --output <file>', 'Save suggestions to file')
543
+ .option('-a, --apply', 'Apply suggestions to config file')
544
+ .action(async (options) => {
545
+ try {
546
+ const config = loadConfig(options.config, options.profile);
547
+
548
+ if (!config) {
549
+ console.log(chalk.red('Config file not found'));
550
+ console.log(chalk.gray('Run: dbsafedump init'));
551
+ return;
552
+ }
553
+
554
+ await checkAndShowLicense();
555
+
556
+ if (!canUseSmartDiscovery()) {
557
+ console.log(chalk.red('\nSmart Discovery requires STARTER or PRO license.'));
558
+ console.log(chalk.gray('Get your license at: https://dbsafedump.com/pricing\n'));
559
+ return;
560
+ }
561
+
562
+ if (!config.connections || !config.connections.source) {
563
+ console.log(chalk.red('Invalid config: missing source connection'));
564
+ return;
565
+ }
566
+
567
+ const tier = getTierInfo();
568
+ console.log(chalk.blue(`\nDBSafeDump - Smart Discovery`));
569
+ console.log(chalk.gray(`Running ${tier === 'pro' ? 'FULL' : 'BASIC'} analysis...\n`));
570
+
571
+ const sourceConfig = config.connections.source;
572
+ const driver = sourceConfig.driver || 'mysql';
573
+
574
+ const sourceTest = await require('../src/database').testConnection(sourceConfig);
575
+ if (!sourceTest.success) {
576
+ console.log(chalk.red(`Source connection failed!`));
577
+ console.log(chalk.red(` ${sourceTest.message}\n`));
578
+ return;
579
+ }
580
+ console.log(chalk.green(`Connected to ${driver}://${sourceConfig.host}/${sourceConfig.database}\n`));
581
+
582
+ const conn = await createConnection(sourceConfig);
583
+ const allTables = await getTables(conn, driver);
584
+ const { shouldProcessTable } = require('../src/commands/utils');
585
+ const tables = allTables.filter(t => shouldProcessTable(t, config));
586
+
587
+ console.log(chalk.gray(`Analyzing ${tables.length} tables...\n`));
588
+
589
+ const discoveryResults = await discoverSensitiveColumns(conn, driver, tables, config);
590
+
591
+ await closeConnection(conn, driver);
592
+
593
+ if (Object.keys(discoveryResults).length === 0) {
594
+ console.log(chalk.yellow('\nNo sensitive columns detected.\n'));
595
+ return;
596
+ }
597
+
598
+ console.log(chalk.green(`\nDiscovered ${Object.keys(discoveryResults).length} tables with sensitive data:\n`));
599
+
600
+ for (const [table, columns] of Object.entries(discoveryResults)) {
601
+ console.log(chalk.cyan(` ${table}:`));
602
+ for (const col of columns) {
603
+ const mask = col.suggestedMask || 'sensitive';
604
+ const confidence = col.confidence || 'unknown';
605
+ console.log(chalk.gray(` - ${col.column}: ${mask} (${confidence})`));
606
+ }
607
+ }
608
+
609
+ const suggestions = require('../src/smartDiscovery').generateConfigSuggestions(discoveryResults);
610
+
611
+ console.log(chalk.blue(`\nSuggested rules:\n`));
612
+ for (const [key, mask] of Object.entries(suggestions)) {
613
+ console.log(chalk.gray(` "${key}": ${mask}`));
614
+ }
615
+
616
+ if (!options.apply && !options.output) {
617
+ console.log(chalk.blue(`\nNext steps:\n`));
618
+ console.log(chalk.gray(` dbsafedump discover --apply # Apply rules to config.yml`));
619
+ console.log(chalk.gray(` dbsafedump discover --output rules.json # Save to file\n`));
620
+ }
621
+
622
+ if (options.apply) {
623
+ const { updateConfigField } = require('../src/config');
624
+ let applied = 0;
625
+ for (const [key, mask] of Object.entries(suggestions)) {
626
+ if (updateConfigField(options.config || getDefaultConfigPath(), key, mask)) {
627
+ applied++;
628
+ }
629
+ }
630
+ console.log(chalk.green(`\nApplied ${applied} rules to config.\n`));
631
+ }
632
+
633
+ if (options.output) {
634
+ const fs = require('fs');
635
+ fs.writeFileSync(options.output, JSON.stringify(suggestions, null, 2));
636
+ console.log(chalk.green(`Saved suggestions to: ${options.output}\n`));
637
+ }
638
+
639
+ } catch (error) {
640
+ console.error(chalk.red(`\nError: ${error.message}`));
641
+ console.error(chalk.gray(error.stack));
642
+ process.exit(1);
643
+ }
644
+ });
645
+
646
+ program
647
+ .command('scrub')
648
+ .description('Anonymize data in-place in database (PRO)')
649
+ .option('-c, --config <file>', 'Config file', getDefaultConfigPath())
650
+ .option('-p, --profile <name>', 'Use named profile (STARTER/PRO)')
651
+ .option('-y, --yes', 'Skip confirmation')
652
+ .option('--ci', 'CI/CD mode (PRO)')
653
+ .option('--force', 'Skip confirmation (for CI/CD)')
654
+ .action(async (options) => {
655
+ try {
656
+ const config = loadConfig(options.config, options.profile);
657
+
658
+ if (!config) {
659
+ console.log(chalk.red('Config file not found'));
660
+ console.log(chalk.gray('Run: dbsafedump init'));
661
+ return;
662
+ }
663
+
664
+ await checkAndShowLicense();
665
+
666
+ if (!canUseCI()) {
667
+ console.log(chalk.red('\nScrub requires PRO license.'));
668
+ console.log(chalk.gray('Get your license at: https://dbsafedump.com/pricing\n'));
669
+ return;
670
+ }
671
+
672
+ if (!config.connections || !config.connections.source) {
673
+ console.log(chalk.red('Invalid config: missing source connection'));
674
+ return;
675
+ }
676
+
677
+ const skipConfirm = options.ci || options.yes || options.force;
678
+ const dbConfig = config.connections.source;
679
+ const driver = dbConfig.driver || 'mysql';
680
+
681
+ if (!skipConfirm) {
682
+ console.log(chalk.yellow(`\nWARNING: This will MODIFY data in your database!`));
683
+ console.log(chalk.yellow(`This operation CANNOT be undone!\n`));
684
+ console.log(chalk.red(`Target database:`));
685
+ console.log(chalk.red(` ${driver}://${dbConfig.host}/${dbConfig.database}\n`));
686
+
687
+ const { confirm } = await inquirer.prompt([
688
+ {
689
+ type: 'confirm',
690
+ name: 'confirm',
691
+ message: 'Type "yes" to confirm the scrub operation:',
692
+ default: false
693
+ }
694
+ ]);
695
+
696
+ if (!confirm) {
697
+ console.log(chalk.gray('\nScrub cancelled.\n'));
698
+ return;
699
+ }
700
+ } else {
701
+ console.log(chalk.yellow(`\nWARNING: This will MODIFY data in your database!`));
702
+ console.log(chalk.yellow(`This operation CANNOT be undone!\n`));
703
+ console.log(chalk.red(`Target database:`));
704
+ console.log(chalk.red(` ${driver}://${dbConfig.host}/${dbConfig.database}\n`));
705
+ console.log(chalk.gray(`[${options.force ? 'FORCE' : 'CI MODE'}] Proceeding with scrub...\n`));
706
+ }
707
+
708
+ await runScrub(config, options);
709
+ } catch (error) {
710
+ console.error(chalk.red(`\nError: ${error.message}`));
711
+ process.exit(1);
712
+ }
713
+ });
714
+
715
+ program.parse(process.argv);
716
+
717
+ if (!process.argv.slice(2).length) {
718
+ program.outputHelp();
719
+ }