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 +3 -1
- package/bin/cli.js +48 -32
- package/package.json +4 -2
- package/src/services/auditLogService.js +7 -5
- package/src/utils/apiClient.js +8 -9
- package/src/utils/errorReporter.js +81 -0
package/README.md
CHANGED
|
@@ -420,7 +420,9 @@ metadata/
|
|
|
420
420
|
|
|
421
421
|
## π©Ί Troubleshooting
|
|
422
422
|
|
|
423
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
15
|
-
//
|
|
16
|
-
// (
|
|
17
|
-
// `
|
|
18
|
-
|
|
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);
|
package/src/utils/apiClient.js
CHANGED
|
@@ -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
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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 };
|