wanas-zcrm-extractor 1.7.2 → 1.8.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
@@ -28,7 +28,7 @@
28
28
  </p>
29
29
 
30
30
  <p align="center">
31
- <code>zcrm login</code> &nbsp;·&nbsp; <code>zcrm pull</code> &nbsp;·&nbsp; <code>zcrm skill</code> &nbsp;·&nbsp; <code>zcrm audit</code> &nbsp;·&nbsp; <code>zcrm dashboard</code>
31
+ <code>zcrm login</code> &nbsp;·&nbsp; <code>zcrm pull</code> &nbsp;·&nbsp; <code>zcrm skill</code> &nbsp;·&nbsp; <code>zcrm audit</code> &nbsp;·&nbsp; <code>zcrm fn test</code> &nbsp;·&nbsp; <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,79 @@ 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 &lt;apiName&gt;</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
+ ```bash
385
+ zcrm fn create My_Fn # empty stub + local .ds
386
+ zcrm fn create My_Fn --from ./My_Fn.ds # create + push code in one step
387
+ zcrm fn create My_Fn --return-type string # stub with a chosen return type
388
+ ```
389
+
390
+ | Option | Description |
391
+ | :--- | :--- |
392
+ | `--from <file.ds>` | Seed the function with code from a local `.ds` (its signature name must match `<apiName>`). |
393
+ | `--return-type <t>` | Return type for an empty stub (default `void`; ignored with `--from`). |
394
+ | `-o, --output <dir>` | Metadata root for the written `.ds` (default `./metadata`). |
395
+ | `--force` | Skip the write-confirmation prompt. |
396
+
397
+ <h4><code>zcrm fn push &lt;file.ds&gt;</code> — save local code to CRM (writes)</h4>
398
+
399
+ 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.
400
+
401
+ ```bash
402
+ zcrm fn push ./metadata/crm/functions/standalone/standalone.My_Fn.ds
403
+ ```
404
+
405
+ <h4>⚠️ <code>zcrm fn test &lt;function&gt;</code> — run it on the live CRM (NOT read-only)</h4>
406
+
407
+ 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).
408
+
409
+ `<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.
410
+
411
+ ```bash
412
+ # Test local edits to a function before saving them in CRM:
413
+ zcrm fn test ./metadata/crm/functions/standalone/standalone.My_Fn.ds
414
+
415
+ # Run a live function with arguments:
416
+ zcrm fn test My_Fn --args '{"Data":"hello"}'
417
+
418
+ # No prompt + raw JSON (automation):
419
+ zcrm fn test My_Fn --force --json
420
+ ```
421
+
422
+ | Option | Description |
423
+ | :--- | :--- |
424
+ | `--args <json>` | Inline JSON argument map, e.g. `'{"Data":"hi"}'`. |
425
+ | `--args-file <path>` | JSON file containing the argument map. |
426
+ | `-o, --output <dir>` | Where to save the result artifact — `<dir>/crm/function_tests/` (default `./metadata`). |
427
+ | `--json` | Print Zoho's raw JSON response to stdout. |
428
+ | `--force` | Skip the execution warning + confirmation prompt (CI/automation). |
429
+
430
+ 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.
431
+
432
+ <h4><code>zcrm fn pull &lt;api_name&gt;</code> — fetch one function (read-only)</h4>
433
+
434
+ 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.
435
+
436
+ ```bash
437
+ zcrm fn pull My_Fn
438
+ zcrm fn pull My_Fn -o ./metadata
439
+ ```
440
+
441
+ > **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).
442
+ </details>
443
+
348
444
  ---
349
445
 
350
446
  ## 📂 Generated Directory Structure
package/bin/cli.js CHANGED
@@ -14,6 +14,7 @@ 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');
18
19
  const { addSessionLog, logInfo, shortErrorMessage } = require('../src/utils/logger');
19
20
 
@@ -25,7 +26,7 @@ const { addSessionLog, logInfo, shortErrorMessage } = require('../src/utils/logg
25
26
  // and full API error bodies that are not printed to the console. The path is
26
27
  // shown if the command fails. (Command args are intentionally NOT logged, to
27
28
  // avoid writing secrets like --client-secret / --refresh-token to disk.)
28
- const LOGGED_COMMANDS = ['login', 'logout', 'whoami', 'status', 'pull', 'audit', 'llm', 'skill'];
29
+ const LOGGED_COMMANDS = ['login', 'logout', 'whoami', 'status', 'pull', 'audit', 'llm', 'skill', 'fn'];
29
30
  let debugLogPath = null;
30
31
  (function setupDebugLog() {
31
32
  try {
@@ -807,5 +808,332 @@ program
807
808
  require('../server.js');
808
809
  });
809
810
 
811
+ // ---------------------------------------------------------------------------
812
+ // fn — single-function operations (test / pull)
813
+ // ---------------------------------------------------------------------------
814
+
815
+ /**
816
+ * Colorizes an HTTP status code: green for 2xx, red otherwise.
817
+ * @param {number} code
818
+ * @returns {string}
819
+ */
820
+ function colorStatus(code) {
821
+ const ok = code >= 200 && code < 300;
822
+ return `${ok ? '\x1b[32m' : '\x1b[1;31m'}${code}\x1b[0m`;
823
+ }
824
+
825
+ // Maps functionService error codes to a short, actionable hint so routine,
826
+ // user-facing failures render as clean CliErrors (concise hint, no "report a
827
+ // bug / contact support" footer). Anything without a known code falls through
828
+ // to the generic unexpected-error handler.
829
+ const FN_ERROR_HINTS = {
830
+ DS_NOT_FOUND: 'Check the path to your .ds file.',
831
+ NO_API_NAME: 'Ensure the .ds starts with a signature like `string standalone.My_Fn()`.',
832
+ FN_NOT_FOUND: 'Check the api_name (case-sensitive), or create it first with `zcrm fn create`.',
833
+ NAME_INVALID: 'Create the function first with `zcrm fn create`, then test it.',
834
+ NOT_STANDALONE: 'The CRM test/authoring actions resolve names in the standalone namespace only.',
835
+ NAME_MISMATCH: 'Rename the function or the .ds so the signature name matches.',
836
+ EMPTY_CODE: 'The function exists but Zoho returned no code for it.',
837
+ CREATE_FAILED: 'The name may already exist, or a field was rejected. Try a different name.',
838
+ PUSH_FAILED: 'Open the .ds in the Zoho editor to check it compiles.',
839
+ };
840
+
841
+ /**
842
+ * Funnels an `fn`-command error: known functionService codes become clean
843
+ * CliErrors; anything else (e.g. a raw API error) goes through fail()'s generic
844
+ * unexpected-error path.
845
+ * @param {Error} err
846
+ */
847
+ function failFn(err) {
848
+ if (err && err.code && FN_ERROR_HINTS[err.code] && !err.isCliError) {
849
+ fail(new CliError(err.message, FN_ERROR_HINTS[err.code]), 'fn');
850
+ }
851
+ fail(err, 'fn');
852
+ }
853
+
854
+ /**
855
+ * Parse a JSON argument map from a string, enforcing that it is a plain object.
856
+ * Exits with a clean CliError on parse failure or a non-object value.
857
+ * @param {string} raw
858
+ * @param {string} sourceLabel - e.g. '--args' or '--args-file'
859
+ * @returns {object}
860
+ */
861
+ function parseArgMap(raw, sourceLabel) {
862
+ let parsed;
863
+ try {
864
+ parsed = JSON.parse(raw);
865
+ } catch (e) {
866
+ fail(new CliError(`Invalid ${sourceLabel} JSON: ${e.message}`, 'Pass a JSON object, e.g. \'{"Data":"hi"}\''), 'fn');
867
+ }
868
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
869
+ fail(new CliError(`${sourceLabel} must be a JSON object map, e.g. {"Data":"hi"}.`), 'fn');
870
+ }
871
+ return parsed;
872
+ }
873
+
874
+ /**
875
+ * Coerce an interactively-entered (always-string) argument to its declared
876
+ * Deluge type, so a function expecting `int`/`bool` receives a number/boolean.
877
+ * @param {string} value
878
+ * @param {string} type
879
+ */
880
+ function coerceArg(value, type) {
881
+ const t = (type || '').toLowerCase();
882
+ if (/^(int|integer|long|bigint)$/.test(t)) { const n = parseInt(value, 10); return Number.isNaN(n) ? value : n; }
883
+ if (/^(double|float|decimal|number|percent|currency)$/.test(t)) { const n = Number(value); return Number.isNaN(n) ? value : n; }
884
+ if (/^(bool|boolean)$/.test(t)) return /^(true|yes|y|1)$/i.test(String(value).trim());
885
+ return value;
886
+ }
887
+
888
+ /**
889
+ * Confirmation prompt that treats a non-TTY / EOF (e.g. CI without --force) as a
890
+ * decline rather than crashing — so a skipped confirmation cancels cleanly.
891
+ * @param {string} message
892
+ * @returns {Promise<boolean>}
893
+ */
894
+ async function confirmOrCancel(message) {
895
+ try {
896
+ const { proceed } = await inquirer.prompt([{ type: 'confirm', name: 'proceed', message, default: false }]);
897
+ return proceed;
898
+ } catch (e) {
899
+ return false;
900
+ }
901
+ }
902
+
903
+ /**
904
+ * Renders a `fn test` result (status, return value, info logs, network/
905
+ * integration calls, metrics, saved artifact path) to the console.
906
+ * @param {object} result - The structured result from functionService.executeTest.
907
+ */
908
+ function renderTestResult(result) {
909
+ const { resolved, fnResult, savedPath, success } = result;
910
+ const statusColor = success ? '\x1b[1;32m' : '\x1b[1;31m';
911
+ const m = fnResult.metrics || {};
912
+
913
+ console.log('\n\x1b[1;36m====================================================================\x1b[0m');
914
+ console.log(`🧪 \x1b[1mfn test:\x1b[0m standalone.${resolved.apiName} ${statusColor}${(fnResult.status || 'unknown').toUpperCase()}\x1b[0m`);
915
+ console.log('\x1b[1;36m====================================================================\x1b[0m');
916
+
917
+ // Return value
918
+ const out = fnResult.output;
919
+ let outStr;
920
+ if (out === null || out === undefined) outStr = '\x1b[90m(null)\x1b[0m';
921
+ else if (typeof out === 'object') outStr = JSON.stringify(out.value !== undefined ? out.value : out);
922
+ else outStr = String(out);
923
+ console.log(` \x1b[1mReturn value:\x1b[0m ${outStr}`);
924
+
925
+ // info logs
926
+ const logs = fnResult.logs || [];
927
+ if (logs.length) {
928
+ console.log('\n \x1b[1minfo logs:\x1b[0m');
929
+ for (const l of logs) {
930
+ const tag = l.category && l.category !== 'info' ? `\x1b[33m${l.category}\x1b[0m ` : '';
931
+ console.log(` \x1b[90m[line ${l.line_number}]\x1b[0m ${tag}${l.value}`);
932
+ }
933
+ }
934
+
935
+ // network / integration calls
936
+ const net = fnResult.network_logs || [];
937
+ if (net.length) {
938
+ console.log('\n \x1b[1mNetwork / integration calls:\x1b[0m');
939
+ for (const n of net) {
940
+ // invoke_url rows carry http_method; integration_task rows don't — label them TASK.
941
+ const method = (n.http_method || (n.details && n.details.task ? 'TASK' : '—')).toUpperCase().padEnd(5);
942
+ const where = (n.details && (n.details.url || n.details.task)) || n.function_name || '';
943
+ console.log(` \x1b[90m[${method}]\x1b[0m ${colorStatus(n.status_code)} ${where} \x1b[90m${n.time_taken_in_ms}ms\x1b[0m`);
944
+ }
945
+ }
946
+
947
+ console.log(`\n \x1b[1mMetrics:\x1b[0m ${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`);
948
+ if (savedPath) console.log(` \x1b[1mSaved:\x1b[0m \x1b[34m${savedPath}\x1b[0m`);
949
+ console.log('\x1b[1;36m====================================================================\x1b[0m\n');
950
+ }
951
+
952
+ const fnCmd = program
953
+ .command('fn')
954
+ .description('Work with a single Deluge function (create / push / pull / test)');
955
+
956
+ // fn test — EXECUTES code; outside the read-only guarantee.
957
+ fnCmd
958
+ .command('test <function>')
959
+ .description('Execute a Standalone Deluge function on the live CRM (EXECUTES code — NOT read-only)')
960
+ .option('--args <json>', 'Inline JSON map of arguments, e.g. --args \'{"Data":"hi"}\'')
961
+ .option('--args-file <path>', 'Path to a JSON file containing the arguments map')
962
+ .option('-o, --output <dir>', 'Result folder to save the test artifact into (its crm/function_tests/ subfolder)', './metadata')
963
+ .option('--json', 'Print the raw Zoho JSON response to stdout (artifact still saved)')
964
+ .option('--force', 'Skip the execution warning + confirmation prompt (for automation/CI)')
965
+ .action(async (functionArg, options) => {
966
+ console.log(getBanner());
967
+ authService.loadTokens();
968
+ if (!authService.isAuthenticated()) {
969
+ fail(new CliError('Not authenticated. Please log in first.', 'Run: zcrm login'), 'fn');
970
+ }
971
+
972
+ try {
973
+ // 1. Resolve the script + identity (local .ds or live api_name), then the
974
+ // standalone-only guard (single source of truth; mapped to a clean
975
+ // CliError by failFn). Both run before any execution.
976
+ logInfo(`fn test target: ${functionArg}`, 'cli');
977
+ const resolved = await functionService.resolveTarget(functionArg);
978
+ functionService.assertStandalone(resolved);
979
+
980
+ // 2. Resolve arguments: --args / --args-file / interactive prompt / none.
981
+ let args = {};
982
+ if (options.args) {
983
+ args = parseArgMap(options.args, '--args');
984
+ } else if (options.argsFile) {
985
+ const fsx = require('fs');
986
+ const p = path.resolve(options.argsFile);
987
+ if (!fsx.existsSync(p)) fail(new CliError(`--args-file not found: ${p}`), 'fn');
988
+ args = parseArgMap(fsx.readFileSync(p, 'utf8'), '--args-file');
989
+ } else if (resolved.args.length && process.stdin.isTTY && !options.force) {
990
+ try {
991
+ const answers = await inquirer.prompt(resolved.args.map((a) => ({
992
+ type: 'input',
993
+ name: a.name,
994
+ message: `${a.name} \x1b[90m(${a.type})\x1b[0m:`,
995
+ })));
996
+ // Coerce each answer to its declared type (prompts return strings).
997
+ args = {};
998
+ for (const a of resolved.args) args[a.name] = coerceArg(answers[a.name], a.type);
999
+ } catch (e) {
1000
+ console.log('\n\x1b[33mℹ Cancelled.\x1b[0m\n');
1001
+ process.exit(0);
1002
+ }
1003
+ }
1004
+
1005
+ // 3. Confirmation gate — fn test EXECUTES code in the live org.
1006
+ if (!options.force) {
1007
+ 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 —');
1008
+ console.log(' it can create/update/delete records, send mail/SMS, and call external APIs.');
1009
+ console.log(` Function: \x1b[36mstandalone.${resolved.apiName}\x1b[0m`);
1010
+ if (!(await confirmOrCancel('Proceed?'))) {
1011
+ console.log('\n\x1b[33mℹ Cancelled. Nothing was executed.\x1b[0m\n');
1012
+ process.exit(0);
1013
+ }
1014
+ }
1015
+
1016
+ // 4. Execute, render, and exit with the run's success status.
1017
+ const result = await functionService.executeTest({ resolved, args, outputDir: options.output });
1018
+
1019
+ if (options.json) {
1020
+ console.log(JSON.stringify(result.response, null, 2));
1021
+ if (result.savedPath) console.log(`\x1b[90mSaved: ${result.savedPath}\x1b[0m`);
1022
+ } else {
1023
+ renderTestResult(result);
1024
+ }
1025
+ process.exit(result.success ? 0 : 1);
1026
+ } catch (err) {
1027
+ failFn(err);
1028
+ }
1029
+ });
1030
+
1031
+ // fn pull — read-only single-function code fetch.
1032
+ fnCmd
1033
+ .command('pull <apiName>')
1034
+ .description('Fetch one function\'s Deluge code into its .ds file (read-only)')
1035
+ .option('-o, --output <dir>', 'Metadata root to write the .ds into', './metadata')
1036
+ .action(async (apiNameArg, options) => {
1037
+ console.log(getBanner());
1038
+ authService.loadTokens();
1039
+ if (!authService.isAuthenticated()) {
1040
+ fail(new CliError('Not authenticated. Please log in first.', 'Run: zcrm login'), 'fn');
1041
+ }
1042
+
1043
+ try {
1044
+ logInfo(`fn pull api_name: ${apiNameArg}`, 'cli');
1045
+ const res = await functionService.pullOne({ apiName: apiNameArg, outputDir: options.output });
1046
+ console.log('\n\x1b[1;32m====================================================================\x1b[0m');
1047
+ console.log('📥 \x1b[1;32mFunction Deluge script pulled successfully!\x1b[0m');
1048
+ console.log('\x1b[1;32m====================================================================\x1b[0m');
1049
+ console.log(` \x1b[1mFunction:\x1b[0m \x1b[36m${res.apiName}\x1b[0m`);
1050
+ console.log(` \x1b[1mCategory:\x1b[0m \x1b[35m${res.category || 'N/A'}\x1b[0m`);
1051
+ console.log(` \x1b[1mSaved To:\x1b[0m \x1b[34m${res.filePath}\x1b[0m`);
1052
+ console.log('\x1b[1;32m====================================================================\x1b[0m\n');
1053
+ process.exit(0);
1054
+ } catch (err) {
1055
+ failFn(err);
1056
+ }
1057
+ });
1058
+
1059
+ // fn create — creates a Standalone function on CRM (writes) + writes the .ds locally.
1060
+ fnCmd
1061
+ .command('create <apiName>')
1062
+ .description('Create a Standalone function on the live CRM and write its .ds locally (WRITES to CRM)')
1063
+ .option('--from <file>', 'Seed the new function with code from a local .ds file')
1064
+ .option('--return-type <type>', 'Return type for an empty stub (ignored when --from is used; default void)')
1065
+ .option('-o, --output <dir>', 'Metadata root to write the .ds into', './metadata')
1066
+ .option('--force', 'Skip the write confirmation prompt (for automation/CI)')
1067
+ .action(async (apiNameArg, options) => {
1068
+ console.log(getBanner());
1069
+ authService.loadTokens();
1070
+ if (!authService.isAuthenticated()) {
1071
+ fail(new CliError('Not authenticated. Please log in first.', 'Run: zcrm login'), 'fn');
1072
+ }
1073
+ try {
1074
+ if (!options.force) {
1075
+ console.log('\x1b[1;33m⚠ fn create WRITES to your LIVE Zoho CRM org\x1b[0m — it creates a new Standalone function.');
1076
+ console.log(` Function: \x1b[36mstandalone.${apiNameArg}\x1b[0m${options.from ? ` \x1b[90m(seeded from ${options.from})\x1b[0m` : ''}`);
1077
+ if (!(await confirmOrCancel('Proceed?'))) {
1078
+ console.log('\n\x1b[33mℹ Cancelled. Nothing was created.\x1b[0m\n');
1079
+ process.exit(0);
1080
+ }
1081
+ }
1082
+ logInfo(`fn create: ${apiNameArg}${options.from ? ' from ' + options.from : ''}`, 'cli');
1083
+ const res = await functionService.createFunction({
1084
+ apiName: apiNameArg,
1085
+ returnType: options.returnType,
1086
+ fromFile: options.from,
1087
+ outputDir: options.output,
1088
+ });
1089
+ console.log('\n\x1b[1;32m====================================================================\x1b[0m');
1090
+ console.log('✨ \x1b[1;32mStandalone function created!\x1b[0m');
1091
+ console.log('\x1b[1;32m====================================================================\x1b[0m');
1092
+ console.log(` \x1b[1mFunction:\x1b[0m \x1b[36m${res.apiName}\x1b[0m \x1b[90m(id ${res.id})\x1b[0m`);
1093
+ console.log(` \x1b[1mCode:\x1b[0m ${res.pushed ? 'pushed from your --from file' : 'empty stub'}`);
1094
+ console.log(` \x1b[1mSaved To:\x1b[0m \x1b[34m${res.filePath}\x1b[0m`);
1095
+ console.log('\n \x1b[90mNext: edit the .ds, `zcrm fn push` it, then `zcrm fn test` to run it.\x1b[0m');
1096
+ console.log('\x1b[1;32m====================================================================\x1b[0m\n');
1097
+ process.exit(0);
1098
+ } catch (err) {
1099
+ failFn(err);
1100
+ }
1101
+ });
1102
+
1103
+ // fn push — pushes a local .ds file's code to its matching live function (writes).
1104
+ fnCmd
1105
+ .command('push <file>')
1106
+ .description('Push (save) a local .ds file\'s code to its matching live function (WRITES to CRM)')
1107
+ .option('--force', 'Skip the write confirmation prompt (for automation/CI)')
1108
+ .action(async (fileArg, options) => {
1109
+ console.log(getBanner());
1110
+ authService.loadTokens();
1111
+ if (!authService.isAuthenticated()) {
1112
+ fail(new CliError('Not authenticated. Please log in first.', 'Run: zcrm login'), 'fn');
1113
+ }
1114
+ try {
1115
+ if (!options.force) {
1116
+ console.log('\x1b[1;33m⚠ fn push WRITES to your LIVE Zoho CRM org\x1b[0m — it overwrites the function\'s saved code.');
1117
+ console.log(` File: \x1b[36m${fileArg}\x1b[0m`);
1118
+ if (!(await confirmOrCancel('Proceed?'))) {
1119
+ console.log('\n\x1b[33mℹ Cancelled. Nothing was changed.\x1b[0m\n');
1120
+ process.exit(0);
1121
+ }
1122
+ }
1123
+ logInfo(`fn push: ${fileArg}`, 'cli');
1124
+ const res = await functionService.pushCode({ file: fileArg });
1125
+ console.log('\n\x1b[1;32m====================================================================\x1b[0m');
1126
+ console.log('⬆️ \x1b[1;32mCode pushed successfully!\x1b[0m');
1127
+ console.log('\x1b[1;32m====================================================================\x1b[0m');
1128
+ console.log(` \x1b[1mFunction:\x1b[0m \x1b[36m${res.apiName}\x1b[0m \x1b[90m(id ${res.id})\x1b[0m`);
1129
+ console.log(` \x1b[1mFrom:\x1b[0m \x1b[34m${res.filePath}\x1b[0m`);
1130
+ console.log(' \x1b[90mTip: run `zcrm fn test` to execute it.\x1b[0m');
1131
+ console.log('\x1b[1;32m====================================================================\x1b[0m\n');
1132
+ process.exit(0);
1133
+ } catch (err) {
1134
+ failFn(err);
1135
+ }
1136
+ });
1137
+
810
1138
  // Run parser
811
1139
  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**. It is produced by the `zcrm` extractor CLI, which can **only read
8
- > metadata** and **cannot create, read, update, or delete** anything in Zoho CRM. Use it to look
9
- > up exact API names, data types, and constraints never assume the tooling can run scripts
10
- > against the live CRM. Any Deluge/REST code you generate must be deployed and executed by a
11
- > human inside Zoho.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wanas-zcrm-extractor",
3
- "version": "1.7.2",
3
+ "version": "1.8.0",
4
4
  "description": "Local Zoho CRM V8 Metadata Extractor CLI and Web Dashboard Utility",
5
5
  "main": "server.js",
6
6
  "bin": {