wanas-zcrm-extractor 1.7.2 → 1.8.1
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 +99 -1
- package/bin/cli.js +362 -1
- package/llm.md +10 -5
- package/package.json +1 -1
- package/public/app.js +434 -434
- package/public/index.html +255 -255
- package/public/style.css +981 -981
- package/server.js +48 -48
- package/src/controllers/authController.js +213 -213
- package/src/controllers/metadataController.js +319 -319
- package/src/middleware/authMiddleware.js +22 -22
- package/src/routes/authRoutes.js +15 -15
- package/src/routes/metadataRoutes.js +23 -23
- package/src/services/functionService.js +427 -0
- package/src/services/metadata/baseMetadataService.js +17 -17
- package/src/services/metadata/creatorMetadataService.js +27 -27
- package/src/services/metadata/crmMetadataService.js +6 -0
- package/src/services/metadata/projectsMetadataService.js +27 -27
- package/src/utils/apiClient.js +30 -0
- package/src/utils/delugeSignature.js +70 -0
- package/src/utils/prettyJson.js +81 -0
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
</p>
|
|
29
29
|
|
|
30
30
|
<p align="center">
|
|
31
|
-
<code>zcrm login</code> · <code>zcrm pull</code> · <code>zcrm skill</code> · <code>zcrm audit</code> · <code>zcrm dashboard</code>
|
|
31
|
+
<code>zcrm login</code> · <code>zcrm pull</code> · <code>zcrm skill</code> · <code>zcrm audit</code> · <code>zcrm fn test</code> · <code>zcrm dashboard</code>
|
|
32
32
|
</p>
|
|
33
33
|
|
|
34
34
|
<!-- Read-only callout. Rendered as a bordered box (a single-cell table) so it
|
|
@@ -139,6 +139,27 @@ npx zcrm pull
|
|
|
139
139
|
|
|
140
140
|
> *On Windows you may need an **Administrator** terminal; on macOS/Linux you may need `sudo` for a global install.*
|
|
141
141
|
|
|
142
|
+
<details>
|
|
143
|
+
<summary><strong>Alternative: install from GitHub Packages (<code>@wanas-apps</code> scope)</strong></summary>
|
|
144
|
+
|
|
145
|
+
<br>
|
|
146
|
+
|
|
147
|
+
The same release is also published to **GitHub Packages** as the scoped package **`@wanas-apps/wanas-zcrm-extractor`**. GitHub Packages requires authentication even for public packages, so point the `@wanas-apps` scope at GitHub and authenticate with a GitHub token that has the **`read:packages`** scope:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# ~/.npmrc (or a project-local .npmrc)
|
|
151
|
+
@wanas-apps:registry=https://npm.pkg.github.com
|
|
152
|
+
//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
npm install -g @wanas-apps/wanas-zcrm-extractor
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
> Most users should prefer the public npmjs package above — it needs no token. GitHub Packages is provided for org-internal/CI consumers.
|
|
160
|
+
|
|
161
|
+
</details>
|
|
162
|
+
|
|
142
163
|
---
|
|
143
164
|
|
|
144
165
|
## 🔑 Setup & Registration
|
|
@@ -163,6 +184,8 @@ To authenticate, register a Zoho API client in the Zoho Developer Console:
|
|
|
163
184
|
| [`zcrm pull`](#2-zcrm-pull) | Extract & sync all CRM metadata into a local folder. |
|
|
164
185
|
| [`zcrm skill`](#6-zcrm-skill) | Generate an AI-assistant context file for your IDE. |
|
|
165
186
|
| [`zcrm audit`](#7-zcrm-audit) | Export the CRM audit log (async job). |
|
|
187
|
+
| [`zcrm fn create`](#9-zcrm-fn--single-function-lifecycle-create--push--pull--test) · [`push`](#9-zcrm-fn--single-function-lifecycle-create--push--pull--test) | ⚠️ Create a Standalone function / push its code on the live CRM (**writes**). |
|
|
188
|
+
| [`zcrm fn test`](#9-zcrm-fn--single-function-lifecycle-create--push--pull--test) · [`pull`](#9-zcrm-fn--single-function-lifecycle-create--push--pull--test) | ⚠️ Execute a function (**not** read-only) · fetch one function's `.ds` (read-only). |
|
|
166
189
|
| [`zcrm dashboard`](#3-zcrm-dashboard) | Launch the local web explorer dashboard. |
|
|
167
190
|
| [`zcrm whoami`](#4-zcrm-whoami) · [`zcrm llm`](#5-zcrm-llm) · [`zcrm logout`](#8-zcrm-logout) | Session info · AI guide · Sign out. |
|
|
168
191
|
|
|
@@ -345,6 +368,81 @@ zcrm logout
|
|
|
345
368
|
```
|
|
346
369
|
</details>
|
|
347
370
|
|
|
371
|
+
<br>
|
|
372
|
+
|
|
373
|
+
<details>
|
|
374
|
+
<summary><h3>9. <code>zcrm fn</code> — single-function lifecycle (create · push · pull · test)</h3></summary>
|
|
375
|
+
|
|
376
|
+
Work with **one** Deluge function at a time — the full loop: **create** a Standalone function, **push** code edits, **pull** the latest, and **test**-run it.
|
|
377
|
+
|
|
378
|
+
> ⚠️ `fn create`, `fn push`, and `fn test` **write to / execute on** your live Zoho CRM org and are **outside** the read-only guarantee. Each is confirm-gated (use `--force` to skip in CI). `fn pull` is read-only.
|
|
379
|
+
|
|
380
|
+
<h4><code>zcrm fn create <apiName></code> — create a Standalone function (writes)</h4>
|
|
381
|
+
|
|
382
|
+
Creates a new Standalone function on CRM and writes its `.ds` into your metadata tree. With `--from`, it also pushes that file's code; without it, you get an empty stub to edit and `fn push` later.
|
|
383
|
+
|
|
384
|
+
> **Names are lowercased.** Zoho lowercases generated function code signatures, so `zcrm` normalizes api_names to lowercase end-to-end (`My_Fn` → `my_fn`, file `standalone.my_fn.ds`) to keep the function, its code, and the filename consistent. Lookups (`push`/`test`/`pull`) are case-insensitive.
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
zcrm fn create My_Fn # empty stub + local .ds
|
|
388
|
+
zcrm fn create My_Fn --from ./My_Fn.ds # create + push code in one step
|
|
389
|
+
zcrm fn create My_Fn --return-type string # stub with a chosen return type
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
| Option | Description |
|
|
393
|
+
| :--- | :--- |
|
|
394
|
+
| `--from <file.ds>` | Seed the function with code from a local `.ds` (its signature name must match `<apiName>`). |
|
|
395
|
+
| `--return-type <t>` | Return type for an empty stub (default `void`; ignored with `--from`). |
|
|
396
|
+
| `-o, --output <dir>` | Metadata root for the written `.ds` (default `./metadata`). |
|
|
397
|
+
| `--force` | Skip the write-confirmation prompt. |
|
|
398
|
+
|
|
399
|
+
<h4><code>zcrm fn push <file.ds></code> — save local code to CRM (writes)</h4>
|
|
400
|
+
|
|
401
|
+
Reads a local `.ds`, resolves the function from its signature, and uploads the code to the matching **existing** function. Create it first with `fn create` if it doesn't exist yet.
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
zcrm fn push ./metadata/crm/functions/standalone/standalone.My_Fn.ds
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
<h4>⚠️ <code>zcrm fn test <function></code> — run it on the live CRM (NOT read-only)</h4>
|
|
408
|
+
|
|
409
|
+
Unlike every other command, `fn test` **executes** Deluge in your **live** Zoho CRM org (the function editor's "test" action). It can **create/update/delete records, send mail/SMS, and call external APIs** — exactly what the script does. It is therefore an **explicit, opt-in** command, gated behind a confirmation prompt (use `--force` to skip it in CI).
|
|
410
|
+
|
|
411
|
+
`<function>` is either a **local `.ds` file** (runs *your local edits*) or the **api_name** of a Standalone function (fetches & runs the saved code). Only **Standalone** functions can be tested.
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
# Test local edits to a function before saving them in CRM:
|
|
415
|
+
zcrm fn test ./metadata/crm/functions/standalone/standalone.My_Fn.ds
|
|
416
|
+
|
|
417
|
+
# Run a live function with arguments:
|
|
418
|
+
zcrm fn test My_Fn --args '{"Data":"hello"}'
|
|
419
|
+
|
|
420
|
+
# No prompt + raw JSON (automation):
|
|
421
|
+
zcrm fn test My_Fn --force --json
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
| Option | Description |
|
|
425
|
+
| :--- | :--- |
|
|
426
|
+
| `--args <json>` | Inline JSON argument map, e.g. `'{"Data":"hi"}'`. |
|
|
427
|
+
| `--args-file <path>` | JSON file containing the argument map. |
|
|
428
|
+
| `-o, --output <dir>` | Where to save the result artifact — `<dir>/crm/function_tests/` (default `./metadata`). |
|
|
429
|
+
| `--json` | Print Zoho's raw JSON response to stdout. |
|
|
430
|
+
| `--force` | Skip the execution warning + confirmation prompt (CI/automation). |
|
|
431
|
+
|
|
432
|
+
It prints the **return value**, `info` **logs**, every **network/integration call** (with its HTTP status), and execution **metrics**, and saves the full response to `<output>/crm/function_tests/<api_name>-<timestamp>.json`. The exit code is non-zero if the run failed.
|
|
433
|
+
|
|
434
|
+
<h4><code>zcrm fn pull <api_name></code> — fetch one function (read-only)</h4>
|
|
435
|
+
|
|
436
|
+
Fetches a single function's Deluge code into `<output>/crm/functions/<namespace>/<namespace>.<api_name>.ds` — the **same path** `zcrm pull` uses — without running a full pull. Strictly read-only; never deletes anything.
|
|
437
|
+
|
|
438
|
+
```bash
|
|
439
|
+
zcrm fn pull My_Fn
|
|
440
|
+
zcrm fn pull My_Fn -o ./metadata
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
> **Typical loop:** `zcrm fn create My_Fn` → edit the `.ds` → `zcrm fn push <file>` → `zcrm fn test My_Fn`. Only **Standalone** functions are supported (the namespace these endpoints resolve).
|
|
444
|
+
</details>
|
|
445
|
+
|
|
348
446
|
---
|
|
349
447
|
|
|
350
448
|
## 📂 Generated Directory Structure
|
package/bin/cli.js
CHANGED
|
@@ -14,7 +14,9 @@ const configStore = require('../src/utils/configStore');
|
|
|
14
14
|
const authService = require('../src/services/authService');
|
|
15
15
|
const crmMetadataService = require('../src/services/metadata/crmMetadataService');
|
|
16
16
|
const auditLogService = require('../src/services/auditLogService');
|
|
17
|
+
const functionService = require('../src/services/functionService');
|
|
17
18
|
const skillGenerator = require('../src/utils/skillGenerator');
|
|
19
|
+
const { formatValue } = require('../src/utils/prettyJson');
|
|
18
20
|
const { addSessionLog, logInfo, shortErrorMessage } = require('../src/utils/logger');
|
|
19
21
|
|
|
20
22
|
// ---------------------------------------------------------------------------
|
|
@@ -25,7 +27,7 @@ const { addSessionLog, logInfo, shortErrorMessage } = require('../src/utils/logg
|
|
|
25
27
|
// and full API error bodies that are not printed to the console. The path is
|
|
26
28
|
// shown if the command fails. (Command args are intentionally NOT logged, to
|
|
27
29
|
// avoid writing secrets like --client-secret / --refresh-token to disk.)
|
|
28
|
-
const LOGGED_COMMANDS = ['login', 'logout', 'whoami', 'status', 'pull', 'audit', 'llm', 'skill'];
|
|
30
|
+
const LOGGED_COMMANDS = ['login', 'logout', 'whoami', 'status', 'pull', 'audit', 'llm', 'skill', 'fn'];
|
|
29
31
|
let debugLogPath = null;
|
|
30
32
|
(function setupDebugLog() {
|
|
31
33
|
try {
|
|
@@ -807,5 +809,364 @@ program
|
|
|
807
809
|
require('../server.js');
|
|
808
810
|
});
|
|
809
811
|
|
|
812
|
+
// ---------------------------------------------------------------------------
|
|
813
|
+
// fn — single-function operations (test / pull)
|
|
814
|
+
// ---------------------------------------------------------------------------
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Colorizes an HTTP status code: green for 2xx, red otherwise.
|
|
818
|
+
* @param {number} code
|
|
819
|
+
* @returns {string}
|
|
820
|
+
*/
|
|
821
|
+
function colorStatus(code) {
|
|
822
|
+
const ok = code >= 200 && code < 300;
|
|
823
|
+
return `${ok ? '\x1b[32m' : '\x1b[1;31m'}${code}\x1b[0m`;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Maps functionService error codes to a short, actionable hint so routine,
|
|
827
|
+
// user-facing failures render as clean CliErrors (concise hint, no "report a
|
|
828
|
+
// bug / contact support" footer). Anything without a known code falls through
|
|
829
|
+
// to the generic unexpected-error handler.
|
|
830
|
+
const FN_ERROR_HINTS = {
|
|
831
|
+
DS_NOT_FOUND: 'Check the path to your .ds file.',
|
|
832
|
+
NO_API_NAME: 'Ensure the .ds starts with a signature like `string standalone.My_Fn()`.',
|
|
833
|
+
FN_NOT_FOUND: 'Check the api_name (case-sensitive), or create it first with `zcrm fn create`.',
|
|
834
|
+
NAME_INVALID: 'Create the function first with `zcrm fn create`, then test it.',
|
|
835
|
+
NOT_STANDALONE: 'The CRM test/authoring actions resolve names in the standalone namespace only.',
|
|
836
|
+
NAME_MISMATCH: 'Rename the function or the .ds so the signature name matches.',
|
|
837
|
+
EMPTY_CODE: 'The function exists but Zoho returned no code for it.',
|
|
838
|
+
CREATE_FAILED: 'The name may already exist, or a field was rejected. Try a different name.',
|
|
839
|
+
PUSH_FAILED: 'Open the .ds in the Zoho editor to check it compiles.',
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Funnels an `fn`-command error: known functionService codes become clean
|
|
844
|
+
* CliErrors; anything else (e.g. a raw API error) goes through fail()'s generic
|
|
845
|
+
* unexpected-error path.
|
|
846
|
+
* @param {Error} err
|
|
847
|
+
*/
|
|
848
|
+
function failFn(err) {
|
|
849
|
+
if (err && err.code && FN_ERROR_HINTS[err.code] && !err.isCliError) {
|
|
850
|
+
fail(new CliError(err.message, FN_ERROR_HINTS[err.code]), 'fn');
|
|
851
|
+
}
|
|
852
|
+
fail(err, 'fn');
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Parse a JSON argument map from a string, enforcing that it is a plain object.
|
|
857
|
+
* Exits with a clean CliError on parse failure or a non-object value.
|
|
858
|
+
* @param {string} raw
|
|
859
|
+
* @param {string} sourceLabel - e.g. '--args' or '--args-file'
|
|
860
|
+
* @returns {object}
|
|
861
|
+
*/
|
|
862
|
+
function parseArgMap(raw, sourceLabel) {
|
|
863
|
+
let parsed;
|
|
864
|
+
try {
|
|
865
|
+
parsed = JSON.parse(raw);
|
|
866
|
+
} catch (e) {
|
|
867
|
+
fail(new CliError(`Invalid ${sourceLabel} JSON: ${e.message}`, 'Pass a JSON object, e.g. \'{"Data":"hi"}\''), 'fn');
|
|
868
|
+
}
|
|
869
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
870
|
+
fail(new CliError(`${sourceLabel} must be a JSON object map, e.g. {"Data":"hi"}.`), 'fn');
|
|
871
|
+
}
|
|
872
|
+
return parsed;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Coerce an interactively-entered (always-string) argument to its declared
|
|
877
|
+
* Deluge type, so a function expecting `int`/`bool` receives a number/boolean.
|
|
878
|
+
* @param {string} value
|
|
879
|
+
* @param {string} type
|
|
880
|
+
*/
|
|
881
|
+
function coerceArg(value, type) {
|
|
882
|
+
const t = (type || '').toLowerCase();
|
|
883
|
+
if (/^(int|integer|long|bigint)$/.test(t)) { const n = parseInt(value, 10); return Number.isNaN(n) ? value : n; }
|
|
884
|
+
if (/^(double|float|decimal|number|percent|currency)$/.test(t)) { const n = Number(value); return Number.isNaN(n) ? value : n; }
|
|
885
|
+
if (/^(bool|boolean)$/.test(t)) return /^(true|yes|y|1)$/i.test(String(value).trim());
|
|
886
|
+
return value;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Confirmation prompt that treats a non-TTY / EOF (e.g. CI without --force) as a
|
|
891
|
+
* decline rather than crashing — so a skipped confirmation cancels cleanly.
|
|
892
|
+
* @param {string} message
|
|
893
|
+
* @returns {Promise<boolean>}
|
|
894
|
+
*/
|
|
895
|
+
async function confirmOrCancel(message) {
|
|
896
|
+
try {
|
|
897
|
+
const { proceed } = await inquirer.prompt([{ type: 'confirm', name: 'proceed', message, default: false }]);
|
|
898
|
+
return proceed;
|
|
899
|
+
} catch (e) {
|
|
900
|
+
return false;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Indents a (possibly multi-line) string by `n` spaces on every line.
|
|
905
|
+
function indentLines(text, n) {
|
|
906
|
+
const pad = ' '.repeat(n);
|
|
907
|
+
return String(text).split('\n').map((l) => pad + l).join('\n');
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Renders a single value beneath a label. JSON values (objects, or strings that
|
|
912
|
+
* parse to JSON) are pretty-printed + syntax-colored on their own indented
|
|
913
|
+
* lines; plain scalars render inline after the label.
|
|
914
|
+
*/
|
|
915
|
+
function printValue(label, value) {
|
|
916
|
+
const { isJson, text } = formatValue(value, 0);
|
|
917
|
+
if (isJson) {
|
|
918
|
+
console.log(` \x1b[1m${label}\x1b[0m`);
|
|
919
|
+
console.log(indentLines(text, 4));
|
|
920
|
+
} else {
|
|
921
|
+
console.log(` \x1b[1m${label}\x1b[0m ${text}`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Renders a `fn test` result (status, return value, info logs, network/
|
|
927
|
+
* integration calls, metrics, saved artifact path) to the console. JSON in the
|
|
928
|
+
* return value or `info` logs is parsed and pretty-printed for readability.
|
|
929
|
+
* @param {object} result - The structured result from functionService.executeTest.
|
|
930
|
+
*/
|
|
931
|
+
function renderTestResult(result) {
|
|
932
|
+
const { resolved, fnResult, savedPath, success } = result;
|
|
933
|
+
const bar = '\x1b[90m──────────────────────────────────────────────────────────────\x1b[0m';
|
|
934
|
+
const badge = success ? '\x1b[1;42;30m SUCCESS \x1b[0m' : '\x1b[1;41;37m FAILED \x1b[0m';
|
|
935
|
+
const m = fnResult.metrics || {};
|
|
936
|
+
|
|
937
|
+
console.log('');
|
|
938
|
+
console.log(`${badge} \x1b[1m🧪 standalone.${resolved.apiName}\x1b[0m`);
|
|
939
|
+
console.log(bar);
|
|
940
|
+
|
|
941
|
+
// Return value (unwrap Zoho's {value,...} envelope; pretty-print JSON).
|
|
942
|
+
let out = fnResult.output;
|
|
943
|
+
if (out && typeof out === 'object' && 'value' in out) out = out.value;
|
|
944
|
+
if (out === null || out === undefined || out === '') {
|
|
945
|
+
console.log(' \x1b[1mReturn\x1b[0m \x1b[90m(empty)\x1b[0m');
|
|
946
|
+
} else {
|
|
947
|
+
printValue('Return', out);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// info logs (with line numbers; JSON values pretty-printed)
|
|
951
|
+
const logs = fnResult.logs || [];
|
|
952
|
+
if (logs.length) {
|
|
953
|
+
console.log(`\n \x1b[1mLogs\x1b[0m \x1b[90m(${logs.length})\x1b[0m`);
|
|
954
|
+
for (const l of logs) {
|
|
955
|
+
const cat = l.category && l.category !== 'info' ? `\x1b[33m${l.category}\x1b[0m ` : '';
|
|
956
|
+
const loc = l.line_number != null ? `\x1b[90mL${l.line_number}\x1b[0m ` : '';
|
|
957
|
+
const { isJson, text } = formatValue(l.value, 0);
|
|
958
|
+
if (isJson) {
|
|
959
|
+
console.log(` ${loc}${cat}`);
|
|
960
|
+
console.log(indentLines(text, 6));
|
|
961
|
+
} else {
|
|
962
|
+
console.log(` ${loc}${cat}${text}`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// network / integration calls
|
|
968
|
+
const net = fnResult.network_logs || [];
|
|
969
|
+
if (net.length) {
|
|
970
|
+
console.log(`\n \x1b[1mNetwork / integration calls\x1b[0m \x1b[90m(${net.length})\x1b[0m`);
|
|
971
|
+
for (const n of net) {
|
|
972
|
+
// invoke_url rows carry http_method; integration_task rows don't — label them TASK.
|
|
973
|
+
const method = (n.http_method || (n.details && n.details.task ? 'TASK' : '—')).toUpperCase().padEnd(5);
|
|
974
|
+
const where = (n.details && (n.details.url || n.details.task)) || n.function_name || '';
|
|
975
|
+
console.log(` \x1b[90m${method}\x1b[0m ${colorStatus(n.status_code)} ${where} \x1b[90m${n.time_taken_in_ms}ms\x1b[0m`);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
console.log(bar);
|
|
980
|
+
console.log(` \x1b[90m${m.statements_executed || 0} statements · ${m.integration_task || 0} integration · ${m.send_mail || 0} mail · ${m.send_sms || 0} sms · ${m.time_taken_in_ms || 0}ms\x1b[0m`);
|
|
981
|
+
if (savedPath) console.log(` \x1b[90mSaved → ${savedPath}\x1b[0m`);
|
|
982
|
+
console.log('');
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const fnCmd = program
|
|
986
|
+
.command('fn')
|
|
987
|
+
.description('Work with a single Deluge function (create / push / pull / test)');
|
|
988
|
+
|
|
989
|
+
// fn test — EXECUTES code; outside the read-only guarantee.
|
|
990
|
+
fnCmd
|
|
991
|
+
.command('test <function>')
|
|
992
|
+
.description('Execute a Standalone Deluge function on the live CRM (EXECUTES code — NOT read-only)')
|
|
993
|
+
.option('--args <json>', 'Inline JSON map of arguments, e.g. --args \'{"Data":"hi"}\'')
|
|
994
|
+
.option('--args-file <path>', 'Path to a JSON file containing the arguments map')
|
|
995
|
+
.option('-o, --output <dir>', 'Result folder to save the test artifact into (its crm/function_tests/ subfolder)', './metadata')
|
|
996
|
+
.option('--json', 'Print the raw Zoho JSON response to stdout (artifact still saved)')
|
|
997
|
+
.option('--force', 'Skip the execution warning + confirmation prompt (for automation/CI)')
|
|
998
|
+
.action(async (functionArg, options) => {
|
|
999
|
+
console.log(getBanner());
|
|
1000
|
+
authService.loadTokens();
|
|
1001
|
+
if (!authService.isAuthenticated()) {
|
|
1002
|
+
fail(new CliError('Not authenticated. Please log in first.', 'Run: zcrm login'), 'fn');
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
try {
|
|
1006
|
+
// 1. Resolve the script + identity (local .ds or live api_name), then the
|
|
1007
|
+
// standalone-only guard (single source of truth; mapped to a clean
|
|
1008
|
+
// CliError by failFn). Both run before any execution.
|
|
1009
|
+
logInfo(`fn test target: ${functionArg}`, 'cli');
|
|
1010
|
+
const resolved = await functionService.resolveTarget(functionArg);
|
|
1011
|
+
functionService.assertStandalone(resolved);
|
|
1012
|
+
|
|
1013
|
+
// 2. Resolve arguments: --args / --args-file / interactive prompt / none.
|
|
1014
|
+
let args = {};
|
|
1015
|
+
if (options.args) {
|
|
1016
|
+
args = parseArgMap(options.args, '--args');
|
|
1017
|
+
} else if (options.argsFile) {
|
|
1018
|
+
const fsx = require('fs');
|
|
1019
|
+
const p = path.resolve(options.argsFile);
|
|
1020
|
+
if (!fsx.existsSync(p)) fail(new CliError(`--args-file not found: ${p}`), 'fn');
|
|
1021
|
+
args = parseArgMap(fsx.readFileSync(p, 'utf8'), '--args-file');
|
|
1022
|
+
} else if (resolved.args.length && process.stdin.isTTY && !options.force) {
|
|
1023
|
+
try {
|
|
1024
|
+
const answers = await inquirer.prompt(resolved.args.map((a) => ({
|
|
1025
|
+
type: 'input',
|
|
1026
|
+
name: a.name,
|
|
1027
|
+
message: `${a.name} \x1b[90m(${a.type})\x1b[0m:`,
|
|
1028
|
+
})));
|
|
1029
|
+
// Coerce each answer to its declared type (prompts return strings).
|
|
1030
|
+
args = {};
|
|
1031
|
+
for (const a of resolved.args) args[a.name] = coerceArg(answers[a.name], a.type);
|
|
1032
|
+
} catch (e) {
|
|
1033
|
+
console.log('\n\x1b[33mℹ Cancelled.\x1b[0m\n');
|
|
1034
|
+
process.exit(0);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// 3. Confirmation gate — fn test EXECUTES code in the live org.
|
|
1039
|
+
if (!options.force) {
|
|
1040
|
+
console.log('\x1b[1;33m⚠ fn test EXECUTES this Deluge in your LIVE Zoho CRM org.\x1b[0m It is \x1b[1mNOT\x1b[0m a dry run —');
|
|
1041
|
+
console.log(' it can create/update/delete records, send mail/SMS, and call external APIs.');
|
|
1042
|
+
console.log(` Function: \x1b[36mstandalone.${resolved.apiName}\x1b[0m`);
|
|
1043
|
+
if (!(await confirmOrCancel('Proceed?'))) {
|
|
1044
|
+
console.log('\n\x1b[33mℹ Cancelled. Nothing was executed.\x1b[0m\n');
|
|
1045
|
+
process.exit(0);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// 4. Execute, render, and exit with the run's success status.
|
|
1050
|
+
const result = await functionService.executeTest({ resolved, args, outputDir: options.output });
|
|
1051
|
+
|
|
1052
|
+
if (options.json) {
|
|
1053
|
+
console.log(JSON.stringify(result.response, null, 2));
|
|
1054
|
+
if (result.savedPath) console.log(`\x1b[90mSaved: ${result.savedPath}\x1b[0m`);
|
|
1055
|
+
} else {
|
|
1056
|
+
renderTestResult(result);
|
|
1057
|
+
}
|
|
1058
|
+
process.exit(result.success ? 0 : 1);
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
failFn(err);
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
// fn pull — read-only single-function code fetch.
|
|
1065
|
+
fnCmd
|
|
1066
|
+
.command('pull <apiName>')
|
|
1067
|
+
.description('Fetch one function\'s Deluge code into its .ds file (read-only)')
|
|
1068
|
+
.option('-o, --output <dir>', 'Metadata root to write the .ds into', './metadata')
|
|
1069
|
+
.action(async (apiNameArg, options) => {
|
|
1070
|
+
console.log(getBanner());
|
|
1071
|
+
authService.loadTokens();
|
|
1072
|
+
if (!authService.isAuthenticated()) {
|
|
1073
|
+
fail(new CliError('Not authenticated. Please log in first.', 'Run: zcrm login'), 'fn');
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
try {
|
|
1077
|
+
logInfo(`fn pull api_name: ${apiNameArg}`, 'cli');
|
|
1078
|
+
const res = await functionService.pullOne({ apiName: apiNameArg, outputDir: options.output });
|
|
1079
|
+
console.log('\n\x1b[1;32m====================================================================\x1b[0m');
|
|
1080
|
+
console.log('📥 \x1b[1;32mFunction Deluge script pulled successfully!\x1b[0m');
|
|
1081
|
+
console.log('\x1b[1;32m====================================================================\x1b[0m');
|
|
1082
|
+
console.log(` \x1b[1mFunction:\x1b[0m \x1b[36m${res.apiName}\x1b[0m`);
|
|
1083
|
+
console.log(` \x1b[1mCategory:\x1b[0m \x1b[35m${res.category || 'N/A'}\x1b[0m`);
|
|
1084
|
+
console.log(` \x1b[1mSaved To:\x1b[0m \x1b[34m${res.filePath}\x1b[0m`);
|
|
1085
|
+
console.log('\x1b[1;32m====================================================================\x1b[0m\n');
|
|
1086
|
+
process.exit(0);
|
|
1087
|
+
} catch (err) {
|
|
1088
|
+
failFn(err);
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
// fn create — creates a Standalone function on CRM (writes) + writes the .ds locally.
|
|
1093
|
+
fnCmd
|
|
1094
|
+
.command('create <apiName>')
|
|
1095
|
+
.description('Create a Standalone function on the live CRM and write its .ds locally (WRITES to CRM)')
|
|
1096
|
+
.option('--from <file>', 'Seed the new function with code from a local .ds file')
|
|
1097
|
+
.option('--return-type <type>', 'Return type for an empty stub (ignored when --from is used; default void)')
|
|
1098
|
+
.option('-o, --output <dir>', 'Metadata root to write the .ds into', './metadata')
|
|
1099
|
+
.option('--force', 'Skip the write confirmation prompt (for automation/CI)')
|
|
1100
|
+
.action(async (apiNameArg, options) => {
|
|
1101
|
+
console.log(getBanner());
|
|
1102
|
+
authService.loadTokens();
|
|
1103
|
+
if (!authService.isAuthenticated()) {
|
|
1104
|
+
fail(new CliError('Not authenticated. Please log in first.', 'Run: zcrm login'), 'fn');
|
|
1105
|
+
}
|
|
1106
|
+
try {
|
|
1107
|
+
if (!options.force) {
|
|
1108
|
+
console.log('\x1b[1;33m⚠ fn create WRITES to your LIVE Zoho CRM org\x1b[0m — it creates a new Standalone function.');
|
|
1109
|
+
console.log(` Function: \x1b[36mstandalone.${apiNameArg}\x1b[0m${options.from ? ` \x1b[90m(seeded from ${options.from})\x1b[0m` : ''}`);
|
|
1110
|
+
if (!(await confirmOrCancel('Proceed?'))) {
|
|
1111
|
+
console.log('\n\x1b[33mℹ Cancelled. Nothing was created.\x1b[0m\n');
|
|
1112
|
+
process.exit(0);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
logInfo(`fn create: ${apiNameArg}${options.from ? ' from ' + options.from : ''}`, 'cli');
|
|
1116
|
+
const res = await functionService.createFunction({
|
|
1117
|
+
apiName: apiNameArg,
|
|
1118
|
+
returnType: options.returnType,
|
|
1119
|
+
fromFile: options.from,
|
|
1120
|
+
outputDir: options.output,
|
|
1121
|
+
});
|
|
1122
|
+
console.log('\n\x1b[1;32m====================================================================\x1b[0m');
|
|
1123
|
+
console.log('✨ \x1b[1;32mStandalone function created!\x1b[0m');
|
|
1124
|
+
console.log('\x1b[1;32m====================================================================\x1b[0m');
|
|
1125
|
+
console.log(` \x1b[1mFunction:\x1b[0m \x1b[36m${res.apiName}\x1b[0m \x1b[90m(id ${res.id})\x1b[0m`);
|
|
1126
|
+
console.log(` \x1b[1mCode:\x1b[0m ${res.pushed ? 'pushed from your --from file' : 'empty stub'}`);
|
|
1127
|
+
console.log(` \x1b[1mSaved To:\x1b[0m \x1b[34m${res.filePath}\x1b[0m`);
|
|
1128
|
+
console.log('\n \x1b[90mNext: edit the .ds, `zcrm fn push` it, then `zcrm fn test` to run it.\x1b[0m');
|
|
1129
|
+
console.log('\x1b[1;32m====================================================================\x1b[0m\n');
|
|
1130
|
+
process.exit(0);
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
failFn(err);
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
// fn push — pushes a local .ds file's code to its matching live function (writes).
|
|
1137
|
+
fnCmd
|
|
1138
|
+
.command('push <file>')
|
|
1139
|
+
.description('Push (save) a local .ds file\'s code to its matching live function (WRITES to CRM)')
|
|
1140
|
+
.option('--force', 'Skip the write confirmation prompt (for automation/CI)')
|
|
1141
|
+
.action(async (fileArg, options) => {
|
|
1142
|
+
console.log(getBanner());
|
|
1143
|
+
authService.loadTokens();
|
|
1144
|
+
if (!authService.isAuthenticated()) {
|
|
1145
|
+
fail(new CliError('Not authenticated. Please log in first.', 'Run: zcrm login'), 'fn');
|
|
1146
|
+
}
|
|
1147
|
+
try {
|
|
1148
|
+
if (!options.force) {
|
|
1149
|
+
console.log('\x1b[1;33m⚠ fn push WRITES to your LIVE Zoho CRM org\x1b[0m — it overwrites the function\'s saved code.');
|
|
1150
|
+
console.log(` File: \x1b[36m${fileArg}\x1b[0m`);
|
|
1151
|
+
if (!(await confirmOrCancel('Proceed?'))) {
|
|
1152
|
+
console.log('\n\x1b[33mℹ Cancelled. Nothing was changed.\x1b[0m\n');
|
|
1153
|
+
process.exit(0);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
logInfo(`fn push: ${fileArg}`, 'cli');
|
|
1157
|
+
const res = await functionService.pushCode({ file: fileArg });
|
|
1158
|
+
console.log('\n\x1b[1;32m====================================================================\x1b[0m');
|
|
1159
|
+
console.log('⬆️ \x1b[1;32mCode pushed successfully!\x1b[0m');
|
|
1160
|
+
console.log('\x1b[1;32m====================================================================\x1b[0m');
|
|
1161
|
+
console.log(` \x1b[1mFunction:\x1b[0m \x1b[36m${res.apiName}\x1b[0m \x1b[90m(id ${res.id})\x1b[0m`);
|
|
1162
|
+
console.log(` \x1b[1mFrom:\x1b[0m \x1b[34m${res.filePath}\x1b[0m`);
|
|
1163
|
+
console.log(' \x1b[90mTip: run `zcrm fn test` to execute it.\x1b[0m');
|
|
1164
|
+
console.log('\x1b[1;32m====================================================================\x1b[0m\n');
|
|
1165
|
+
process.exit(0);
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
failFn(err);
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
|
|
810
1171
|
// Run parser
|
|
811
1172
|
program.parse(process.argv);
|
package/llm.md
CHANGED
|
@@ -4,11 +4,16 @@ This guide is designed for **Large Language Models (LLMs) and AI agents** to ing
|
|
|
4
4
|
|
|
5
5
|
> ⚠️ **Read-only schema metadata.** This folder describes only the *structure* of a Zoho CRM
|
|
6
6
|
> org — modules, fields, layouts, picklists, related lists, and function source. It contains
|
|
7
|
-
> **no CRM record data**.
|
|
8
|
-
>
|
|
9
|
-
>
|
|
10
|
-
>
|
|
11
|
-
>
|
|
7
|
+
> **no CRM record data**. The `zcrm` **extraction** (`pull`/`audit`/`fn pull`) is **read-only** —
|
|
8
|
+
> it **cannot create, read, update, or delete** records. Use this metadata to look up exact API
|
|
9
|
+
> names, data types, and constraints. Any record-level Deluge/REST code you generate must be
|
|
10
|
+
> deployed and executed by a human inside Zoho.
|
|
11
|
+
>
|
|
12
|
+
> **Exception — the `zcrm fn` commands:** `fn create` (create a Standalone function),
|
|
13
|
+
> `fn push` (upload code), and `fn test` (execute it) all **write to / run on** the live org
|
|
14
|
+
> (they can create/update records, send mail, call APIs), so each is gated behind a confirmation
|
|
15
|
+
> prompt and kept separate from the read-only guarantee. `fn pull` (fetch one function's code)
|
|
16
|
+
> stays read-only. All `fn` commands operate on **Standalone** functions only.
|
|
12
17
|
|
|
13
18
|
---
|
|
14
19
|
|