ante-erp-cli 1.6.12 → 1.6.14

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/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to the ANTE CLI will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ### Fixed
11
+ - **Misleading success messages in `clone-db` command** - Now shows accurate operation status
12
+ - Added result tracking for Prisma generate and migrate operations
13
+ - Summary now displays actual success/failure instead of always showing success
14
+ - Improved error messages: explains that migrations may not be needed after pg_restore
15
+ - Shows "⚠ Prisma migrations skipped (schema already exists from restore)" when appropriate
16
+ - **ESLint warnings** - Removed unused variables and imports
17
+ - Removed unused `join` import from docker.js
18
+ - Removed unused `needsSudo` variable from update-cli.js
19
+ - Removed unused `getInstallDir` import from doctor.js
20
+ - Added eslint-disable comments for intentionally unused parameters
21
+
8
22
  ## [1.6.0] - 2025-01-28
9
23
 
10
24
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ante-erp-cli",
3
- "version": "1.6.12",
3
+ "version": "1.6.14",
4
4
  "description": "Comprehensive CLI tool for managing ANTE ERP self-hosted installations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,10 @@ import {
9
9
  parsePostgresUrl,
10
10
  testConnection,
11
11
  dumpDatabase,
12
- restoreDatabase
12
+ restoreDatabase,
13
+ disableRLS,
14
+ enableRLS,
15
+ verifyDataRestored
13
16
  } from '../utils/postgres.js';
14
17
 
15
18
  /**
@@ -187,8 +190,20 @@ export async function cloneDb(sourceUrl, options = {}) {
187
190
  console.log('');
188
191
  }
189
192
 
193
+ // Step 5A: Disable RLS on target database (to allow data insertion)
194
+ console.log(chalk.yellow('Step 5/7: Preparing target database...'));
195
+ const rlsDisableSpinner = ora('Disabling Row-Level Security policies...').start();
196
+ const rlsDisableResult = await disableRLS(localInfo, composeFile);
197
+
198
+ if (!rlsDisableResult.success) {
199
+ rlsDisableSpinner.warn(chalk.yellow('Could not disable RLS (this is normal for non-Supabase databases)'));
200
+ } else {
201
+ rlsDisableSpinner.succeed(chalk.green(`RLS disabled on ${rlsDisableResult.tablesAffected} tables`));
202
+ }
203
+ console.log('');
204
+
190
205
  // Step 6: Restore to local database
191
- console.log(chalk.yellow('Step 5/6: Restoring to local database...'));
206
+ console.log(chalk.yellow('Step 6/7: Restoring to local database...'));
192
207
  console.log(chalk.gray('This may take several minutes...'));
193
208
  console.log('');
194
209
 
@@ -203,9 +218,46 @@ export async function cloneDb(sourceUrl, options = {}) {
203
218
  restoreSpinner.succeed(chalk.green('Database restore completed'));
204
219
  console.log('');
205
220
 
221
+ // Step 6A: Verify data was restored
222
+ const verifySpinner = ora('Verifying data restoration...').start();
223
+ const verifyResult = await verifyDataRestored(localInfo, composeFile);
224
+
225
+ if (!verifyResult.success) {
226
+ verifySpinner.fail(chalk.red('Data verification failed'));
227
+ console.log(chalk.yellow(' Warning: Could not verify data restoration'));
228
+ } else {
229
+ verifySpinner.succeed(chalk.green(`Verified: ${verifyResult.tableCount} tables, ${verifyResult.totalRows.toLocaleString()} rows restored`));
230
+
231
+ // Warn if no data was restored
232
+ if (verifyResult.totalRows === 0) {
233
+ console.log(chalk.red('\n⚠️ WARNING: No data was restored!'));
234
+ console.log(chalk.yellow(' Possible causes:'));
235
+ console.log(chalk.gray(' - Source database was empty'));
236
+ console.log(chalk.gray(' - RLS policies blocked data insertion'));
237
+ console.log(chalk.gray(' - Permission issues prevented data restore'));
238
+ console.log(chalk.gray(' - Data restore errors were silently ignored\n'));
239
+ }
240
+ }
241
+ console.log('');
242
+
243
+ // Step 6B: Re-enable RLS on target database
244
+ const rlsEnableSpinner = ora('Re-enabling Row-Level Security policies...').start();
245
+ const rlsEnableResult = await enableRLS(localInfo, composeFile);
246
+
247
+ if (!rlsEnableResult.success) {
248
+ rlsEnableSpinner.warn(chalk.yellow('Could not re-enable RLS'));
249
+ } else {
250
+ rlsEnableSpinner.succeed(chalk.green(`RLS re-enabled on ${rlsEnableResult.tablesAffected} tables`));
251
+ }
252
+ console.log('');
253
+
254
+ // Track Prisma operation results
255
+ let generateSuccess = false;
256
+ let migrateSuccess = false;
257
+
206
258
  // Step 7: Run Prisma operations (if not skipped)
207
259
  if (!options.noPrisma) {
208
- console.log(chalk.yellow('Step 6/6: Running Prisma operations...'));
260
+ console.log(chalk.yellow('Step 7/7: Running Prisma operations...'));
209
261
 
210
262
  // Generate Prisma client
211
263
  const generateSpinner = ora('Generating Prisma client...').start();
@@ -216,9 +268,10 @@ export async function cloneDb(sourceUrl, options = {}) {
216
268
  ['npx', 'prisma', 'generate']
217
269
  );
218
270
  generateSpinner.succeed(chalk.green('Prisma client generated'));
271
+ generateSuccess = true;
219
272
  } catch (error) {
220
273
  generateSpinner.fail(chalk.red('Prisma generate failed'));
221
- console.log(chalk.yellow(' Warning: You may need to run "ante db migrate" manually'));
274
+ console.log(chalk.yellow(' Warning: You may need to run "npx prisma generate" manually in the backend container'));
222
275
  }
223
276
 
224
277
  // Run Prisma migrations
@@ -230,9 +283,10 @@ export async function cloneDb(sourceUrl, options = {}) {
230
283
  ['npx', 'prisma', 'migrate', 'deploy']
231
284
  );
232
285
  migrateSpinner.succeed(chalk.green('Prisma migrations completed'));
286
+ migrateSuccess = true;
233
287
  } catch (error) {
234
288
  migrateSpinner.fail(chalk.red('Prisma migrate failed'));
235
- console.log(chalk.yellow(' Warning: You may need to run "ante db migrate" manually'));
289
+ console.log(chalk.yellow(' Note: Database schema already exists from restore. Migrations may not be needed.'));
236
290
  }
237
291
  console.log('');
238
292
  }
@@ -245,10 +299,32 @@ export async function cloneDb(sourceUrl, options = {}) {
245
299
  console.log(chalk.blue('Summary:'));
246
300
  console.log(chalk.gray(` ✓ Source database dumped to: ${dumpFile}`));
247
301
  console.log(chalk.gray(` ✓ Restored to local database: ${localInfo.database}`));
302
+
303
+ // Show data restoration summary
304
+ if (verifyResult && verifyResult.success) {
305
+ if (verifyResult.totalRows > 0) {
306
+ console.log(chalk.green(` ✓ Data restored: ${verifyResult.totalRows.toLocaleString()} rows across ${verifyResult.tableCount} tables`));
307
+ } else {
308
+ console.log(chalk.red(` ✗ No data restored (${verifyResult.tableCount} tables created but empty)`));
309
+ }
310
+ }
311
+
248
312
  if (!options.noPrisma) {
249
- console.log(chalk.gray(' ✓ Prisma client generated'));
250
- console.log(chalk.gray(' ✓ Prisma migrations applied'));
313
+ // Show Prisma generate status
314
+ if (generateSuccess) {
315
+ console.log(chalk.gray(' ✓ Prisma client generated'));
316
+ } else {
317
+ console.log(chalk.yellow(' ⚠ Prisma client generation failed - run manually if needed'));
318
+ }
319
+
320
+ // Show Prisma migrate status
321
+ if (migrateSuccess) {
322
+ console.log(chalk.gray(' ✓ Prisma migrations applied'));
323
+ } else {
324
+ console.log(chalk.yellow(' ⚠ Prisma migrations skipped (schema already exists from restore)'));
325
+ }
251
326
  }
327
+
252
328
  console.log('');
253
329
  console.log(chalk.cyan('Your local database is now cloned from the source!'));
254
330
  console.log(chalk.gray('You can start your services with: ante start'));
@@ -87,7 +87,7 @@ export async function testConnection(connectionInfo, composeFile = null) {
87
87
  }
88
88
 
89
89
  dockerArgs.push(
90
- 'postgres:17-alpine',
90
+ 'postgres:15-alpine',
91
91
  'psql',
92
92
  '-h', testHost,
93
93
  '-p', port,
@@ -129,13 +129,13 @@ export async function dumpDatabase(connectionInfo, outputFile) {
129
129
  // Convert to absolute path for Docker volume mount
130
130
  const absoluteBackupDir = resolve(backupDir);
131
131
 
132
- // Use Docker with PostgreSQL 17 to avoid version mismatch
132
+ // Use Docker with PostgreSQL 15 to match ANTE installation
133
133
  await execa('docker', [
134
134
  'run',
135
135
  '--rm',
136
136
  '-e', `PGPASSWORD=${password}`,
137
137
  '-v', `${absoluteBackupDir}:/backups`,
138
- 'postgres:17-alpine',
138
+ 'postgres:15-alpine',
139
139
  'pg_dump',
140
140
  '-h', host,
141
141
  '-p', port,
@@ -227,8 +227,9 @@ export async function restoreDatabase(dumpFile, connectionInfo, dropSchema = tru
227
227
  });
228
228
 
229
229
  // Restore from inside the container
230
+ let restoreResult;
230
231
  try {
231
- await execa('docker', [
232
+ restoreResult = await execa('docker', [
232
233
  'compose',
233
234
  '-f', composeFile,
234
235
  'exec',
@@ -244,8 +245,24 @@ export async function restoreDatabase(dumpFile, connectionInfo, dropSchema = tru
244
245
  '/tmp/' + dumpFileName
245
246
  ], {
246
247
  timeout: 600000, // 10 minute timeout
247
- reject: false // Don't reject on non-zero exit (pg_restore often exits with warnings)
248
+ reject: false, // Don't reject on non-zero exit (pg_restore often exits with warnings)
249
+ all: true // Capture both stdout and stderr
248
250
  });
251
+
252
+ // Check for critical errors in output
253
+ const output = (restoreResult.stdout || '') + (restoreResult.stderr || '');
254
+ if (output.includes('FATAL:') || output.includes('ERROR:')) {
255
+ console.error('\n⚠️ pg_restore reported errors:');
256
+ console.error(output);
257
+
258
+ // Check for specific error patterns that indicate data restoration failed
259
+ if (output.includes('permission denied') ||
260
+ output.includes('must be owner') ||
261
+ output.includes('RLS policy') ||
262
+ output.includes('no data was returned by command')) {
263
+ throw new Error('Data restoration may have failed due to permission or RLS policy issues. Check output above.');
264
+ }
265
+ }
249
266
  } finally {
250
267
  // Clean up: Remove dump file from container
251
268
  try {
@@ -283,7 +300,7 @@ export async function restoreDatabase(dumpFile, connectionInfo, dropSchema = tru
283
300
  }
284
301
 
285
302
  dockerArgs.push(
286
- 'postgres:17-alpine',
303
+ 'postgres:15-alpine',
287
304
  'psql',
288
305
  '-h', psqlHost,
289
306
  '-p', port,
@@ -310,7 +327,7 @@ export async function restoreDatabase(dumpFile, connectionInfo, dropSchema = tru
310
327
  }
311
328
 
312
329
  restoreArgs.push(
313
- 'postgres:17-alpine',
330
+ 'postgres:15-alpine',
314
331
  'pg_restore',
315
332
  '-h', restoreHost,
316
333
  '-p', port,
@@ -322,11 +339,28 @@ export async function restoreDatabase(dumpFile, connectionInfo, dropSchema = tru
322
339
  `/backups/${join('/', dumpFileName)}`
323
340
  );
324
341
 
342
+ let restoreResult;
325
343
  try {
326
- await execa('docker', restoreArgs, {
344
+ restoreResult = await execa('docker', restoreArgs, {
327
345
  timeout: 600000, // 10 minute timeout
328
- reject: false // Don't reject on non-zero exit (pg_restore often exits with warnings)
346
+ reject: false, // Don't reject on non-zero exit (pg_restore often exits with warnings)
347
+ all: true // Capture both stdout and stderr
329
348
  });
349
+
350
+ // Check for critical errors in output
351
+ const output = (restoreResult.stdout || '') + (restoreResult.stderr || '');
352
+ if (output.includes('FATAL:') || output.includes('ERROR:')) {
353
+ console.error('\n⚠️ pg_restore reported errors:');
354
+ console.error(output);
355
+
356
+ // Check for specific error patterns that indicate data restoration failed
357
+ if (output.includes('permission denied') ||
358
+ output.includes('must be owner') ||
359
+ output.includes('RLS policy') ||
360
+ output.includes('no data was returned by command')) {
361
+ throw new Error('Data restoration may have failed due to permission or RLS policy issues. Check output above.');
362
+ }
363
+ }
330
364
  } catch (error) {
331
365
  // pg_restore might return non-zero even on successful restore due to warnings
332
366
  // We'll consider it successful if no critical error occurred
@@ -385,3 +419,292 @@ export async function runPrismaMigrate(backendDir) {
385
419
  };
386
420
  }
387
421
  }
422
+
423
+ /**
424
+ * Disable Row-Level Security on all tables (for Supabase databases)
425
+ * @param {Object} connectionInfo - Connection details
426
+ * @param {string} composeFile - Optional path to docker-compose.yml
427
+ * @returns {Promise<{success: boolean, tablesAffected: number, error?: string}>}
428
+ */
429
+ export async function disableRLS(connectionInfo, composeFile = null) {
430
+ try {
431
+ const { user, password, database, schema } = connectionInfo;
432
+
433
+ const query = `
434
+ DO $$
435
+ DECLARE
436
+ r RECORD;
437
+ count INTEGER := 0;
438
+ BEGIN
439
+ FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = '${schema}')
440
+ LOOP
441
+ EXECUTE 'ALTER TABLE ' || quote_ident('${schema}') || '.' || quote_ident(r.tablename) || ' DISABLE ROW LEVEL SECURITY';
442
+ count := count + 1;
443
+ END LOOP;
444
+ RAISE NOTICE 'Disabled RLS on % tables', count;
445
+ END $$;
446
+ `;
447
+
448
+ let result;
449
+ if (composeFile) {
450
+ result = await execa('docker', [
451
+ 'compose',
452
+ '-f', composeFile,
453
+ 'exec',
454
+ '-T',
455
+ '-e', `PGPASSWORD=${password}`,
456
+ 'postgres',
457
+ 'psql',
458
+ '-U', user,
459
+ '-d', database,
460
+ '-c', query
461
+ ], {
462
+ timeout: 30000,
463
+ all: true
464
+ });
465
+ } else {
466
+ const { host, port } = connectionInfo;
467
+ let psqlHost = host;
468
+ const dockerArgs = ['run', '--rm', '-e', `PGPASSWORD=${password}`];
469
+
470
+ if (host === 'localhost' || host === '127.0.0.1') {
471
+ psqlHost = 'host.docker.internal';
472
+ dockerArgs.push('--add-host=host.docker.internal:host-gateway');
473
+ }
474
+
475
+ dockerArgs.push(
476
+ 'postgres:15-alpine',
477
+ 'psql',
478
+ '-h', psqlHost,
479
+ '-p', port,
480
+ '-U', user,
481
+ '-d', database,
482
+ '-c', query
483
+ );
484
+
485
+ result = await execa('docker', dockerArgs, {
486
+ timeout: 30000,
487
+ all: true
488
+ });
489
+ }
490
+
491
+ // Extract table count from NOTICE message
492
+ const noticeMatch = result.all?.match(/Disabled RLS on (\d+) tables/);
493
+ const tablesAffected = noticeMatch ? parseInt(noticeMatch[1]) : 0;
494
+
495
+ return {
496
+ success: true,
497
+ tablesAffected
498
+ };
499
+ } catch (error) {
500
+ return {
501
+ success: false,
502
+ tablesAffected: 0,
503
+ error: error.message || 'Failed to disable RLS'
504
+ };
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Enable Row-Level Security on all tables (for Supabase databases)
510
+ * @param {Object} connectionInfo - Connection details
511
+ * @param {string} composeFile - Optional path to docker-compose.yml
512
+ * @returns {Promise<{success: boolean, tablesAffected: number, error?: string}>}
513
+ */
514
+ export async function enableRLS(connectionInfo, composeFile = null) {
515
+ try {
516
+ const { user, password, database, schema } = connectionInfo;
517
+
518
+ const query = `
519
+ DO $$
520
+ DECLARE
521
+ r RECORD;
522
+ count INTEGER := 0;
523
+ BEGIN
524
+ FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = '${schema}')
525
+ LOOP
526
+ EXECUTE 'ALTER TABLE ' || quote_ident('${schema}') || '.' || quote_ident(r.tablename) || ' ENABLE ROW LEVEL SECURITY';
527
+ count := count + 1;
528
+ END LOOP;
529
+ RAISE NOTICE 'Enabled RLS on % tables', count;
530
+ END $$;
531
+ `;
532
+
533
+ let result;
534
+ if (composeFile) {
535
+ result = await execa('docker', [
536
+ 'compose',
537
+ '-f', composeFile,
538
+ 'exec',
539
+ '-T',
540
+ '-e', `PGPASSWORD=${password}`,
541
+ 'postgres',
542
+ 'psql',
543
+ '-U', user,
544
+ '-d', database,
545
+ '-c', query
546
+ ], {
547
+ timeout: 30000,
548
+ all: true
549
+ });
550
+ } else {
551
+ const { host, port } = connectionInfo;
552
+ let psqlHost = host;
553
+ const dockerArgs = ['run', '--rm', '-e', `PGPASSWORD=${password}`];
554
+
555
+ if (host === 'localhost' || host === '127.0.0.1') {
556
+ psqlHost = 'host.docker.internal';
557
+ dockerArgs.push('--add-host=host.docker.internal:host-gateway');
558
+ }
559
+
560
+ dockerArgs.push(
561
+ 'postgres:15-alpine',
562
+ 'psql',
563
+ '-h', psqlHost,
564
+ '-p', port,
565
+ '-U', user,
566
+ '-d', database,
567
+ '-c', query
568
+ );
569
+
570
+ result = await execa('docker', dockerArgs, {
571
+ timeout: 30000,
572
+ all: true
573
+ });
574
+ }
575
+
576
+ // Extract table count from NOTICE message
577
+ const noticeMatch = result.all?.match(/Enabled RLS on (\d+) tables/);
578
+ const tablesAffected = noticeMatch ? parseInt(noticeMatch[1]) : 0;
579
+
580
+ return {
581
+ success: true,
582
+ tablesAffected
583
+ };
584
+ } catch (error) {
585
+ return {
586
+ success: false,
587
+ tablesAffected: 0,
588
+ error: error.message || 'Failed to enable RLS'
589
+ };
590
+ }
591
+ }
592
+
593
+ /**
594
+ * Verify data was restored by counting rows in all tables
595
+ * @param {Object} connectionInfo - Connection details
596
+ * @param {string} composeFile - Optional path to docker-compose.yml
597
+ * @returns {Promise<{success: boolean, tableCount: number, totalRows: number, error?: string}>}
598
+ */
599
+ export async function verifyDataRestored(connectionInfo, composeFile = null) {
600
+ try {
601
+ const { user, password, database, schema } = connectionInfo;
602
+
603
+ // Count tables
604
+ const tableCountQuery = `SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '${schema}'`;
605
+
606
+ // Count total rows across all tables (excluding _prisma_migrations)
607
+ const rowCountQuery = `
608
+ SELECT COALESCE(SUM(n_live_tup), 0)::bigint as total_rows
609
+ FROM pg_stat_user_tables
610
+ WHERE schemaname = '${schema}' AND relname != '_prisma_migrations'
611
+ `;
612
+
613
+ let tableCountResult, rowCountResult;
614
+
615
+ if (composeFile) {
616
+ // Get table count
617
+ tableCountResult = await execa('docker', [
618
+ 'compose',
619
+ '-f', composeFile,
620
+ 'exec',
621
+ '-T',
622
+ '-e', `PGPASSWORD=${password}`,
623
+ 'postgres',
624
+ 'psql',
625
+ '-U', user,
626
+ '-d', database,
627
+ '-t', // Tuples only (no headers)
628
+ '-c', tableCountQuery
629
+ ], {
630
+ timeout: 10000
631
+ });
632
+
633
+ // Get row count
634
+ rowCountResult = await execa('docker', [
635
+ 'compose',
636
+ '-f', composeFile,
637
+ 'exec',
638
+ '-T',
639
+ '-e', `PGPASSWORD=${password}`,
640
+ 'postgres',
641
+ 'psql',
642
+ '-U', user,
643
+ '-d', database,
644
+ '-t',
645
+ '-c', rowCountQuery
646
+ ], {
647
+ timeout: 10000
648
+ });
649
+ } else {
650
+ const { host, port } = connectionInfo;
651
+ let psqlHost = host;
652
+ const dockerArgs = ['run', '--rm', '-e', `PGPASSWORD=${password}`];
653
+
654
+ if (host === 'localhost' || host === '127.0.0.1') {
655
+ psqlHost = 'host.docker.internal';
656
+ dockerArgs.push('--add-host=host.docker.internal:host-gateway');
657
+ }
658
+
659
+ // Get table count
660
+ const tableCountArgs = [...dockerArgs];
661
+ tableCountArgs.push(
662
+ 'postgres:15-alpine',
663
+ 'psql',
664
+ '-h', psqlHost,
665
+ '-p', port,
666
+ '-U', user,
667
+ '-d', database,
668
+ '-t',
669
+ '-c', tableCountQuery
670
+ );
671
+
672
+ tableCountResult = await execa('docker', tableCountArgs, {
673
+ timeout: 10000
674
+ });
675
+
676
+ // Get row count
677
+ const rowCountArgs = [...dockerArgs];
678
+ rowCountArgs.push(
679
+ 'postgres:15-alpine',
680
+ 'psql',
681
+ '-h', psqlHost,
682
+ '-p', port,
683
+ '-U', user,
684
+ '-d', database,
685
+ '-t',
686
+ '-c', rowCountQuery
687
+ );
688
+
689
+ rowCountResult = await execa('docker', rowCountArgs, {
690
+ timeout: 10000
691
+ });
692
+ }
693
+
694
+ const tableCount = parseInt(tableCountResult.stdout.trim());
695
+ const totalRows = parseInt(rowCountResult.stdout.trim() || '0');
696
+
697
+ return {
698
+ success: true,
699
+ tableCount,
700
+ totalRows
701
+ };
702
+ } catch (error) {
703
+ return {
704
+ success: false,
705
+ tableCount: 0,
706
+ totalRows: 0,
707
+ error: error.message || 'Failed to verify data'
708
+ };
709
+ }
710
+ }