wanas-zcrm-extractor 1.3.0 → 1.5.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  <p align="center">
2
2
  <a href="https://www.wanasapps.com">
3
- <img src="https://www.wanasapps.com/WanasApps-logo.60b1ba4ed8605755.svg" alt="Wanas Apps Logo" width="320">
3
+ <img src="./WanasApps-logo.svg" alt="Wanas Apps Logo" width="320">
4
4
  </a>
5
5
  </p>
6
6
 
@@ -135,7 +135,9 @@ zcrm pull [options]
135
135
  4. Fetches Profiles, Roles, and Webhooks settings.
136
136
  5. Downloads all custom Deluge function scripts.
137
137
  6. Compiles a consolidated database of schemas and summaries.
138
- 7. Prints a rich-text CLI statistics summary showing modules, layouts, fields, and stale files removed.
138
+ 7. Generates a **`README.md`** in the output folder describing the snapshot (org, counts, a per-module table, and the directory layout) — regenerated on every pull from the real data.
139
+ 8. Writes a **debug log** for the run to `<output>/.zcrm/.logs/ZCRM-CLI-<date>.log` (includes the quiet "skipped" entries that don't print to the console).
140
+ 9. Prints a rich-text CLI statistics summary showing modules, layouts, fields, stale files removed, and any skipped/errored requests.
139
141
 
140
142
  #### ⚡ Concurrency (faster pulls, safely)
141
143
  A pull makes many independent metadata requests, so they are run **in parallel** with a global cap on simultaneous calls. Zoho enforces a per-org **concurrency limit** based on your edition — **5** (Free), **10** (Standard/Starter), **15** (Professional), **20** (Enterprise/Zoho One), **25** (Ultimate/CRM Plus). The metadata endpoints used here are 1 credit each and are *not* subject to Zoho's stricter sub-concurrency limits.
@@ -234,7 +236,9 @@ zcrm skill --ide claude --metadata-dir ./crm-meta
234
236
  ---
235
237
 
236
238
  ### `zcrm audit`
237
- Triggers an asynchronous export of the Zoho CRM audit log. By default, it exports all audit logs for the last 3 years. It automatically polls the API until the job is complete and downloads the resulting CSV or ZIP file to `storage/audit_logs/`.
239
+ Triggers an asynchronous export of the Zoho CRM audit log. By default, it exports all audit logs for the last 3 years. It automatically polls the API (with a bounded timeout) until the job is complete and downloads the resulting CSV or ZIP file to `storage/audit_logs/`.
240
+
241
+ > **Requires the `ZohoCRM.settings.audit_logs.ALL` OAuth scope.** It is included in the default scopes, but if you authenticated *before* this scope was added you'll get a `401 invalid oauth scope` — just re-authenticate to grant it: `zcrm logout` then `zcrm login`.
238
242
 
239
243
  ```bash
240
244
  # Export all audit logs
@@ -276,7 +280,6 @@ metadata/
276
280
 
277
281
  ├── crm/ # Standard Zoho CRM metadata layout
278
282
  │ ├── meta/
279
- │ │ ├── custom_links/ # Global Custom Links
280
283
  │ │ ├── data_sharing/ # Data Sharing Settings
281
284
  │ │ ├── data_sharing_actions_summary/ # Data Sharing Actions Summary
282
285
  │ │ ├── data_sharing_rules/ # Data Sharing Rules
@@ -308,6 +311,8 @@ metadata/
308
311
  │ │ │ │ └── <Layout_ID>.json
309
312
  │ │ │ ├── custom_views/ # Custom View configurations
310
313
  │ │ │ │ └── <View_API_Name>.custom_views-meta.json
314
+ │ │ │ ├── custom_links/ # Module custom links/buttons
315
+ │ │ │ │ └── <Link_ID>.json
311
316
  │ │ │ ├── record_locking_configurations/ # Record Locking Rules
312
317
  │ │ │ │ └── <Config_ID>.json
313
318
  │ │ │ ├── related_lists/ # Related List configurations
package/bin/cli.js CHANGED
@@ -521,9 +521,15 @@ program
521
521
  console.log(` \x1b[1mCustom Views:\x1b[0m \x1b[35m${details.totalCustomViews || 0}\x1b[0m (saved inside crm/meta/modules/)`);
522
522
  console.log(` \x1b[1mStale Files Removed:\x1b[0m \x1b[33m${details.removedStale || 0}\x1b[0m (deleted on Zoho CRM since last pull)`);
523
523
  console.log(` \x1b[1mOutput Directory:\x1b[0m \x1b[34m${outputDir}\x1b[0m`);
524
+ if (details.logFile) {
525
+ console.log(` \x1b[1mDebug Log:\x1b[0m \x1b[34m${details.logFile}\x1b[0m`);
526
+ }
524
527
 
528
+ if (details.skipped > 0) {
529
+ console.log(` \x1b[1mSkipped (optional):\x1b[0m \x1b[90m${details.skipped}\x1b[0m settings not available for some modules/org (normal).`);
530
+ }
525
531
  if (details.errors && details.errors.length > 0) {
526
- console.log(`\n \x1b[1;33m⚠️ Warnings:\x1b[0m \x1b[33m${details.errors.length} API request(s) failed or were skipped due to permissions.\x1b[0m`);
532
+ console.log(`\n \x1b[1;33m⚠️ Errors:\x1b[0m \x1b[33m${details.errors.length} unexpected API failure(s).\x1b[0m Details in the log file.`);
527
533
  }
528
534
  console.log('\x1b[1;32m====================================================================\x1b[0m\n');
529
535
  break;
@@ -571,7 +577,14 @@ program
571
577
  console.log('\x1b[1;32m====================================================================\x1b[0m\n');
572
578
  process.exit(0);
573
579
  } catch (error) {
574
- console.error(`\n❌ Audit log export failed: ${error.message}`);
580
+ const apiMsg = error.response?.data?.message || error.message;
581
+ const isScope = error.response?.data?.code === 'OAUTH_SCOPE_MISMATCH' || /scope/i.test(apiMsg || '');
582
+ console.error(`\n❌ Audit log export failed: ${apiMsg}`);
583
+ if (isScope) {
584
+ console.error('\n\x1b[33mThis needs the \x1b[1mZohoCRM.settings.audit_logs.ALL\x1b[0m\x1b[33m OAuth scope, which your current');
585
+ console.error('session was not granted. Re-authenticate to add it:\x1b[0m');
586
+ console.error(' \x1b[36mzcrm logout\x1b[0m then \x1b[36mzcrm login\x1b[0m');
587
+ }
575
588
  process.exit(1);
576
589
  }
577
590
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wanas-zcrm-extractor",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Local Zoho CRM V8 Metadata Extractor CLI and Web Dashboard Utility",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -54,7 +54,7 @@ class AuthService {
54
54
  * @returns {string} Commas-separated list of scopes.
55
55
  */
56
56
  getScopes() {
57
- 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';
57
+ 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.ALL';
58
58
  if (process.env.ZCRM_CLI_MODE === 'true') {
59
59
  return configStore.get('scopes') || process.env.ZOHO_SCOPES || defaultScopes;
60
60
  }
@@ -95,8 +95,13 @@ async function request(method, endpoint, params = {}, data = null, retryCount =
95
95
 
96
96
  logInfo(`Request to ${endpoint} failed. Status: ${status}, Message: ${JSON.stringify(errorData || error.message)}`, 'apiClient.get');
97
97
 
98
+ // A 401 caused by a missing OAuth scope cannot be fixed by refreshing the
99
+ // token (the new token has the same scopes), so fail fast instead of
100
+ // refreshing and retrying twice (which just produced duplicate errors).
101
+ const isScopeError = errorData?.code === 'OAUTH_SCOPE_MISMATCH' || /scope/i.test(errorData?.message || '');
102
+
98
103
  // Handle 401 Unauthorized (Expired token or session)
99
- if (status === 401 && retryCount < 2) {
104
+ if (status === 401 && !isScopeError && retryCount < 2) {
100
105
  logInfo('Received 401 Unauthorized. Forcing token refresh and retrying...', 'apiClient.request');
101
106
  try {
102
107
  await authService.refreshAccessToken();
@@ -13,6 +13,44 @@ const getLogFile = () => {
13
13
  return path.join(process.cwd(), 'storage/logs/errors.log');
14
14
  };
15
15
 
16
+ // Optional per-run "session" log file. When set (e.g. during `zcrm pull`), every
17
+ // log entry is ALSO appended here so the result folder gets a self-contained
18
+ // debug log — including the quiet info/skip entries suppressed from the console.
19
+ let sessionLogPath = null;
20
+
21
+ /**
22
+ * Sets an additional log file that receives a copy of every log entry until
23
+ * cleared. Used to drop a debug log into the pull's output folder.
24
+ * @param {string|null} filePath
25
+ */
26
+ function setSessionLog(filePath) {
27
+ sessionLogPath = filePath || null;
28
+ }
29
+
30
+ /** Stops mirroring log entries to the session log file. */
31
+ function clearSessionLog() {
32
+ sessionLogPath = null;
33
+ }
34
+
35
+ /**
36
+ * Appends an entry to the default log file and (if set) the session log file.
37
+ * @param {string} entry
38
+ */
39
+ function writeEntry(entry) {
40
+ try {
41
+ fs.outputFileSync(getLogFile(), entry, { flag: 'a' });
42
+ } catch (ioErr) {
43
+ console.error('Failed to write to log file:', ioErr);
44
+ }
45
+ if (sessionLogPath) {
46
+ try {
47
+ fs.outputFileSync(sessionLogPath, entry, { flag: 'a' });
48
+ } catch (ioErr) {
49
+ // Best-effort; never let session logging break the run.
50
+ }
51
+ }
52
+ }
53
+
16
54
  /**
17
55
  * Log error message to console and output log file.
18
56
  * @param {Error|object|string} error - The error to log.
@@ -47,11 +85,7 @@ function logError(error, context = '') {
47
85
  console.error(`[ERROR] ${context ? `(${context}) ` : ''}`, error);
48
86
  }
49
87
 
50
- try {
51
- fs.outputFileSync(getLogFile(), logEntry, { flag: 'a' });
52
- } catch (ioErr) {
53
- console.error('Failed to write to log file:', ioErr);
54
- }
88
+ writeEntry(logEntry);
55
89
  }
56
90
 
57
91
  /**
@@ -62,20 +96,18 @@ function logError(error, context = '') {
62
96
  function logInfo(message, context = '') {
63
97
  const timestamp = new Date().toISOString();
64
98
  const logEntry = `[${timestamp}] [INFO] ${context ? `(${context}) ` : ''}${message}\n`;
65
-
99
+
66
100
  // Suppress info console logging when running CLI normally (unless verbose mode is enabled)
67
101
  if (process.env.ZCRM_CLI_MODE !== 'true' || process.env.ZCRM_VERBOSE === 'true') {
68
102
  console.log(`[INFO] ${context ? `(${context}) ` : ''}${message}`);
69
103
  }
70
-
71
- try {
72
- fs.outputFileSync(getLogFile(), logEntry, { flag: 'a' });
73
- } catch (ioErr) {
74
- console.error('Failed to write to log file:', ioErr);
75
- }
104
+
105
+ writeEntry(logEntry);
76
106
  }
77
107
 
78
108
  module.exports = {
79
109
  logError,
80
- logInfo
110
+ logInfo,
111
+ setSessionLog,
112
+ clearSessionLog
81
113
  };
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Builds the README.md that documents a metadata extraction's output folder.
3
+ * The content is generated dynamically from the data actually pulled, so it
4
+ * always reflects the real modules, counts, and settings in the snapshot.
5
+ */
6
+
7
+ /** Formats an ISO timestamp into a readable UTC string. */
8
+ function fmtDate(iso) {
9
+ try {
10
+ return new Date(iso).toUTCString();
11
+ } catch (e) {
12
+ return iso || 'unknown';
13
+ }
14
+ }
15
+
16
+ /**
17
+ * @param {object} info
18
+ * @param {string} info.orgName
19
+ * @param {string} info.generatedAt - ISO timestamp.
20
+ * @param {string} info.version - CLI version.
21
+ * @param {string} info.dc - Zoho data center.
22
+ * @param {object} info.stats - Extraction stats (totals, skipped, errors[]).
23
+ * @param {Array<{api_name:string,label:string,custom:boolean,fields:number,layouts:number,related_lists:number}>} info.modules
24
+ * @param {number} info.functionsCount
25
+ * @param {number} info.profilesCount
26
+ * @param {number} info.rolesCount
27
+ * @param {Array<{key:string,label:string,count:number}>} info.globalSettings
28
+ * @returns {string} Markdown.
29
+ */
30
+ function buildResultReadme(info) {
31
+ const {
32
+ orgName = 'Unknown Org',
33
+ generatedAt = new Date(0).toISOString(),
34
+ version = '',
35
+ dc = 'com',
36
+ stats = {},
37
+ modules = [],
38
+ functionsCount = 0,
39
+ profilesCount = 0,
40
+ rolesCount = 0,
41
+ globalSettings = []
42
+ } = info || {};
43
+
44
+ const errorsCount = Array.isArray(stats.errors) ? stats.errors.length : 0;
45
+ const sortedModules = [...modules].sort((a, b) => (b.fields || 0) - (a.fields || 0));
46
+ const customCount = modules.filter(m => m.custom).length;
47
+ const standardCount = modules.length - customCount;
48
+
49
+ const lines = [];
50
+ lines.push(`# Zoho CRM Metadata Snapshot — ${orgName}`);
51
+ lines.push('');
52
+ lines.push(`> Generated by the \`zcrm\` CLI (Wanas Apps — Zoho CRM V8 Metadata Extractor${version ? ` v${version}` : ''}) on **${fmtDate(generatedAt)}**.`);
53
+ lines.push('');
54
+ lines.push('> ⚠️ **Read-only schema snapshot.** This folder describes the *structure* of the CRM');
55
+ lines.push('> (modules, fields, layouts, picklists, functions, settings). It contains **no CRM record');
56
+ lines.push('> data** and was produced by a tool that **never creates, updates, or deletes** anything in');
57
+ lines.push('> Zoho CRM. Use it to look up exact API names and constraints when writing Deluge or REST code.');
58
+ lines.push('');
59
+ lines.push('---');
60
+ lines.push('');
61
+
62
+ // ---- Summary ----
63
+ lines.push('## 📊 Snapshot summary');
64
+ lines.push('');
65
+ lines.push('| Resource | Count |');
66
+ lines.push('| :--- | ---: |');
67
+ lines.push(`| Organization | ${orgName} |`);
68
+ lines.push(`| Data center | \`${dc}\` |`);
69
+ lines.push(`| Modules (standard / custom) | **${modules.length}** (${standardCount} / ${customCount}) |`);
70
+ lines.push(`| Fields | ${stats.totalFields || 0} |`);
71
+ lines.push(`| Layouts | ${stats.totalLayouts || 0} |`);
72
+ lines.push(`| Related lists | ${stats.totalRelatedLists || 0} |`);
73
+ lines.push(`| Custom views | ${stats.totalCustomViews || 0} |`);
74
+ lines.push(`| Profiles | ${profilesCount} |`);
75
+ lines.push(`| Roles | ${rolesCount} |`);
76
+ lines.push(`| Deluge functions | ${functionsCount} |`);
77
+ lines.push(`| Stale files removed this pull | ${stats.removedStale || 0} |`);
78
+ lines.push(`| Skipped (optional, not available) | ${stats.skipped || 0} |`);
79
+ lines.push(`| Errors (unexpected) | ${errorsCount} |`);
80
+ lines.push('');
81
+ if (stats.skipped) {
82
+ lines.push(`> ℹ️ "Skipped" are optional settings that some modules/this org simply don't have (e.g. tags or duplicate-check on system modules). That is normal and not an error.`);
83
+ lines.push('');
84
+ }
85
+
86
+ // ---- Directory layout ----
87
+ lines.push('## 📂 What\'s in this folder');
88
+ lines.push('');
89
+ lines.push('```text');
90
+ lines.push('.');
91
+ lines.push('├── crm/');
92
+ lines.push('│ ├── meta/');
93
+ lines.push('│ │ ├── modules/<Module_API_Name>/ # per-module schema (see table below)');
94
+ lines.push('│ │ │ ├── <Module>.modules-meta.json # module definition');
95
+ lines.push('│ │ │ ├── summary.json # quick counts + field classifications');
96
+ lines.push('│ │ │ ├── fields/ # every field (.fields-meta.json)');
97
+ lines.push('│ │ │ ├── layouts/ # page layouts (+ map_dependencies/)');
98
+ lines.push('│ │ │ ├── custom_views/ # custom views');
99
+ lines.push('│ │ │ ├── custom_links/ # module links/buttons');
100
+ lines.push('│ │ │ ├── related_lists/ # related lists');
101
+ lines.push('│ │ │ ├── tags/ # module tags');
102
+ lines.push('│ │ │ ├── record_locking_configurations/');
103
+ lines.push('│ │ │ └── workflow_configurations/');
104
+ lines.push('│ │ ├── profiles/ # permission profiles');
105
+ lines.push('│ │ ├── roles/ # role hierarchy');
106
+ lines.push('│ │ ├── email_templates/<Module>/ # .meta.json + .html (body)');
107
+ lines.push('│ │ ├── inventory_templates/<Module>/ # .meta.json + .html (body)');
108
+ lines.push('│ │ ├── workflow_rules/<Trigger_Module>/<Rule>.json');
109
+ lines.push('│ │ └── ... # other org-wide settings (see below)');
110
+ lines.push('│ └── functions/ # custom Deluge scripts (.ds), by namespace');
111
+ lines.push('├── .zcrm/');
112
+ lines.push('│ ├── .store/ # consolidated indexes (index.json, field_map.json, ...)');
113
+ lines.push('│ ├── .org/org.json # organization metadata');
114
+ lines.push('│ └── .logs/ # debug log for this pull');
115
+ lines.push('├── zcrm-project.json # ZCRM project config');
116
+ lines.push('└── README.md # this file');
117
+ lines.push('```');
118
+ lines.push('');
119
+ lines.push('Handy starting points for an AI assistant or a developer:');
120
+ lines.push('');
121
+ lines.push('- **`.zcrm/.store/index.json`** — master list of module API names.');
122
+ lines.push('- **`.zcrm/.store/field_map.json`** — `{ Module: { Field: data_type } }` lookup.');
123
+ lines.push('- **`crm/meta/modules/<Module>/summary.json`** — required fields, picklists, lookups, subforms.');
124
+ lines.push('');
125
+
126
+ // ---- Modules table ----
127
+ if (sortedModules.length > 0) {
128
+ lines.push('## 🧩 Modules');
129
+ lines.push('');
130
+ lines.push('| Module (API name) | Label | Type | Fields | Layouts | Related lists |');
131
+ lines.push('| :--- | :--- | :--- | ---: | ---: | ---: |');
132
+ for (const m of sortedModules) {
133
+ const type = m.custom ? 'Custom' : 'Standard';
134
+ lines.push(`| \`${m.api_name}\` | ${m.label || ''} | ${type} | ${m.fields || 0} | ${m.layouts || 0} | ${m.related_lists || 0} |`);
135
+ }
136
+ lines.push('');
137
+ }
138
+
139
+ // ---- Global settings ----
140
+ const presentSettings = globalSettings.filter(g => g && g.count > 0);
141
+ if (presentSettings.length > 0) {
142
+ lines.push('## ⚙️ Org-wide settings extracted');
143
+ lines.push('');
144
+ lines.push('| Setting | Items |');
145
+ lines.push('| :--- | ---: |');
146
+ for (const g of presentSettings) {
147
+ lines.push(`| ${g.label || g.key} | ${g.count} |`);
148
+ }
149
+ lines.push('');
150
+ }
151
+
152
+ // ---- Usage ----
153
+ lines.push('## 🚀 Using this snapshot');
154
+ lines.push('');
155
+ lines.push('- Generate an AI-assistant context file from this snapshot: **`zcrm skill`**.');
156
+ lines.push('- Read the AI ingestion guide: **`zcrm llm`**.');
157
+ lines.push('- Browse it in a local web UI: **`zcrm dashboard`**.');
158
+ lines.push('- Refresh it any time: **`zcrm pull`** (overwrites changed files, removes data deleted on Zoho).');
159
+ lines.push('');
160
+ lines.push('_This file is regenerated on every `zcrm pull`._');
161
+ lines.push('');
162
+
163
+ return lines.join('\n');
164
+ }
165
+
166
+ module.exports = { buildResultReadme };