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 +9 -4
- package/bin/cli.js +15 -2
- package/package.json +1 -1
- package/src/services/authService.js +1 -1
- package/src/services/metadata/crmMetadataService.js +0 -0
- package/src/utils/apiClient.js +6 -1
- package/src/utils/logger.js +45 -13
- package/src/utils/readmeTemplate.js +166 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<a href="https://www.wanasapps.com">
|
|
3
|
-
<img src="
|
|
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.
|
|
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⚠️
|
|
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
|
-
|
|
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
|
@@ -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
|
}
|
|
Binary file
|
package/src/utils/apiClient.js
CHANGED
|
@@ -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();
|
package/src/utils/logger.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 };
|