wanas-zcrm-extractor 1.6.0 β†’ 1.6.2

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/README.md CHANGED
@@ -420,7 +420,9 @@ metadata/
420
420
 
421
421
  ## 🩺 Troubleshooting
422
422
 
423
- Every command writes a timestamped **debug log** to `~/.zcrm/logs/` (e.g. `pull-<timestamp>.log`, `audit-<timestamp>.log`) capturing all activity for that run β€” including the quiet "skipped" lines and the **full API error bodies** that aren't printed to the console. When a command fails, its log path is shown in the output. `zcrm pull` additionally writes a copy into its output folder at `<output>/.zcrm/.logs/`.
423
+ Errors are reported in a single, consistent format: **expected** problems (e.g. not logged in, bad input) show a clean message with an actionable hint, while **unexpected** errors show the real underlying reason plus how to reach support β€” so you're never left with a raw stack trace.
424
+
425
+ Every command also writes a timestamped **debug log** to `~/.zcrm/logs/` (e.g. `pull-<timestamp>.log`, `audit-<timestamp>.log`) capturing all activity for that run β€” including the quiet "skipped" lines and the **full API error bodies** that aren't printed to the console. When a command fails, its log path is shown in the output. `zcrm pull` additionally writes a copy into its output folder at `<output>/.zcrm/.logs/`.
424
426
 
425
427
  ---
426
428
 
package/bin/cli.js CHANGED
@@ -40,9 +40,33 @@ let debugLogPath = null;
40
40
  }
41
41
  })();
42
42
 
43
- // Point the user at the debug log when a command exits with an error.
43
+ const { CliError, reportError } = require('../src/utils/errorReporter');
44
+
45
+ let errorReported = false;
46
+ /**
47
+ * Unified fatal-error handler: render one consistent error response (with a
48
+ * "contact support" block for unexpected errors), then exit non-zero.
49
+ * @param {Error|object|string} error
50
+ * @param {string} context
51
+ */
52
+ function fail(error, context) {
53
+ errorReported = true;
54
+ reportError(error, { context, debugLogPath });
55
+ process.exit(1);
56
+ }
57
+
58
+ // Anything that escapes a command handler is funnelled through the same unified
59
+ // response, so the user never sees a raw stack trace β€” they always get a clean
60
+ // message and a way to contact support.
61
+ process.on('uncaughtException', (err) => fail(err, 'uncaughtException'));
62
+ process.on('unhandledRejection', (reason) => {
63
+ fail(reason instanceof Error ? reason : new Error(`Unhandled rejection: ${reason}`), 'unhandledRejection');
64
+ });
65
+
66
+ // Fallback: if something exits non-zero without going through fail(), still
67
+ // point the user at the debug log.
44
68
  process.on('exit', (code) => {
45
- if (code && debugLogPath) {
69
+ if (code && !errorReported && debugLogPath) {
46
70
  try { process.stderr.write(`\x1b[90mβ„Ή Debug log: ${debugLogPath}\x1b[0m\n`); } catch (e) { /* ignore */ }
47
71
  }
48
72
  });
@@ -88,10 +112,13 @@ const getBanner = () => {
88
112
  try {
89
113
  const fs = require('fs');
90
114
  const bannerPath = path.join(__dirname, '../banner.txt');
115
+ const version = require('../package.json').version;
91
116
  if (fs.existsSync(bannerPath)) {
92
117
  const banner = fs.readFileSync(bannerPath, 'utf8');
93
- return `\x1b[34m${banner}\x1b[0m\n`;
118
+ return `\x1b[34m${banner}\x1b[0m\n\x1b[1;36m zcrm v${version}\x1b[0m\n`;
94
119
  }
120
+ // Banner art missing β€” still show the version line.
121
+ return `\x1b[1;36m zcrm v${version}\x1b[0m\n`;
95
122
  } catch (err) {
96
123
  // Fail silently
97
124
  }
@@ -315,8 +342,7 @@ program
315
342
  console.log('\x1b[1;32m====================================================================\x1b[0m\n');
316
343
  process.exit(0);
317
344
  } catch (err) {
318
- console.error('\n❌ Login error:', err.message);
319
- process.exit(1);
345
+ fail(err, 'login');
320
346
  }
321
347
  });
322
348
 
@@ -339,8 +365,7 @@ program
339
365
  authService.loadTokens();
340
366
 
341
367
  if (!authService.isAuthenticated()) {
342
- console.error('\n❌ Error: Not authenticated. Please login first using:\n zcrm login');
343
- process.exit(1);
368
+ fail(new CliError('Not authenticated. Please log in first.', 'Run: zcrm login'), 'auth');
344
369
  }
345
370
 
346
371
  try {
@@ -374,8 +399,7 @@ program
374
399
 
375
400
  process.exit(0);
376
401
  } catch (err) {
377
- console.error(`\n❌ Failed to query session info: ${err.message}`);
378
- process.exit(1);
402
+ fail(err, 'whoami');
379
403
  }
380
404
  });
381
405
 
@@ -455,8 +479,7 @@ program
455
479
  authService.loadTokens();
456
480
 
457
481
  if (!authService.isAuthenticated()) {
458
- console.error('\n❌ Error: Not authenticated. Please login first using:\n zcrm login');
459
- process.exit(1);
482
+ fail(new CliError('Not authenticated. Please log in first.', 'Run: zcrm login'), 'auth');
460
483
  }
461
484
 
462
485
  const outputDir = path.resolve(options.output);
@@ -536,10 +559,11 @@ program
536
559
  }
537
560
  break;
538
561
  case 'FAILURE':
562
+ // Clean up an in-progress bar line; the unified handler (catch
563
+ // below) renders the actual error.
539
564
  if (inModulesPhase || inFunctionsPhase) {
540
565
  process.stdout.write('\n');
541
566
  }
542
- console.error(`\n❌ Error: ${message}`);
543
567
  break;
544
568
  case 'COMPLETE':
545
569
  console.log('\n\x1b[1;32m====================================================================\x1b[0m');
@@ -575,8 +599,7 @@ program
575
599
 
576
600
  process.exit(0);
577
601
  } catch (error) {
578
- console.error(`\n❌ Metadata pull failed: ${error.message}`);
579
- process.exit(1);
602
+ fail(error, 'pull');
580
603
  }
581
604
  });
582
605
 
@@ -597,8 +620,7 @@ program
597
620
  // Ensure user is authenticated
598
621
  authService.loadTokens();
599
622
  if (!authService.isAuthenticated()) {
600
- console.error('\n❌ You are not logged in. Please run `zcrm login` first.');
601
- process.exit(1);
623
+ fail(new CliError('You are not logged in.', 'Run: zcrm login'), 'audit');
602
624
  }
603
625
 
604
626
  try {
@@ -611,15 +633,14 @@ program
611
633
  } catch (error) {
612
634
  const apiMsg = shortErrorMessage(error);
613
635
  const isScope = error.response?.data?.code === 'OAUTH_SCOPE_MISMATCH' || /scope/i.test(apiMsg || '');
614
- console.error(`\n❌ Audit log export failed: ${apiMsg}`);
615
636
  if (isScope) {
616
- console.error('\n\x1b[33mThis needs the audit-log scopes \x1b[1mZohoCRM.settings.audit_logs.CREATE\x1b[0m\x1b[33m and');
617
- console.error('\x1b[1mZohoCRM.settings.audit_logs.READ\x1b[0m\x1b[33m, which your current session was not granted.');
618
- console.error('Re-authenticate to add them:\x1b[0m \x1b[36mzcrm logout\x1b[0m then \x1b[36mzcrm login\x1b[0m');
619
- console.error('\x1b[90m(If the download step then reports a file-scope error, add ZohoFiles.files.READ');
620
- console.error('to ZOHO_SCOPES and log in again.)\x1b[0m');
637
+ fail(new CliError(
638
+ `Audit log export failed: ${apiMsg}`,
639
+ 'Needs scopes ZohoCRM.settings.audit_logs.CREATE & .READ β€” re-authenticate: zcrm logout then zcrm login.\n (If the download step then reports a file-scope error, add ZohoFiles.files.READ to ZOHO_SCOPES and log in again.)'
640
+ ), 'audit');
641
+ } else {
642
+ fail(error, 'audit');
621
643
  }
622
- process.exit(1);
623
644
  }
624
645
  });
625
646
 
@@ -636,13 +657,11 @@ program
636
657
  console.log('\n\x1b[36mπŸ“˜ Wanas Extractor - LLM System Manual:\x1b[0m\n');
637
658
  console.log(content);
638
659
  } else {
639
- console.error('\n❌ Error: llm.md file not found in package directory.');
640
- process.exit(1);
660
+ fail(new Error('llm.md file not found in package directory.'), 'llm');
641
661
  }
642
662
  process.exit(0);
643
663
  } catch (err) {
644
- console.error('\n❌ Error reading llm.md:', err.message);
645
- process.exit(1);
664
+ fail(err, 'llm');
646
665
  }
647
666
  });
648
667
 
@@ -673,9 +692,7 @@ program
673
692
  }
674
693
 
675
694
  if (!skillGenerator.TARGETS[ide]) {
676
- console.error(`\n❌ Unknown IDE/AI target: "${ide}".`);
677
- console.error(` Valid values: ${Object.keys(skillGenerator.TARGETS).join(', ')}`);
678
- process.exit(1);
695
+ fail(new CliError(`Unknown IDE/AI target: "${ide}".`, `Valid values: ${Object.keys(skillGenerator.TARGETS).join(', ')}`), 'skill');
679
696
  }
680
697
 
681
698
  const projectDir = path.resolve(options.dir);
@@ -731,8 +748,7 @@ program
731
748
  console.log('\x1b[1;32m====================================================================\x1b[0m\n');
732
749
  process.exit(0);
733
750
  } catch (err) {
734
- console.error(`\n❌ Skill generation failed: ${err.message}`);
735
- process.exit(1);
751
+ fail(err, 'skill');
736
752
  }
737
753
  });
738
754
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wanas-zcrm-extractor",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "Local Zoho CRM V8 Metadata Extractor CLI and Web Dashboard Utility",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -8,7 +8,9 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node server.js",
11
- "dev": "nodemon server.js"
11
+ "dev": "nodemon server.js",
12
+ "test": "node test/run.js",
13
+ "prepublishOnly": "node test/run.js"
12
14
  },
13
15
  "files": [
14
16
  "bin",
@@ -11,11 +11,13 @@ async function delay(ms) {
11
11
 
12
12
  async function exportAuditLog(filterFile) {
13
13
  try {
14
- // For a full export the request body is OPTIONAL β€” and sending an object
15
- // without a non-empty `criteria` is rejected by Zoho (400). So the default
16
- // (no filter) sends NO body, and a filter file (which must contain a valid
17
- // `audit_log_export` criteria object) overrides it when supplied.
18
- let requestBody = null;
14
+ // For a full export the request body is OPTIONAL. Sending an object WITH an
15
+ // empty `criteria` is rejected (400), and sending NO body trips a 415
16
+ // (missing content type). So the default (no filter) sends an empty JSON
17
+ // object `{}` β€” which carries the JSON content type and contains no
18
+ // criteria, i.e. "export everything". A filter file (which must contain a
19
+ // valid `audit_log_export` criteria object) overrides it when supplied.
20
+ let requestBody = {};
19
21
 
20
22
  if (filterFile) {
21
23
  const filterPath = path.resolve(process.cwd(), filterFile);
@@ -73,15 +73,14 @@ async function request(method, endpoint, params = {}, data = null, retryCount =
73
73
 
74
74
  logInfo(`${method.toUpperCase()} request to ${url} (Retry: ${retryCount})`, 'apiClient.request');
75
75
 
76
- const response = await apiClient({
77
- method,
78
- url,
79
- params,
80
- data,
81
- headers: {
82
- 'Authorization': `Zoho-oauthtoken ${accessToken}`
83
- }
84
- });
76
+ const headers = { 'Authorization': `Zoho-oauthtoken ${accessToken}` };
77
+ // Zoho requires an explicit JSON content type on write requests β€” even when
78
+ // the body is empty β€” otherwise it responds 415 MEDIA_TYPE_NOT_SUPPORTED.
79
+ if (method && method.toLowerCase() !== 'get') {
80
+ headers['Content-Type'] = 'application/json';
81
+ }
82
+
83
+ const response = await apiClient({ method, url, params, data, headers });
85
84
 
86
85
  // Zoho API sometimes returns error statuses inside a 200 OK response, e.g. code: 'INVALID_TOKEN'
87
86
  if (response.data && response.data.status === 'error') {
@@ -0,0 +1,81 @@
1
+ const { logError, shortErrorMessage } = require('./logger');
2
+
3
+ const SUPPORT_EMAIL = 'support@wanasapps.com';
4
+ const SUPPORT_SITE = 'https://www.wanasapps.com';
5
+ const ISSUES_URL = 'https://github.com/Wanas-Apps/Module-fields-metadata-extractor/issues';
6
+
7
+ const C = {
8
+ reset: '\x1b[0m',
9
+ red: '\x1b[1;31m',
10
+ redPlain: '\x1b[31m',
11
+ yellow: '\x1b[33m',
12
+ cyan: '\x1b[36m',
13
+ blue: '\x1b[34m',
14
+ dim: '\x1b[90m',
15
+ bold: '\x1b[1m'
16
+ };
17
+ const BAR = `${C.red}====================================================================${C.reset}`;
18
+
19
+ /**
20
+ * A known, expected CLI failure (bad input, not authenticated, missing file…).
21
+ * These are shown cleanly with an optional actionable hint β€” and intentionally
22
+ * WITHOUT the "report a bug / contact support" footer, which is reserved for
23
+ * genuinely unexpected errors.
24
+ */
25
+ class CliError extends Error {
26
+ /**
27
+ * @param {string} message - User-facing message.
28
+ * @param {string} [hint] - Optional next-step hint.
29
+ */
30
+ constructor(message, hint) {
31
+ super(message);
32
+ this.name = 'CliError';
33
+ this.isCliError = true;
34
+ this.hint = hint;
35
+ }
36
+ }
37
+
38
+ const write = (s) => { try { process.stderr.write(s); } catch (e) { /* ignore */ } };
39
+
40
+ /**
41
+ * Renders a single, unified error response for the CLI and records full details
42
+ * to the log files. Expected errors (CliError) are concise + actionable;
43
+ * everything else is treated as unexpected and gets a clear "contact support"
44
+ * block so the user always knows how to get help.
45
+ *
46
+ * @param {Error|object|string} error
47
+ * @param {object} [opts]
48
+ * @param {string} [opts.context] - Where the error came from (for the log).
49
+ * @param {string|null} [opts.debugLogPath] - Per-run debug log to reference.
50
+ */
51
+ function reportError(error, opts = {}) {
52
+ const { context = 'cli', debugLogPath = null } = opts;
53
+
54
+ // Always capture the full error (incl. API response bodies) to the log files.
55
+ try { logError(error, context); } catch (e) { /* never let logging break reporting */ }
56
+
57
+ if (error && error.isCliError) {
58
+ write(`\n${C.red}❌ ${error.message}${C.reset}\n`);
59
+ if (error.hint) write(`${C.yellow} ${error.hint}${C.reset}\n`);
60
+ if (debugLogPath) write(`${C.dim} Debug log: ${debugLogPath}${C.reset}\n`);
61
+ return;
62
+ }
63
+
64
+ // Unexpected error β†’ unified block + how to get help.
65
+ const message = shortErrorMessage(error);
66
+ write(`\n${BAR}\n`);
67
+ write(` ${C.red}❌ Unexpected Error${C.reset}\n`);
68
+ write(`${BAR}\n`);
69
+ write(` ${C.redPlain}${message}${C.reset}\n\n`);
70
+ write(` ${C.bold}This shouldn't have happened β€” sorry about that.${C.reset}\n`);
71
+ write(` Please report it and we'll get it fixed:\n`);
72
+ write(` βœ‰ ${C.cyan}${SUPPORT_EMAIL}${C.reset}\n`);
73
+ write(` 🌐 ${C.cyan}${SUPPORT_SITE}${C.reset}\n`);
74
+ write(` πŸ› ${C.cyan}${ISSUES_URL}${C.reset}\n`);
75
+ if (debugLogPath) {
76
+ write(` πŸ“„ ${C.bold}Attach this debug log:${C.reset} ${C.blue}${debugLogPath}${C.reset}\n`);
77
+ }
78
+ write(`${BAR}\n`);
79
+ }
80
+
81
+ module.exports = { CliError, reportError, SUPPORT_EMAIL, SUPPORT_SITE, ISSUES_URL };