wanas-zcrm-extractor 1.5.2 → 1.6.0
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 +7 -1
- package/bin/cli.js +38 -4
- package/package.json +1 -1
- package/src/services/auditLogService.js +5 -1
- package/src/services/authService.js +7 -1
- package/src/services/metadata/crmMetadataService.js +12 -7
- package/src/utils/logger.js +80 -39
package/README.md
CHANGED
|
@@ -329,7 +329,7 @@ zcrm audit # last 3 years by default
|
|
|
329
329
|
zcrm audit --filter input.json # custom export criteria
|
|
330
330
|
```
|
|
331
331
|
|
|
332
|
-
> **Requires the `ZohoCRM.settings.audit_logs.
|
|
332
|
+
> **Requires the audit-log OAuth scopes** — `ZohoCRM.settings.audit_logs.CREATE` and `ZohoCRM.settings.audit_logs.READ` (both in the default scopes). If you authenticated *before* they were added you'll see `401 invalid oauth scope` — just re-authenticate: `zcrm logout` then `zcrm login`. *(If the download step later reports a file-scope error, add `ZohoFiles.files.READ` to `ZOHO_SCOPES` and log in again.)*
|
|
333
333
|
</details>
|
|
334
334
|
|
|
335
335
|
<br>
|
|
@@ -418,6 +418,12 @@ metadata/
|
|
|
418
418
|
|
|
419
419
|
---
|
|
420
420
|
|
|
421
|
+
## 🩺 Troubleshooting
|
|
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/`.
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
421
427
|
## ✉️ Support & Feedback
|
|
422
428
|
|
|
423
429
|
Found a bug, have a feature request, or need custom Zoho integrations? Reach out:
|
package/bin/cli.js
CHANGED
|
@@ -9,11 +9,43 @@ const http = require('http');
|
|
|
9
9
|
const url = require('url');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const { exec, spawn } = require('child_process');
|
|
12
|
+
const os = require('os');
|
|
12
13
|
const configStore = require('../src/utils/configStore');
|
|
13
14
|
const authService = require('../src/services/authService');
|
|
14
15
|
const crmMetadataService = require('../src/services/metadata/crmMetadataService');
|
|
15
16
|
const auditLogService = require('../src/services/auditLogService');
|
|
16
17
|
const skillGenerator = require('../src/utils/skillGenerator');
|
|
18
|
+
const { addSessionLog, logInfo, shortErrorMessage } = require('../src/utils/logger');
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Per-command debug log
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Every real command writes a timestamped debug log to ~/.zcrm/logs/ that
|
|
24
|
+
// captures ALL log entries for the run — including the quiet info/skip lines
|
|
25
|
+
// and full API error bodies that are not printed to the console. The path is
|
|
26
|
+
// shown if the command fails. (Command args are intentionally NOT logged, to
|
|
27
|
+
// avoid writing secrets like --client-secret / --refresh-token to disk.)
|
|
28
|
+
const LOGGED_COMMANDS = ['login', 'logout', 'whoami', 'status', 'pull', 'audit', 'llm', 'skill'];
|
|
29
|
+
let debugLogPath = null;
|
|
30
|
+
(function setupDebugLog() {
|
|
31
|
+
try {
|
|
32
|
+
const cmd = process.argv[2];
|
|
33
|
+
if (!LOGGED_COMMANDS.includes(cmd)) return;
|
|
34
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
35
|
+
debugLogPath = path.join(os.homedir(), '.zcrm', 'logs', `${cmd}-${ts}.log`);
|
|
36
|
+
addSessionLog(debugLogPath);
|
|
37
|
+
logInfo(`zcrm ${cmd} started.`, 'cli');
|
|
38
|
+
} catch (err) {
|
|
39
|
+
// Debug-logging setup must never break the command.
|
|
40
|
+
}
|
|
41
|
+
})();
|
|
42
|
+
|
|
43
|
+
// Point the user at the debug log when a command exits with an error.
|
|
44
|
+
process.on('exit', (code) => {
|
|
45
|
+
if (code && debugLogPath) {
|
|
46
|
+
try { process.stderr.write(`\x1b[90mℹ Debug log: ${debugLogPath}\x1b[0m\n`); } catch (e) { /* ignore */ }
|
|
47
|
+
}
|
|
48
|
+
});
|
|
17
49
|
|
|
18
50
|
/**
|
|
19
51
|
* Opens a URL in the user's default browser in a cross-platform-safe way.
|
|
@@ -577,13 +609,15 @@ program
|
|
|
577
609
|
console.log('\x1b[1;32m====================================================================\x1b[0m\n');
|
|
578
610
|
process.exit(0);
|
|
579
611
|
} catch (error) {
|
|
580
|
-
const apiMsg = error
|
|
612
|
+
const apiMsg = shortErrorMessage(error);
|
|
581
613
|
const isScope = error.response?.data?.code === 'OAUTH_SCOPE_MISMATCH' || /scope/i.test(apiMsg || '');
|
|
582
614
|
console.error(`\n❌ Audit log export failed: ${apiMsg}`);
|
|
583
615
|
if (isScope) {
|
|
584
|
-
console.error('\n\x1b[33mThis needs the \x1b[1mZohoCRM.settings.audit_logs.
|
|
585
|
-
console.error('session was not granted.
|
|
586
|
-
console.error('
|
|
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');
|
|
587
621
|
}
|
|
588
622
|
process.exit(1);
|
|
589
623
|
}
|
package/package.json
CHANGED
|
@@ -11,7 +11,11 @@ async function delay(ms) {
|
|
|
11
11
|
|
|
12
12
|
async function exportAuditLog(filterFile) {
|
|
13
13
|
try {
|
|
14
|
-
|
|
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;
|
|
15
19
|
|
|
16
20
|
if (filterFile) {
|
|
17
21
|
const filterPath = path.resolve(process.cwd(), filterFile);
|
|
@@ -54,7 +54,13 @@ class AuthService {
|
|
|
54
54
|
* @returns {string} Commas-separated list of scopes.
|
|
55
55
|
*/
|
|
56
56
|
getScopes() {
|
|
57
|
-
|
|
57
|
+
// Every scope below is a documented ZohoCRM V8 scope. Audit logs have NO
|
|
58
|
+
// `.ALL` variant — the export flow uses the specific operation scopes
|
|
59
|
+
// (CREATE to initiate, READ to poll). The download step's cross-service
|
|
60
|
+
// ZohoFiles scope is intentionally NOT requested here so a single login
|
|
61
|
+
// stays entirely within ZohoCRM scopes (it can be added via ZOHO_SCOPES if
|
|
62
|
+
// a particular account needs it for the audit download).
|
|
63
|
+
const defaultScopes = 'ZohoCRM.modules.ALL,ZohoCRM.settings.ALL,ZohoCRM.users.ALL,ZohoCRM.org.ALL,ZohoCRM.bulk.ALL,ZohoCRM.notifications.ALL,ZohoCRM.coql.READ,ZohoCRM.settings.functions.ALL,ZohoCRM.settings.audit_logs.CREATE,ZohoCRM.settings.audit_logs.READ';
|
|
58
64
|
if (process.env.ZCRM_CLI_MODE === 'true') {
|
|
59
65
|
return configStore.get('scopes') || process.env.ZOHO_SCOPES || defaultScopes;
|
|
60
66
|
}
|
|
@@ -4,7 +4,7 @@ const BaseMetadataService = require('./baseMetadataService');
|
|
|
4
4
|
const apiClient = require('../../utils/apiClient');
|
|
5
5
|
const { mapWithConcurrency } = require('../../utils/concurrency');
|
|
6
6
|
const { sweepDir, sweepDeletedDirs, normalizePath } = require('../../utils/fsSync');
|
|
7
|
-
const { logError, logInfo,
|
|
7
|
+
const { logError, logInfo, addSessionLog, removeSessionLog } = require('../../utils/logger');
|
|
8
8
|
const { formatDeluge } = require('../../utils/delugeFormatter');
|
|
9
9
|
const { buildResultReadme } = require('../../utils/readmeTemplate');
|
|
10
10
|
const pkg = require('../../../package.json');
|
|
@@ -113,6 +113,10 @@ class CrmMetadataService extends BaseMetadataService {
|
|
|
113
113
|
// metadata-only with no record-data access.
|
|
114
114
|
const withCounts = options.withCounts === true;
|
|
115
115
|
|
|
116
|
+
// Path of the debug log written into the output folder (set once the run
|
|
117
|
+
// starts; declared here so the finally block can stop mirroring to it).
|
|
118
|
+
let outputLogPath = null;
|
|
119
|
+
|
|
116
120
|
// Track every file written this run so stale files can be swept afterward.
|
|
117
121
|
const writtenFiles = new Set();
|
|
118
122
|
const recordWrite = (p) => writtenFiles.add(normalizePath(p));
|
|
@@ -207,13 +211,13 @@ class CrmMetadataService extends BaseMetadataService {
|
|
|
207
211
|
const month = String(today.getMonth() + 1).padStart(2, '0');
|
|
208
212
|
const date = String(today.getDate()).padStart(2, '0');
|
|
209
213
|
const logFileName = `ZCRM-CLI-${year}-${month}-${date}.log`;
|
|
210
|
-
|
|
214
|
+
outputLogPath = path.join(finalOutputDir, '.zcrm', '.logs', logFileName);
|
|
211
215
|
await fs.outputJson(path.join(finalOutputDir, '.zcrm', '.logs', '.audit.json'), {}, { spaces: 2 });
|
|
212
|
-
await fs.outputFile(
|
|
216
|
+
await fs.outputFile(outputLogPath, `[${today.toISOString()}] zcrm pull started (concurrency ${concurrency}${withCounts ? ', record counts ON' : ''}).\n`, 'utf8');
|
|
213
217
|
// Mirror every subsequent log entry (including the quiet info/skip lines
|
|
214
218
|
// suppressed from the console) into a debug log in the result folder.
|
|
215
|
-
|
|
216
|
-
stats.logFile =
|
|
219
|
+
addSessionLog(outputLogPath);
|
|
220
|
+
stats.logFile = outputLogPath;
|
|
217
221
|
|
|
218
222
|
// 1. Fetch Org Information to get Org ID/Name
|
|
219
223
|
onProgress('FETCH_ORG', 'Fetching CRM Organization details...');
|
|
@@ -900,8 +904,9 @@ class CrmMetadataService extends BaseMetadataService {
|
|
|
900
904
|
onProgress('FAILURE', `Extraction failed: ${error.message}`);
|
|
901
905
|
throw error;
|
|
902
906
|
} finally {
|
|
903
|
-
// Stop mirroring logs to the result folder once the pull is done
|
|
904
|
-
|
|
907
|
+
// Stop mirroring logs to the result folder once the pull is done (the
|
|
908
|
+
// CLI's own per-command debug log stays active).
|
|
909
|
+
if (outputLogPath) removeSessionLog(outputLogPath);
|
|
905
910
|
}
|
|
906
911
|
}
|
|
907
912
|
}
|
package/src/utils/logger.js
CHANGED
|
@@ -3,7 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Dynamically resolves the log file path based on running mode (CLI vs Web).
|
|
6
|
+
* Dynamically resolves the default log file path based on running mode (CLI vs Web).
|
|
7
7
|
* @returns {string} The resolved absolute file path for logging.
|
|
8
8
|
*/
|
|
9
9
|
const getLogFile = () => {
|
|
@@ -13,71 +13,110 @@ const getLogFile = () => {
|
|
|
13
13
|
return path.join(process.cwd(), 'storage/logs/errors.log');
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
let
|
|
16
|
+
// Additional "session" log files that receive a copy of every log entry. The
|
|
17
|
+
// CLI adds one per command run (for debugging), and `zcrm pull` adds another in
|
|
18
|
+
// its output folder. Multiple targets can be active at once.
|
|
19
|
+
let sessionLogPaths = [];
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
/** Adds an extra log file that mirrors every subsequent log entry. */
|
|
22
|
+
function addSessionLog(filePath) {
|
|
23
|
+
if (filePath && !sessionLogPaths.includes(filePath)) sessionLogPaths.push(filePath);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Removes a previously-added session log file. */
|
|
27
|
+
function removeSessionLog(filePath) {
|
|
28
|
+
sessionLogPaths = sessionLogPaths.filter((p) => p !== filePath);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
/**
|
|
31
|
-
function
|
|
32
|
-
|
|
31
|
+
/** Removes all session log files. */
|
|
32
|
+
function clearSessionLogs() {
|
|
33
|
+
sessionLogPaths = [];
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
|
-
* Appends an entry to the default log file and
|
|
37
|
+
* Appends an entry to the default log file and every active session log file.
|
|
37
38
|
* @param {string} entry
|
|
38
39
|
*/
|
|
39
40
|
function writeEntry(entry) {
|
|
40
41
|
try {
|
|
41
42
|
fs.outputFileSync(getLogFile(), entry, { flag: 'a' });
|
|
42
43
|
} catch (ioErr) {
|
|
43
|
-
|
|
44
|
+
// Last resort — don't crash the command over a logging failure.
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
+
for (const p of sessionLogPaths) {
|
|
46
47
|
try {
|
|
47
|
-
fs.outputFileSync(
|
|
48
|
+
fs.outputFileSync(p, entry, { flag: 'a' });
|
|
48
49
|
} catch (ioErr) {
|
|
49
|
-
//
|
|
50
|
+
// best-effort
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
/**
|
|
55
|
-
*
|
|
56
|
+
* Builds a detailed, file-friendly description of an error — including the full
|
|
57
|
+
* HTTP response body for Axios errors, which is essential for debugging API
|
|
58
|
+
* failures (e.g. a Zoho 400 whose real reason lives in the response body).
|
|
59
|
+
* @param {Error|object|string} error
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
function describeError(error) {
|
|
63
|
+
if (error && error.isAxiosError) {
|
|
64
|
+
const method = error.config?.method?.toUpperCase() || 'GET';
|
|
65
|
+
const url = error.config?.url || '';
|
|
66
|
+
const status = error.response?.status || 'no-status';
|
|
67
|
+
const body = error.response?.data;
|
|
68
|
+
const bodyStr = body == null
|
|
69
|
+
? '(no response body)'
|
|
70
|
+
: (typeof body === 'string' ? body : JSON.stringify(body, null, 2));
|
|
71
|
+
return [
|
|
72
|
+
`${error.message}`,
|
|
73
|
+
`Request: ${method} ${url}`,
|
|
74
|
+
`Status: ${status}`,
|
|
75
|
+
`Response body: ${bodyStr}`,
|
|
76
|
+
`Stack: ${error.stack}`
|
|
77
|
+
].join('\n');
|
|
78
|
+
}
|
|
79
|
+
if (error instanceof Error) {
|
|
80
|
+
return `${error.message}\nStack: ${error.stack}`;
|
|
81
|
+
}
|
|
82
|
+
if (typeof error === 'object') {
|
|
83
|
+
return JSON.stringify(error, null, 2);
|
|
84
|
+
}
|
|
85
|
+
return String(error);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns a concise one-line message for console output from any error,
|
|
90
|
+
* preferring the Zoho response message/code when present.
|
|
91
|
+
* @param {Error|object|string} error
|
|
92
|
+
* @returns {string}
|
|
93
|
+
*/
|
|
94
|
+
function shortErrorMessage(error) {
|
|
95
|
+
const body = error?.response?.data;
|
|
96
|
+
if (body) {
|
|
97
|
+
if (typeof body === 'string') return body;
|
|
98
|
+
if (body.message) return `${body.message}${body.code ? ` [${body.code}]` : ''}`;
|
|
99
|
+
return JSON.stringify(body);
|
|
100
|
+
}
|
|
101
|
+
return error?.message || String(error);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Log error message to console and log file(s).
|
|
56
106
|
* @param {Error|object|string} error - The error to log.
|
|
57
107
|
* @param {string} [context] - Context name where error occurred.
|
|
58
108
|
*/
|
|
59
109
|
function logError(error, context = '') {
|
|
60
110
|
const timestamp = new Date().toISOString();
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (error instanceof Error) {
|
|
64
|
-
errorMessage = `${error.message}\nStack: ${error.stack}`;
|
|
65
|
-
} else if (typeof error === 'object') {
|
|
66
|
-
errorMessage = JSON.stringify(error, null, 2);
|
|
67
|
-
} else {
|
|
68
|
-
errorMessage = String(error);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const logEntry = `[${timestamp}] [ERROR] ${context ? `(${context}) ` : ''}${errorMessage}\n--------------------------------------------------\n`;
|
|
111
|
+
const logEntry = `[${timestamp}] [ERROR] ${context ? `(${context}) ` : ''}${describeError(error)}\n--------------------------------------------------\n`;
|
|
72
112
|
|
|
73
|
-
// Output
|
|
113
|
+
// Output a clean one-liner to the CLI instead of dumping the huge Axios object.
|
|
74
114
|
if (process.env.ZCRM_CLI_MODE === 'true') {
|
|
75
115
|
if (error && error.isAxiosError) {
|
|
76
116
|
const method = error.config?.method?.toUpperCase() || 'GET';
|
|
77
117
|
const url = error.config?.url || '';
|
|
78
118
|
const status = error.response?.status || 'Unknown Status';
|
|
79
|
-
|
|
80
|
-
console.error(`[ERROR] ${context ? `(${context}) ` : ''}API Request Failed: ${method} ${url} -> Status: ${status} (${msg})`);
|
|
119
|
+
console.error(`[ERROR] ${context ? `(${context}) ` : ''}API Request Failed: ${method} ${url} -> Status: ${status} (${shortErrorMessage(error)})`);
|
|
81
120
|
} else {
|
|
82
121
|
console.error(`[ERROR] ${context ? `(${context}) ` : ''}${error.message || error}`);
|
|
83
122
|
}
|
|
@@ -89,7 +128,7 @@ function logError(error, context = '') {
|
|
|
89
128
|
}
|
|
90
129
|
|
|
91
130
|
/**
|
|
92
|
-
* Log informational message to console and
|
|
131
|
+
* Log informational message to console and log file(s).
|
|
93
132
|
* @param {string} message - The message.
|
|
94
133
|
* @param {string} [context] - Context name.
|
|
95
134
|
*/
|
|
@@ -97,7 +136,7 @@ function logInfo(message, context = '') {
|
|
|
97
136
|
const timestamp = new Date().toISOString();
|
|
98
137
|
const logEntry = `[${timestamp}] [INFO] ${context ? `(${context}) ` : ''}${message}\n`;
|
|
99
138
|
|
|
100
|
-
// Suppress info console logging when running CLI normally (unless verbose
|
|
139
|
+
// Suppress info console logging when running CLI normally (unless verbose).
|
|
101
140
|
if (process.env.ZCRM_CLI_MODE !== 'true' || process.env.ZCRM_VERBOSE === 'true') {
|
|
102
141
|
console.log(`[INFO] ${context ? `(${context}) ` : ''}${message}`);
|
|
103
142
|
}
|
|
@@ -108,6 +147,8 @@ function logInfo(message, context = '') {
|
|
|
108
147
|
module.exports = {
|
|
109
148
|
logError,
|
|
110
149
|
logInfo,
|
|
111
|
-
|
|
112
|
-
|
|
150
|
+
shortErrorMessage,
|
|
151
|
+
addSessionLog,
|
|
152
|
+
removeSessionLog,
|
|
153
|
+
clearSessionLogs
|
|
113
154
|
};
|