wanas-zcrm-extractor 1.0.7 → 1.2.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 +152 -35
- package/banner.txt +8 -7
- package/bin/cli.js +221 -73
- package/llm.md +8 -0
- package/package.json +2 -1
- package/server.js +2 -1
- package/src/controllers/metadataController.js +33 -30
- package/src/middleware/authMiddleware.js +3 -0
- package/src/services/auditLogService.js +139 -0
- package/src/services/authService.js +16 -0
- package/src/services/metadata/crmMetadataService.js +476 -287
- package/src/utils/apiClient.js +162 -82
- package/src/utils/concurrency.js +38 -0
- package/src/utils/configStore.js +18 -6
- package/src/utils/delugeFormatter.js +45 -0
- package/src/utils/fsSync.js +107 -0
- package/src/utils/skillGenerator.js +220 -0
package/README.md
CHANGED
|
@@ -10,16 +10,32 @@ A robust, production-quality CLI utility and local web dashboard designed to aut
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
> ### ⚠️ Important: read-only — it never modifies your CRM
|
|
14
|
+
>
|
|
15
|
+
> `zcrm` is strictly **read-only**: it **never creates, updates, or deletes** anything in Zoho
|
|
16
|
+
> CRM. By **default** it pulls **schema/metadata only** — module and field definitions, layouts,
|
|
17
|
+
> picklists, related lists, tags, profiles, roles, webhooks, org-wide settings, and custom
|
|
18
|
+
> function source code — with **no access to your CRM records**.
|
|
19
|
+
>
|
|
20
|
+
> Reading actual record data is always **opt-in and explicit**: per-module record *counts* only
|
|
21
|
+
> with `zcrm pull --with-counts` (~50 API credits/module), and audit-log data only via the
|
|
22
|
+
> `zcrm audit` command. Even then, the tool only ever *reads* — it cannot change anything in your CRM.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
13
26
|
## 🌟 Key Features
|
|
14
27
|
|
|
15
28
|
1. **Global CLI Wrapper (`zcrm`):** Command-line tools for authentication, data extraction, and starting the local web explorer dashboard.
|
|
16
29
|
2. **Standard ZCRM CLI Directory Layout:** Writes module structures, layouts, fields, profiles, and roles in a format fully compatible with standard Zoho CRM developer layouts.
|
|
17
30
|
3. **Deluge Script Downloader:** Pulls custom function source codes, organizes them into folders by namespace, and saves them as local Deluge `.ds` files.
|
|
18
31
|
4. **Interactive Dashboard:** Launch a premium, glassmorphic dark-theme dashboard to review extracted modules, view field properties, search and filter APIs, and download structured ZIP archives of the exports.
|
|
19
|
-
5. **
|
|
32
|
+
5. **AI Skill Generator (`zcrm skill`):** Generates ready-to-use skill/rules files for Claude Code, Cursor, Windsurf, GitHub Copilot, Cline, Gemini, Codex/AGENTS.md, and plain Markdown — so your AI assistant can read the extracted schema and write valid Deluge using exact API names.
|
|
33
|
+
6. **Robust Pipeline & Error Handling:**
|
|
34
|
+
- Concurrent extraction with a configurable, edition-aware cap on simultaneous API calls for fast pulls.
|
|
35
|
+
- One-way sync: overwrites changed files and removes local metadata deleted on Zoho — without wiping data on a failed run.
|
|
20
36
|
- Interactive progress indicators showing modules and deluge functions as they download.
|
|
21
37
|
- Resilient Axios client with auto-refreshing OAuth tokens.
|
|
22
|
-
- Exponential backoff retry system to handle API rate limits (HTTP 429).
|
|
38
|
+
- Exponential backoff retry system to handle API rate/concurrency limits (HTTP 429).
|
|
23
39
|
- Rich console results screen showing execution statistics.
|
|
24
40
|
|
|
25
41
|
---
|
|
@@ -101,7 +117,7 @@ zcrm login [options]
|
|
|
101
117
|
---
|
|
102
118
|
|
|
103
119
|
### 2. `zcrm pull`
|
|
104
|
-
Crawls all Zoho CRM endpoints to extract metadata schemas and custom
|
|
120
|
+
Crawls all Zoho CRM endpoints (read-only) to extract metadata schemas and custom function Deluge scripts, and **syncs** them into your local folder.
|
|
105
121
|
|
|
106
122
|
```bash
|
|
107
123
|
zcrm pull [options]
|
|
@@ -109,15 +125,36 @@ zcrm pull [options]
|
|
|
109
125
|
|
|
110
126
|
**Options:**
|
|
111
127
|
* `-o, --output <dir>` - Destination folder path (defaults to `./metadata` in the current working directory).
|
|
128
|
+
* `-c, --concurrency <n>` - Maximum number of simultaneous Zoho API calls, `1`–`25` (defaults to `5`).
|
|
129
|
+
* `--with-counts` - Also fetch each module's **record count**. This reads record data (not just schema) and costs **~50 API credits per module**, so it is **off by default**. Omit it to keep the pull strictly metadata-only.
|
|
112
130
|
|
|
113
131
|
**What happens during a pull:**
|
|
114
132
|
1. Verifies authentication and refreshes active OAuth access tokens automatically.
|
|
115
133
|
2. Fetches Zoho Org information and sets up local project mapping configs.
|
|
116
|
-
3. Crawls every module to pull field configurations and page layout schemas (with a
|
|
134
|
+
3. Crawls every module to pull field configurations and page layout schemas (with a live progress indicator).
|
|
117
135
|
4. Fetches Profiles, Roles, and Webhooks settings.
|
|
118
|
-
5. Downloads all custom
|
|
136
|
+
5. Downloads all custom Deluge function scripts.
|
|
119
137
|
6. Compiles a consolidated database of schemas and summaries.
|
|
120
|
-
7. Prints a rich-text CLI
|
|
138
|
+
7. Prints a rich-text CLI statistics summary showing modules, layouts, fields, and stale files removed.
|
|
139
|
+
|
|
140
|
+
#### ⚡ Concurrency (faster pulls, safely)
|
|
141
|
+
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.
|
|
142
|
+
|
|
143
|
+
The default of `5` is safe for **every** edition. If you are on a higher edition and want faster pulls, raise it up to your edition's limit:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
zcrm pull -o ./metadata --concurrency 15
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
If the limit is ever exceeded (HTTP 429 `TOO_MANY_REQUESTS`), the client automatically backs off exponentially and retries, so a pull never fails just because it was briefly throttled. Leave headroom if other integrations share the same org.
|
|
150
|
+
|
|
151
|
+
#### 🔄 Sync behavior (overwrite + remove stale)
|
|
152
|
+
A pull is a **one-way sync from Zoho into your folder**:
|
|
153
|
+
* Existing files are **overwritten in place** with the latest metadata.
|
|
154
|
+
* Files that no longer correspond to anything on Zoho (a deleted module, field, layout, custom view, related list, profile, role, or function) are **removed locally** — but only within scopes whose authoritative fetch succeeded this run.
|
|
155
|
+
* If a fetch **fails** (e.g. a transient network/permission error), the previous local data for that scope is **kept**, never deleted. A partial or interrupted pull therefore never wipes your data.
|
|
156
|
+
|
|
157
|
+
The summary reports how many stale files were removed. Your own files outside the managed `crm/` and `.zcrm/` trees are never touched.
|
|
121
158
|
|
|
122
159
|
---
|
|
123
160
|
|
|
@@ -157,7 +194,60 @@ zcrm llm
|
|
|
157
194
|
|
|
158
195
|
---
|
|
159
196
|
|
|
160
|
-
### 6. `zcrm
|
|
197
|
+
### 6. `zcrm skill`
|
|
198
|
+
Generates an **AI assistant skill / rules file** for your IDE or AI coding tool, derived from the `zcrm llm` guide. The generated file teaches your assistant how to read the extracted metadata snapshot and produce valid Deluge / REST code using exact API names — and clearly states that the snapshot is read-only schema with no CRM record access.
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
zcrm skill [options]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Options:**
|
|
205
|
+
* `--ide <ide>` - Target tool (skips the interactive prompt). One of: `claude`, `cursor`, `windsurf`, `copilot`, `cline`, `gemini`, `codex`, `markdown`.
|
|
206
|
+
* `-d, --dir <dir>` - Project directory to write into (defaults to the current directory).
|
|
207
|
+
* `-m, --metadata-dir <dir>` - Path to your extracted metadata, referenced inside the skill (defaults to `./metadata`).
|
|
208
|
+
* `-f, --force` - Overwrite an existing dedicated skill file without prompting.
|
|
209
|
+
|
|
210
|
+
When run without `--ide`, it prompts you to choose your tool and writes the file to the right place:
|
|
211
|
+
|
|
212
|
+
| Tool | Generated file |
|
|
213
|
+
| :--- | :--- |
|
|
214
|
+
| **Claude Code** | `.claude/skills/zoho-crm-deluge/SKILL.md` |
|
|
215
|
+
| **Cursor** | `.cursor/rules/zoho-crm-deluge.mdc` |
|
|
216
|
+
| **Windsurf** | `.windsurf/rules/zoho-crm-deluge.md` |
|
|
217
|
+
| **GitHub Copilot** | `.github/copilot-instructions.md` |
|
|
218
|
+
| **Cline / Roo Code** | `.clinerules/zoho-crm-deluge.md` |
|
|
219
|
+
| **Gemini CLI** | `GEMINI.md` |
|
|
220
|
+
| **Codex / generic** | `AGENTS.md` |
|
|
221
|
+
| **Plain Markdown** | `ZOHO_CRM_AI_CONTEXT.md` |
|
|
222
|
+
|
|
223
|
+
> For shared files (`AGENTS.md`, `GEMINI.md`, `.github/copilot-instructions.md`), the command inserts a clearly-delimited `ZCRM` block and re-generates it in place on subsequent runs, leaving the rest of your file untouched.
|
|
224
|
+
|
|
225
|
+
**Examples:**
|
|
226
|
+
```bash
|
|
227
|
+
# Interactive — pick your IDE from a menu
|
|
228
|
+
zcrm skill
|
|
229
|
+
|
|
230
|
+
# Non-interactive — generate a Claude Code skill referencing ./crm-meta
|
|
231
|
+
zcrm skill --ide claude --metadata-dir ./crm-meta
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
### `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/`.
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
# Export all audit logs
|
|
241
|
+
zcrm audit
|
|
242
|
+
|
|
243
|
+
# Export with specific criteria using a JSON file
|
|
244
|
+
zcrm audit --filter input.json
|
|
245
|
+
```
|
|
246
|
+
*(See Zoho CRM documentation for the format of the filter JSON).*
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
### 7. `zcrm logout`
|
|
161
251
|
Clears stored configuration credentials, OAuth tokens, and active sessions from the workspace.
|
|
162
252
|
|
|
163
253
|
```bash
|
|
@@ -185,31 +275,52 @@ metadata/
|
|
|
185
275
|
│ └── functions.json # Extracted functions catalog
|
|
186
276
|
│
|
|
187
277
|
├── crm/ # Standard Zoho CRM metadata layout
|
|
188
|
-
│
|
|
189
|
-
│
|
|
190
|
-
│
|
|
191
|
-
│
|
|
192
|
-
│
|
|
193
|
-
│
|
|
194
|
-
│
|
|
195
|
-
│
|
|
196
|
-
│
|
|
197
|
-
│
|
|
198
|
-
│
|
|
199
|
-
│
|
|
200
|
-
│
|
|
201
|
-
│
|
|
202
|
-
│
|
|
203
|
-
│ ├──
|
|
204
|
-
│
|
|
205
|
-
│
|
|
206
|
-
│
|
|
207
|
-
│
|
|
208
|
-
│
|
|
209
|
-
│
|
|
210
|
-
│
|
|
211
|
-
│
|
|
212
|
-
│
|
|
278
|
+
│ ├── meta/
|
|
279
|
+
│ │ ├── custom_links/ # Global Custom Links
|
|
280
|
+
│ │ ├── data_sharing/ # Data Sharing Settings
|
|
281
|
+
│ │ ├── data_sharing_actions_summary/ # Data Sharing Actions Summary
|
|
282
|
+
│ │ ├── data_sharing_rules/ # Data Sharing Rules
|
|
283
|
+
│ │ ├── email_templates/ # Email Templates
|
|
284
|
+
│ │ ├── global_picklists/ # Global Picklists
|
|
285
|
+
│ │ ├── inventory_templates/# Inventory Templates
|
|
286
|
+
│ │ ├── territories/ # Territories
|
|
287
|
+
│ │ ├── variables/ # Global Variables
|
|
288
|
+
│ │ ├── wizards/ # CRM Wizards
|
|
289
|
+
│ │ ├── workflow_rules/ # Global Workflow Rules
|
|
290
|
+
│ │ ├── modules/ # Individual Module schemas
|
|
291
|
+
│ │ │ └── <Module_API_Name>/
|
|
292
|
+
│ │ │ ├── <Module_API_Name>.modules-meta.json
|
|
293
|
+
│ │ │ ├── summary.json # Custom module summary (counts, fields, picklists, formulas, records_count)
|
|
294
|
+
│ │ │ ├── duplicate_check_preference.json # Duplicate Check Preferences
|
|
295
|
+
│ │ │ ├── fields/ # Field configurations
|
|
296
|
+
│ │ │ │ └── <Field_API_Name>.fields-meta.json
|
|
297
|
+
│ │ │ ├── layouts/ # Page Layout configurations
|
|
298
|
+
│ │ │ │ ├── <Layout_API_Name>.layouts-meta.json
|
|
299
|
+
│ │ │ │ └── map_dependencies/
|
|
300
|
+
│ │ │ │ └── <Layout_ID>.json
|
|
301
|
+
│ │ │ ├── custom_views/ # Custom View configurations
|
|
302
|
+
│ │ │ │ └── <View_API_Name>.custom_views-meta.json
|
|
303
|
+
│ │ │ ├── record_locking_configurations/ # Record Locking Rules
|
|
304
|
+
│ │ │ │ └── <Config_ID>.json
|
|
305
|
+
│ │ │ ├── related_lists/ # Related List configurations
|
|
306
|
+
│ │ │ │ └── <List_API_Name>.related_lists-meta.json
|
|
307
|
+
│ │ │ ├── tags/ # Module Tags
|
|
308
|
+
│ │ │ │ └── <Tag_ID>.json
|
|
309
|
+
│ │ │ └── workflow_configurations/ # Module Workflow Configurations
|
|
310
|
+
│ │ │ └── <Config_ID>.json
|
|
311
|
+
│ │ ├── profiles/ # Profile Permissions mapping
|
|
312
|
+
│ │ │ └── <Profile_Name>.profiles-meta.json
|
|
313
|
+
│ │ └── roles/ # Role Hierarchy definitions
|
|
314
|
+
│ │ └── <Role_Name>.roles-meta.json
|
|
315
|
+
│ └── functions/ # Custom Deluge function files (.ds)
|
|
316
|
+
│ ├── functions.json # Complete list of extracted functions
|
|
317
|
+
│ ├── standalone/ # Deluge scripts sorted by namespace
|
|
318
|
+
│ │ └── standalone.<Function_API_Name>.ds
|
|
319
|
+
│ ├── automation/
|
|
320
|
+
│ ├── button/
|
|
321
|
+
│ ├── related_list/
|
|
322
|
+
│ ├── schedule/
|
|
323
|
+
│ └── salessignals/
|
|
213
324
|
│
|
|
214
325
|
├── zcrm-project.json # ZCRM CLI project settings config
|
|
215
326
|
├── meta.json # ZCRM resource configuration
|
|
@@ -220,14 +331,20 @@ metadata/
|
|
|
220
331
|
|
|
221
332
|
## 🏢 About Wanas Apps
|
|
222
333
|
|
|
223
|
-
**[Wanas Apps](https://www.wanasapps.com)** is an elite **Zoho Premium Partner** and consulting firm
|
|
334
|
+
**[Wanas Apps](https://www.wanasapps.com)** is an elite **Zoho Premium Partner** and advanced technical consulting firm serving the Middle East and North Africa (MENA) region. We bridge the gap between standard business processes and advanced technical architecture, specializing in end-to-end digital transformation.
|
|
335
|
+
|
|
336
|
+
### 🌟 Our Core Expertise
|
|
337
|
+
- **Zoho Ecosystem Implementation:** Tailored deployments of Zoho CRM, Zoho Books, Zoho Creator, and the wider Zoho suite.
|
|
338
|
+
- **Custom Application Development:** Full-stack, scalable, and serverless solutions built on **Zoho Catalyst**, Node.js, and modern frontends like Angular.
|
|
339
|
+
- **Advanced Integrations & Automations:** We start where off-the-shelf software ends—building robust API gateways, custom webhooks, and complex CRM workflow automations.
|
|
340
|
+
- **Compliance & Localization:** Certified business compliance tools, including seamless native integrations for the Egyptian E-Invoice and E-Receipt systems (ETA).
|
|
224
341
|
|
|
225
|
-
We
|
|
342
|
+
*We don't just implement software; we build custom features that fit seamlessly around your unique business blueprint.*
|
|
226
343
|
|
|
227
344
|
---
|
|
228
345
|
|
|
229
346
|
## ✉️ Support & Feedback
|
|
230
347
|
|
|
231
|
-
If you encounter any issues, have feature requests, or need custom Zoho integrations, please reach out to our team at **support@wanasapps.com**.
|
|
348
|
+
If you encounter any issues, have feature requests, or need custom Zoho integrations, please reach out to our team at **support@wanasapps.com** or visit **[wanasapps.com](https://www.wanasapps.com)**.
|
|
232
349
|
|
|
233
350
|
This project is maintained by Wanas Apps and licensed under the **ISC License**.
|
package/banner.txt
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
__ __ _
|
|
2
|
+
\ \ / /_ _ _ __ __ _ ___ / \ _ __ _ __ ___
|
|
3
|
+
\ \ /\ / / _` | '_ \ / _` / __| / _ \ | '_ \| '_ \/ __|
|
|
4
|
+
\ V V / (_| | | | | (_| \__ \ / ___ \| |_) | |_) \__ \
|
|
5
|
+
\_/\_/ \__,_|_| |_|\__,_|___/ /_/ \_\ .__/| .__/|___/
|
|
6
|
+
|_| |_|
|
|
7
|
+
For Support Contact us on support@wanasapps.com
|
|
8
|
+
www.wanasapps.com
|
package/bin/cli.js
CHANGED
|
@@ -8,10 +8,44 @@ const inquirer = require('inquirer').default || require('inquirer');
|
|
|
8
8
|
const http = require('http');
|
|
9
9
|
const url = require('url');
|
|
10
10
|
const path = require('path');
|
|
11
|
-
const { exec } = require('child_process');
|
|
11
|
+
const { exec, spawn } = require('child_process');
|
|
12
12
|
const configStore = require('../src/utils/configStore');
|
|
13
13
|
const authService = require('../src/services/authService');
|
|
14
14
|
const crmMetadataService = require('../src/services/metadata/crmMetadataService');
|
|
15
|
+
const auditLogService = require('../src/services/auditLogService');
|
|
16
|
+
const skillGenerator = require('../src/utils/skillGenerator');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Opens a URL in the user's default browser in a cross-platform-safe way.
|
|
20
|
+
* On Windows, `start` is a cmd builtin whose first quoted token is the window
|
|
21
|
+
* title, so an empty "" placeholder is required before the URL. On macOS/Linux
|
|
22
|
+
* the URL is passed as a separate argv entry (no shell) to avoid metacharacter
|
|
23
|
+
* issues. All failures are swallowed — the URL is also printed for manual use.
|
|
24
|
+
* @param {string} targetUrl - The URL to open.
|
|
25
|
+
*/
|
|
26
|
+
function openBrowser(targetUrl) {
|
|
27
|
+
try {
|
|
28
|
+
if (process.platform === 'win32') {
|
|
29
|
+
// Quotes protect the '&' query separators; "" is the required title arg.
|
|
30
|
+
exec(`start "" "${targetUrl.replace(/"/g, '%22')}"`, () => {});
|
|
31
|
+
} else {
|
|
32
|
+
const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
33
|
+
const child = spawn(cmd, [targetUrl], { stdio: 'ignore', detached: true });
|
|
34
|
+
child.on('error', () => {});
|
|
35
|
+
child.unref();
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
// User can still click the URL printed to the console.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// A short, prominent reminder of the core guarantee: the tool is strictly
|
|
43
|
+
// read-only and never modifies Zoho CRM. By default it pulls schema/metadata
|
|
44
|
+
// only; reading record data is opt-in (pull --with-counts) or explicit (audit).
|
|
45
|
+
const READ_ONLY_NOTICE =
|
|
46
|
+
'\x1b[1;33mℹ Note:\x1b[0m This tool is \x1b[1mread-only\x1b[0m — it \x1b[1mnever creates, updates, or deletes\x1b[0m anything\n' +
|
|
47
|
+
' in Zoho CRM. By default it extracts \x1b[1mschema/metadata only\x1b[0m (record counts and audit\n' +
|
|
48
|
+
' logs are read only when you explicitly ask via --with-counts or the audit command).';
|
|
15
49
|
|
|
16
50
|
// Default client credentials for generating Zoho login links
|
|
17
51
|
const DEFAULT_CLIENT_ID = '1000.M1XE1M7CA4VFDDIRANB8E5AKIBQ72U';
|
|
@@ -64,7 +98,8 @@ program
|
|
|
64
98
|
.name('zcrm')
|
|
65
99
|
.description('Zoho CRM V8 Metadata Extractor CLI Utility')
|
|
66
100
|
.version(pkg.version, '-v, --version', 'output the current version')
|
|
67
|
-
.addHelpText('before', getBanner())
|
|
101
|
+
.addHelpText('before', getBanner())
|
|
102
|
+
.addHelpText('after', `\n${READ_ONLY_NOTICE}\n`);
|
|
68
103
|
|
|
69
104
|
/**
|
|
70
105
|
* CLI Callback Server validation & token retrieval flow.
|
|
@@ -176,11 +211,8 @@ function runOAuthServer(clientId, clientSecret, redirectUri, dc) {
|
|
|
176
211
|
console.log(`\x1b[4;34m${authUrl}\x1b[0m`);
|
|
177
212
|
console.log('\x1b[1;33m========================================================================\x1b[0m\n');
|
|
178
213
|
|
|
179
|
-
// Attempt to auto-open in default browser
|
|
180
|
-
|
|
181
|
-
exec(`${launch} "${authUrl.replace(/"/g, '\\"')}"`, (execErr) => {
|
|
182
|
-
// Suppress errors (user can manually click printed link if this fails)
|
|
183
|
-
});
|
|
214
|
+
// Attempt to auto-open in default browser (failures are non-fatal)
|
|
215
|
+
openBrowser(authUrl);
|
|
184
216
|
});
|
|
185
217
|
});
|
|
186
218
|
}
|
|
@@ -381,8 +413,10 @@ program
|
|
|
381
413
|
// REGISTER COMMAND: pull
|
|
382
414
|
program
|
|
383
415
|
.command('pull')
|
|
384
|
-
.description('Pull Zoho CRM metadata and
|
|
416
|
+
.description('Pull Zoho CRM metadata (read-only) and sync it into a local JSON directory structure')
|
|
385
417
|
.option('-o, --output <dir>', 'Output folder path', './metadata')
|
|
418
|
+
.option('-c, --concurrency <n>', 'Max concurrent Zoho API calls (1-25; default 5 — safe for all editions)', '5')
|
|
419
|
+
.option('--with-counts', 'Also fetch per-module record counts (reads record data; ~50 API credits per module)')
|
|
386
420
|
.action(async (options) => {
|
|
387
421
|
console.log(getBanner());
|
|
388
422
|
// Reload tokens
|
|
@@ -394,94 +428,79 @@ program
|
|
|
394
428
|
}
|
|
395
429
|
|
|
396
430
|
const outputDir = path.resolve(options.output);
|
|
431
|
+
let concurrency = parseInt(options.concurrency, 10);
|
|
432
|
+
if (!Number.isFinite(concurrency) || concurrency < 1) concurrency = 5;
|
|
433
|
+
if (concurrency > 25) concurrency = 25;
|
|
434
|
+
|
|
397
435
|
console.log(`\x1b[36mInitializing CRM Metadata Pulling...\x1b[0m`);
|
|
398
|
-
console.log(`Destination Folder: ${outputDir}
|
|
436
|
+
console.log(`Destination Folder: ${outputDir}`);
|
|
437
|
+
console.log(`Concurrency: ${concurrency} simultaneous API call(s)`);
|
|
438
|
+
if (options.withCounts) {
|
|
439
|
+
console.log('\x1b[33mRecord counts: ON\x1b[0m (reads record data; ~50 API credits per module)');
|
|
440
|
+
}
|
|
441
|
+
console.log('');
|
|
399
442
|
|
|
400
|
-
let currentModule = 0;
|
|
401
|
-
let totalModules = 0;
|
|
402
|
-
let currentFunction = 0;
|
|
403
|
-
let totalFunctions = 0;
|
|
404
443
|
let inModulesPhase = false;
|
|
405
444
|
let inFunctionsPhase = false;
|
|
406
445
|
|
|
446
|
+
// Finalizes an in-progress progress bar line with a success message.
|
|
447
|
+
const finishPhaseLine = (text) => {
|
|
448
|
+
const readline = require('readline');
|
|
449
|
+
readline.clearLine(process.stdout, 0);
|
|
450
|
+
readline.cursorTo(process.stdout, 0);
|
|
451
|
+
console.log(`\x1b[32m✔\x1b[0m ${text}`);
|
|
452
|
+
};
|
|
453
|
+
|
|
407
454
|
try {
|
|
408
455
|
await crmMetadataService.extract((stage, message, details = {}) => {
|
|
409
456
|
switch (stage) {
|
|
410
457
|
case 'FETCH_ORG':
|
|
411
|
-
console.log(`\x1b[34mℹ\x1b[0m ${message}`);
|
|
412
|
-
break;
|
|
413
|
-
case 'FETCH_ORG_SUCCESS':
|
|
414
|
-
console.log(`\x1b[32m✔\x1b[0m ${message}`);
|
|
415
|
-
break;
|
|
416
458
|
case 'FETCH_MODULES':
|
|
417
459
|
console.log(`\x1b[34mℹ\x1b[0m ${message}`);
|
|
418
460
|
break;
|
|
461
|
+
case 'FETCH_ORG_SUCCESS':
|
|
419
462
|
case 'FETCH_MODULES_SUCCESS':
|
|
420
|
-
console.log(`\x1b[32m✔\x1b[0m ${message}`);
|
|
421
|
-
break;
|
|
422
|
-
case 'MODULE_START':
|
|
423
|
-
if (!inModulesPhase) {
|
|
424
|
-
console.log(`\n\x1b[1mExtracting Modules Metadata:\x1b[0m`);
|
|
425
|
-
inModulesPhase = true;
|
|
426
|
-
}
|
|
427
|
-
currentModule = details.index;
|
|
428
|
-
totalModules = details.total;
|
|
429
|
-
drawProgress(currentModule - 1, totalModules, 'Modules', `Extracting ${details.module}...`);
|
|
430
|
-
break;
|
|
431
|
-
case 'MODULE_SUCCESS':
|
|
432
|
-
if (currentModule === totalModules) {
|
|
433
|
-
const readline = require('readline');
|
|
434
|
-
readline.clearLine(process.stdout, 0);
|
|
435
|
-
readline.cursorTo(process.stdout, 0);
|
|
436
|
-
console.log(`\x1b[32m✔\x1b[0m All modules metadata extracted successfully.`);
|
|
437
|
-
inModulesPhase = false;
|
|
438
|
-
} else {
|
|
439
|
-
drawProgress(currentModule, totalModules, 'Modules', `Completed ${details.module || ''}`);
|
|
440
|
-
}
|
|
441
|
-
break;
|
|
442
|
-
case 'FETCH_WEBHOOKS':
|
|
443
|
-
console.log(`\n\x1b[34mℹ\x1b[0m ${message}`);
|
|
444
|
-
break;
|
|
445
463
|
case 'FETCH_WEBHOOKS_SUCCESS':
|
|
446
|
-
console.log(`\x1b[32m✔\x1b[0m ${message}`);
|
|
447
|
-
break;
|
|
448
|
-
case 'FETCH_PROFILES':
|
|
449
|
-
console.log(`\n\x1b[34mℹ\x1b[0m ${message}`);
|
|
450
|
-
break;
|
|
451
464
|
case 'FETCH_PROFILES_SUCCESS':
|
|
452
|
-
console.log(`\x1b[32m✔\x1b[0m ${message}`);
|
|
453
|
-
break;
|
|
454
|
-
case 'FETCH_ROLES':
|
|
455
|
-
console.log(`\n\x1b[34mℹ\x1b[0m ${message}`);
|
|
456
|
-
break;
|
|
457
465
|
case 'FETCH_ROLES_SUCCESS':
|
|
458
466
|
console.log(`\x1b[32m✔\x1b[0m ${message}`);
|
|
459
467
|
break;
|
|
468
|
+
case 'FETCH_WEBHOOKS':
|
|
469
|
+
case 'FETCH_PROFILES':
|
|
470
|
+
case 'FETCH_ROLES':
|
|
460
471
|
case 'FETCH_FUNCTIONS':
|
|
461
472
|
console.log(`\n\x1b[34mℹ\x1b[0m ${message}`);
|
|
462
473
|
break;
|
|
463
|
-
case '
|
|
464
|
-
console.log(`\x1b[
|
|
474
|
+
case 'MODULES_START':
|
|
475
|
+
console.log(`\n\x1b[1mExtracting Modules Metadata:\x1b[0m`);
|
|
476
|
+
inModulesPhase = true;
|
|
477
|
+
if (!details.total) {
|
|
478
|
+
finishPhaseLine('No modules to extract.');
|
|
479
|
+
inModulesPhase = false;
|
|
480
|
+
}
|
|
465
481
|
break;
|
|
466
|
-
case '
|
|
467
|
-
if (
|
|
468
|
-
|
|
469
|
-
|
|
482
|
+
case 'MODULE_PROGRESS':
|
|
483
|
+
if (details.completed >= details.total) {
|
|
484
|
+
finishPhaseLine(`All ${details.total} modules processed${details.removed ? '' : ''}.`);
|
|
485
|
+
inModulesPhase = false;
|
|
486
|
+
} else {
|
|
487
|
+
drawProgress(details.completed, details.total, 'Modules', `${details.module || ''}`);
|
|
470
488
|
}
|
|
471
|
-
currentFunction = details.index;
|
|
472
|
-
totalFunctions = details.total;
|
|
473
|
-
drawProgress(currentFunction - 1, totalFunctions, 'Functions', `Downloading ${details.name}...`);
|
|
474
489
|
break;
|
|
475
|
-
case '
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
490
|
+
case 'FUNCTIONS_START':
|
|
491
|
+
console.log(`\n\x1b[1mExtracting Custom Functions Deluge Scripts:\x1b[0m`);
|
|
492
|
+
inFunctionsPhase = true;
|
|
493
|
+
if (!details.total) {
|
|
494
|
+
finishPhaseLine('No custom functions found.');
|
|
495
|
+
inFunctionsPhase = false;
|
|
496
|
+
}
|
|
497
|
+
break;
|
|
498
|
+
case 'FUNCTION_PROGRESS':
|
|
499
|
+
if (details.completed >= details.total) {
|
|
500
|
+
finishPhaseLine('All custom function deluge scripts extracted.');
|
|
482
501
|
inFunctionsPhase = false;
|
|
483
502
|
} else {
|
|
484
|
-
drawProgress(
|
|
503
|
+
drawProgress(details.completed, details.total, 'Functions', `${details.name || ''}`);
|
|
485
504
|
}
|
|
486
505
|
break;
|
|
487
506
|
case 'FAILURE':
|
|
@@ -500,8 +519,9 @@ program
|
|
|
500
519
|
console.log(` \x1b[1mTotal Layouts:\x1b[0m \x1b[32m${details.totalLayouts || 0}\x1b[0m`);
|
|
501
520
|
console.log(` \x1b[1mRelated Lists:\x1b[0m \x1b[35m${details.totalRelatedLists || 0}\x1b[0m (saved inside crm/meta/modules/)`);
|
|
502
521
|
console.log(` \x1b[1mCustom Views:\x1b[0m \x1b[35m${details.totalCustomViews || 0}\x1b[0m (saved inside crm/meta/modules/)`);
|
|
522
|
+
console.log(` \x1b[1mStale Files Removed:\x1b[0m \x1b[33m${details.removedStale || 0}\x1b[0m (deleted on Zoho CRM since last pull)`);
|
|
503
523
|
console.log(` \x1b[1mOutput Directory:\x1b[0m \x1b[34m${outputDir}\x1b[0m`);
|
|
504
|
-
|
|
524
|
+
|
|
505
525
|
if (details.errors && details.errors.length > 0) {
|
|
506
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`);
|
|
507
527
|
}
|
|
@@ -513,7 +533,7 @@ program
|
|
|
513
533
|
}
|
|
514
534
|
break;
|
|
515
535
|
}
|
|
516
|
-
}, outputDir);
|
|
536
|
+
}, outputDir, { concurrency, withCounts: !!options.withCounts });
|
|
517
537
|
|
|
518
538
|
process.exit(0);
|
|
519
539
|
} catch (error) {
|
|
@@ -522,6 +542,40 @@ program
|
|
|
522
542
|
}
|
|
523
543
|
});
|
|
524
544
|
|
|
545
|
+
// REGISTER COMMAND: audit
|
|
546
|
+
program
|
|
547
|
+
.command('audit')
|
|
548
|
+
.description('Export the Zoho CRM audit log (asynchronous job)')
|
|
549
|
+
.option('-f, --filter <path>', 'Path to a JSON file containing the audit_log_export criteria')
|
|
550
|
+
.action(async (options) => {
|
|
551
|
+
console.log(getBanner());
|
|
552
|
+
// This command DOES read/export audit-log data, so it gets its own honest
|
|
553
|
+
// notice rather than the generic "metadata only / no data access" one.
|
|
554
|
+
console.log(
|
|
555
|
+
'\x1b[1;33mℹ Note:\x1b[0m This exports your Zoho CRM \x1b[1maudit-log data\x1b[0m (read-only).\n' +
|
|
556
|
+
' It never creates, updates, or deletes anything in Zoho CRM.\n'
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
// Ensure user is authenticated
|
|
560
|
+
authService.loadTokens();
|
|
561
|
+
if (!authService.isAuthenticated()) {
|
|
562
|
+
console.error('\n❌ You are not logged in. Please run `zcrm login` first.');
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
try {
|
|
567
|
+
await authService.getAccessToken(); // Refresh if necessary
|
|
568
|
+
await auditLogService.exportAuditLog(options.filter);
|
|
569
|
+
console.log('\n\x1b[1;32m====================================================================\x1b[0m');
|
|
570
|
+
console.log('🎉 \x1b[1;32mAudit Log Export Completed Successfully!\x1b[0m');
|
|
571
|
+
console.log('\x1b[1;32m====================================================================\x1b[0m\n');
|
|
572
|
+
process.exit(0);
|
|
573
|
+
} catch (error) {
|
|
574
|
+
console.error(`\n❌ Audit log export failed: ${error.message}`);
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
525
579
|
// REGISTER COMMAND: llm
|
|
526
580
|
program
|
|
527
581
|
.command('llm')
|
|
@@ -545,6 +599,96 @@ program
|
|
|
545
599
|
}
|
|
546
600
|
});
|
|
547
601
|
|
|
602
|
+
// REGISTER COMMAND: skill
|
|
603
|
+
program
|
|
604
|
+
.command('skill')
|
|
605
|
+
.description('Generate an AI assistant skill/rules file (from llm.md) for your IDE')
|
|
606
|
+
.option('--ide <ide>', 'Target IDE/assistant (claude, cursor, windsurf, copilot, cline, gemini, codex, markdown)')
|
|
607
|
+
.option('-d, --dir <dir>', 'Project directory to write the skill file into', '.')
|
|
608
|
+
.option('-m, --metadata-dir <dir>', 'Path to the extracted metadata, referenced inside the skill', './metadata')
|
|
609
|
+
.option('-f, --force', 'Overwrite an existing skill file without prompting')
|
|
610
|
+
.action(async (options) => {
|
|
611
|
+
console.log(getBanner());
|
|
612
|
+
try {
|
|
613
|
+
let ide = options.ide;
|
|
614
|
+
|
|
615
|
+
// Prompt for the target IDE/assistant when not supplied via --ide.
|
|
616
|
+
if (!ide) {
|
|
617
|
+
const { selected } = await inquirer.prompt([
|
|
618
|
+
{
|
|
619
|
+
type: 'select',
|
|
620
|
+
name: 'selected',
|
|
621
|
+
message: 'Select your IDE / AI assistant:',
|
|
622
|
+
choices: skillGenerator.getChoices()
|
|
623
|
+
}
|
|
624
|
+
]);
|
|
625
|
+
ide = selected;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (!skillGenerator.TARGETS[ide]) {
|
|
629
|
+
console.error(`\n❌ Unknown IDE/AI target: "${ide}".`);
|
|
630
|
+
console.error(` Valid values: ${Object.keys(skillGenerator.TARGETS).join(', ')}`);
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const projectDir = path.resolve(options.dir);
|
|
635
|
+
const packageDir = path.join(__dirname, '..');
|
|
636
|
+
|
|
637
|
+
let result = skillGenerator.generateSkill({
|
|
638
|
+
ide,
|
|
639
|
+
projectDir,
|
|
640
|
+
metadataDir: options.metadataDir,
|
|
641
|
+
packageDir,
|
|
642
|
+
force: options.force
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// Standalone files that already exist require explicit confirmation.
|
|
646
|
+
if (result.status === 'exists') {
|
|
647
|
+
const { overwrite } = await inquirer.prompt([
|
|
648
|
+
{
|
|
649
|
+
type: 'confirm',
|
|
650
|
+
name: 'overwrite',
|
|
651
|
+
message: `File already exists:\n ${result.filePath}\nOverwrite it?`,
|
|
652
|
+
default: false
|
|
653
|
+
}
|
|
654
|
+
]);
|
|
655
|
+
if (!overwrite) {
|
|
656
|
+
console.log('\n\x1b[33mℹ Cancelled. No file was changed.\x1b[0m\n');
|
|
657
|
+
process.exit(0);
|
|
658
|
+
}
|
|
659
|
+
result = skillGenerator.generateSkill({
|
|
660
|
+
ide,
|
|
661
|
+
projectDir,
|
|
662
|
+
metadataDir: options.metadataDir,
|
|
663
|
+
packageDir,
|
|
664
|
+
force: true
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const verb = result.status === 'updated' ? 'Updated' : 'Created';
|
|
669
|
+
console.log('\n\x1b[1;32m====================================================================\x1b[0m');
|
|
670
|
+
console.log(`🧩 \x1b[1;32m${verb} ${skillGenerator.TARGETS[ide].label.split('→')[0].trim()} skill file.\x1b[0m`);
|
|
671
|
+
console.log('\x1b[1;32m====================================================================\x1b[0m');
|
|
672
|
+
console.log(` \x1b[1mFile:\x1b[0m \x1b[36m${result.filePath}\x1b[0m`);
|
|
673
|
+
console.log(` \x1b[1mMetadata root:\x1b[0m \x1b[34m${path.resolve(options.metadataDir)}\x1b[0m`);
|
|
674
|
+
console.log('--------------------------------------------------------------------');
|
|
675
|
+
console.log(` ${READ_ONLY_NOTICE}`);
|
|
676
|
+
|
|
677
|
+
// Helpful nudge if no metadata has been pulled yet.
|
|
678
|
+
const metaIndex = path.join(path.resolve(options.metadataDir), '.zcrm', '.store', 'index.json');
|
|
679
|
+
const fs = require('fs');
|
|
680
|
+
if (!fs.existsSync(metaIndex)) {
|
|
681
|
+
console.log('--------------------------------------------------------------------');
|
|
682
|
+
console.log(` \x1b[33mTip:\x1b[0m No metadata found yet. Run \x1b[36mzcrm pull -o "${options.metadataDir}"\x1b[0m to populate it.`);
|
|
683
|
+
}
|
|
684
|
+
console.log('\x1b[1;32m====================================================================\x1b[0m\n');
|
|
685
|
+
process.exit(0);
|
|
686
|
+
} catch (err) {
|
|
687
|
+
console.error(`\n❌ Skill generation failed: ${err.message}`);
|
|
688
|
+
process.exit(1);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
|
|
548
692
|
// REGISTER COMMAND: dashboard
|
|
549
693
|
program
|
|
550
694
|
.command('dashboard')
|
|
@@ -552,7 +696,11 @@ program
|
|
|
552
696
|
.option('-p, --port <port>', 'Port to run the dashboard on', '3000')
|
|
553
697
|
.action((options) => {
|
|
554
698
|
process.env.PORT = options.port;
|
|
555
|
-
process.env.
|
|
699
|
+
process.env.ZCRM_VERBOSE = 'true';
|
|
700
|
+
// Keep ZCRM_CLI_MODE='true' (set at startup) so the dashboard shares the
|
|
701
|
+
// same global credential/token store as `zcrm login` and `zcrm pull`.
|
|
702
|
+
// Flipping it to 'false' here would desync the already-initialized
|
|
703
|
+
// authService singleton from the web-mode token path.
|
|
556
704
|
console.log(`\x1b[36mLaunching Extractor Dashboard...\x1b[0m`);
|
|
557
705
|
require('../server.js');
|
|
558
706
|
});
|