@mcpher/gas-fakes 1.2.23 → 1.2.24

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.RU.md CHANGED
@@ -359,6 +359,7 @@ const getParentsIterator = ({
359
359
  - [Supercharge Your Google Apps Script Caching with GasFlexCache](https://ramblings.mcpher.com/supercharge-your-google-apps-script-caching-with-gasflexcache/)
360
360
  - [Fake-Sandbox for Google Apps Script: Granular controls.](https://ramblings.mcpher.com/fake-sandbox-for-google-apps-script-granular-controls/)
361
361
  - [A Fake-Sandbox for Google Apps Script: Securely Executing Code Generated by Gemini CLI](https://ramblings.mcpher.com/gas-fakes-sandbox/)
362
+ - [Power of Google Apps Script: Building MCP Server Tools for Gemini CLI and Google Antigravity in Google Workspace Automation](https://medium.com/google-cloud/power-of-google-apps-script-building-mcp-server-tools-for-gemini-cli-and-google-antigravity-in-71e754e4b740)
362
363
  - [A New Era for Google Apps Script: Unlocking the Future of Google Workspace Automation with Natural Language](https://medium.com/google-cloud/a-new-era-for-google-apps-script-unlocking-the-future-of-google-workspace-automation-with-natural-a9cecf87b4c6)
363
364
  - [Next-Generation Google Apps Script Development: Leveraging Antigravity and Gemini 3.0](https://medium.com/google-cloud/next-generation-google-apps-script-development-leveraging-antigravity-and-gemini-3-0-c4d5affbc1a8)
364
365
  - [Modern Google Apps Script Workflow Building on the Cloud](https://medium.com/google-cloud/modern-google-apps-script-workflow-building-on-the-cloud-2255dbd32ac3)
package/README.md CHANGED
@@ -178,6 +178,7 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
178
178
  - [Supercharge Your Google Apps Script Caching with GasFlexCache](https://ramblings.mcpher.com/supercharge-your-google-apps-script-caching-with-gasflexcache/)
179
179
  - [Fake-Sandbox for Google Apps Script: Granular controls.](https://ramblings.mcpher.com/fake-sandbox-for-google-apps-script-granular-controls/)
180
180
  - [A Fake-Sandbox for Google Apps Script: Securely Executing Code Generated by Gemini CLI](https://ramblings.mcpher.com/gas-fakes-sandbox/)
181
+ - [Power of Google Apps Script: Building MCP Server Tools for Gemini CLI and Google Antigravity in Google Workspace Automation](https://medium.com/google-cloud/power-of-google-apps-script-building-mcp-server-tools-for-gemini-cli-and-google-antigravity-in-71e754e4b740)
181
182
  - [A New Era for Google Apps Script: Unlocking the Future of Google Workspace Automation with Natural Language](https://medium.com/google-cloud/a-new-era-for-google-apps-script-unlocking-the-future-of-google-workspace-automation-with-natural-a9cecf87b4c6)
182
183
  - [Next-Generation Google Apps Script Development: Leveraging Antigravity and Gemini 3.0](https://medium.com/google-cloud/next-generation-google-apps-script-development-leveraging-antigravity-and-gemini-3-0-c4d5affbc1a8)
183
184
  - [Modern Google Apps Script Workflow Building on the Cloud](https://medium.com/google-cloud/modern-google-apps-script-workflow-building-on-the-cloud-2255dbd32ac3)
package/drive_tools.js ADDED
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+
3
+ const tools = [{ name: "search_files_by_name", schema: {
4
+ description: "Search for files on Google Drive by filename.",
5
+ inputSchema: {
6
+ filename: z.string().describe("The name of the file to search for.")
7
+ }
8
+ }, func: (object = {}) => { const { filename } = object;
9
+ const files = DriveApp.getFilesByName(filename);
10
+ const results = [];
11
+ while (files.hasNext()) {
12
+ const file = files.next();
13
+ results.push({
14
+ name: file.getName(),
15
+ id: file.getId(),
16
+ url: file.getUrl(),
17
+ mimeType: file.getMimeType()
18
+ });
19
+ }
20
+ return results; } }];
package/gas-fakes.js CHANGED
@@ -12,6 +12,9 @@ import dotenv from "dotenv";
12
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
13
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
14
  import { z } from "zod";
15
+ import { parse } from "acorn";
16
+
17
+ import { Auth } from "./src/support/auth.js";
15
18
 
16
19
  // --- Import setup commands ---
17
20
  import {
@@ -22,6 +25,7 @@ import {
22
25
 
23
26
  // sync the version with gas fakes code since they share a package.json
24
27
  import { createRequire } from "node:module";
28
+ import { access } from "node:fs";
25
29
  const require = createRequire(import.meta.url);
26
30
  const pjson = require("./package.json");
27
31
  const VERSION = pjson.version;
@@ -30,8 +34,8 @@ const VERSION = pjson.version;
30
34
  // CONSTANTS & UTILITIES
31
35
  // -----------------------------------------------------------------------------
32
36
 
33
- const CLI_VERSION = "0.0.14";
34
- const MCP_VERSION = "0.0.4";
37
+ const CLI_VERSION = "0.0.15";
38
+ const MCP_VERSION = "0.0.5";
35
39
 
36
40
  /**
37
41
  * Replaces escaped newline characters ('\\n') with actual newlines,
@@ -40,8 +44,16 @@ const MCP_VERSION = "0.0.4";
40
44
  * @returns {string} The processed text.
41
45
  */
42
46
  function normalizeScriptNewlines(text) {
43
- const regex = /("[^"]*")|('[^']*')|(`[^`]*`)|(\\n)/g;
44
- return text.replace(regex, (match, g1, g2, g3, g4) => (g4 ? "\n" : match));
47
+ // const regex = /("[^"]*")|('[^']*')|(`[^`]*`)|(\\n)/g;
48
+ // return text.replace(regex, (match, g1, g2, g3, g4) => (g4 ? "\n" : match));
49
+
50
+ // Updated the above as follows.
51
+ const regex =
52
+ /("(?:[^"\\]|\\.)*")|('(?:[^'\\]|\\.)*')|(`(?:[^`\\]|\\.)*`)|(\/\\[rn]\/[dgimsuy]*)|(\/\*[\s\S]*?\*\/)|(\/\/(?:(?!\\n).)*)|(\\n)/g;
53
+ return text.replace(regex, (match, g1, g2, g3, g4, g5, g6, g7) => {
54
+ if (g7) return "\n";
55
+ return match;
56
+ });
45
57
  }
46
58
 
47
59
  // -----------------------------------------------------------------------------
@@ -189,6 +201,7 @@ async function executeGasScript(options) {
189
201
  useSandbox,
190
202
  sandboxConfig,
191
203
  args,
204
+ gas_library,
192
205
  } = options;
193
206
 
194
207
  let scriptText = filename ? fs.readFileSync(filename, "utf8") : script;
@@ -197,7 +210,7 @@ async function executeGasScript(options) {
197
210
  scriptText = scriptText.replace(/\\\s*?\n/g, "\n");
198
211
  }
199
212
 
200
- const { mainScript, gasScript } = generateExecutionScript({
213
+ let { mainScript, gasScript } = generateExecutionScript({
201
214
  scriptText: normalizeScriptNewlines(scriptText),
202
215
  useSandbox,
203
216
  sandboxConfig,
@@ -216,6 +229,18 @@ async function executeGasScript(options) {
216
229
  configurable: true,
217
230
  });
218
231
 
232
+ if (gas_library && gas_library.length > 0) {
233
+ const libs = gas_library.reduce((ar, { identifier, libScript }) => {
234
+ if (mainScript.includes(identifier)) {
235
+ ar.push(libScript);
236
+ }
237
+ return ar;
238
+ }, []);
239
+ if (libs.length > 0) {
240
+ mainScript = `${libs.join("\n\n")}\n\n${mainScript}`;
241
+ }
242
+ }
243
+
219
244
  let res;
220
245
  if (args) {
221
246
  const gasFunction = new Function("args", mainScript);
@@ -344,7 +369,7 @@ function registerDefaultTool(server) {
344
369
  inputSchema: {
345
370
  filename: z
346
371
  .string()
347
- .optional() // Made optional because script can be provided
372
+ .optional()
348
373
  .describe(`Path to the file containing Google Apps Script.`),
349
374
  script: z
350
375
  .string()
@@ -416,7 +441,7 @@ function registerDefaultTool(server) {
416
441
 
417
442
  const schema2 = {
418
443
  description: [
419
- `Use this to create the tools of the MCP server using Google Apps Script as a new file. If a file \`settings.json\` or \`mcp_config.json\` or and so on for loading the MCP servers include \`--tools\` and the tool file to \`gas-fakes\` MCP server, ask the current client which you want to create a new file including the tools or you want to add the tools to the existing tool file. When a new tool file is created, show how to use the tool file as follows. When the created tool file is \`toolFile.js\`, modify the MCP server setting to \`"mcpServers": { "gas-fakes": { "command": "gas-fakes", "args": [ "mcp", "--tools", "{Absolute path}/toolFile.js" ] } }\`. Don't forget to replace \`{Absolute path}\` with the actual path.`,
444
+ `Use this to create the tools of the MCP server using Google Apps Script as a new file. Only when the prompt that a new tool for the MCP server is required is clearly shown, use this tool. If a file \`settings.json\` or \`mcp_config.json\` or and so on for loading the MCP servers include \`--tools\` and the tool file to \`gas-fakes\` MCP server, ask the current client which you want to create a new file including the tools or you want to add the tools to the existing tool file. When a new tool file is created, show how to use the tool file as follows. When the created tool file is \`toolFile.js\`, modify the MCP server setting to \`"mcpServers": { "gas-fakes": { "command": "gas-fakes", "args": [ "mcp", "--tools", "{Absolute path}/toolFile.js" ] } }\`. Don't forget to replace \`{Absolute path}\` with the actual path.`,
420
445
  ].join("\n"),
421
446
  inputSchema: {
422
447
  filename: z
@@ -439,6 +464,24 @@ function registerDefaultTool(server) {
439
464
  .describe(
440
465
  `Generated Google Apps Script. Please be careful of the following rule. For example, when you generated a Google Apps Script like \`function sample(object) { script }\`, please add \`return sample(object);\` to execute the function. This is a very important point. When the prompt says "Create a function", please create a function and add a line of script for calling the function and returning the response like \`return sample(object);\`. Or, you can also create a Google Apps Script without enclosing the script with \`function sample(object) { script }\`. When you want to return the value, show the value using \`console.log\` in the script, or return the value using \`return\`. The simple sample script is \`const { filename } = object; const files = DriveApp.getFilesByName(filename); const ar = []; while (files.hasNext()) { const file = files.next(); ar.push({ filename: file.getName(), fileId: file.getId() }); } return ar;\` This tool is required to be used for creating tools as a new file. When you want to add more tools to the existing file, add them to an array \`tools\` in the file by following the specification of \`tools\`. If you cannot find the array, please create it as a new file.`
441
466
  ),
467
+ libraries: z
468
+ .array(
469
+ z
470
+ .string()
471
+ .describe(
472
+ [
473
+ `Use this when Google Apps Script libraries are required to be used in the script. The sample values are as follows.`,
474
+ `It supposes that the identity of the library is \`LIB\`.`,
475
+ `- When the library script is the file with path, \`LIB@{filename1}\``,
476
+ `- When the library script is the file direct link, \`LIB@{file URL}\``,
477
+ `- When the library script is the ID (Library project key or library key or script ID or file ID) of the file on Google Drive, \`LIB@{ID}\``,
478
+ ].join("\n")
479
+ )
480
+ )
481
+ .default([])
482
+ .describe(
483
+ `Use this when Google Apps Script libraries are required to be used in the script.`
484
+ ),
442
485
  })
443
486
  .describe("An object for each tool.")
444
487
  )
@@ -457,16 +500,21 @@ function registerDefaultTool(server) {
457
500
  isError: true,
458
501
  };
459
502
  }
460
- const tool_ar = args.tools
461
- .map(
462
- ({ name, schema, gas_script }) =>
463
- `{ name: "${name}", schema: ${schema}, func: (object = {}) => { ${gas_script} } }`
464
- )
465
- .join(", ");
503
+
504
+ const tool_ar = [];
505
+ for (let i = 0; i < args.length; i++) {
506
+ const { name, schema, gas_script, libraries } = args[i];
507
+ tool_ar.push(
508
+ `{ name: "${name}", schema: ${schema}, func: (object = {}) => { ${gas_library}\n\n${gas_script} }, libraries: [${libraries.join(
509
+ ","
510
+ )}] }`
511
+ );
512
+ }
513
+
466
514
  const tool_script = [
467
515
  `import { z } from "zod";`,
468
516
  ``,
469
- `const tools = [${tool_ar}];`,
517
+ `const tools = [${tool_ar.join(", ")}];`,
470
518
  ].join("\n");
471
519
  const absolutePath = path.resolve(process.cwd(), args.filename);
472
520
  fs.writeFileSync(absolutePath, tool_script);
@@ -499,7 +547,9 @@ async function registerCustomTools(server, toolsPath) {
499
547
 
500
548
  if (!tools || tools.length === 0) return;
501
549
 
502
- tools.forEach((tool) => {
550
+ for (let i = 0; i < tools.length; i++) {
551
+ const tool = tools[i];
552
+
503
553
  // Extend the custom tool schema with sandbox options
504
554
  const extendedSchema = { ...tool.schema };
505
555
  extendedSchema.inputSchema = {
@@ -519,7 +569,24 @@ async function registerCustomTools(server, toolsPath) {
519
569
  json: z.any().optional().describe("Advanced sandbox JSON configuration."),
520
570
  };
521
571
 
522
- const originalFuncStr = tool.func.toString();
572
+ let originalFuncStr = tool.func.toString();
573
+ if (tool.libraries && tool.libraries.length > 0) {
574
+ const gas_library = await getLibraries({ libraries: tool.libraries });
575
+
576
+ if (gas_library && gas_library.length > 0) {
577
+ const libs = gas_library.reduce((ar, { identifier, libScript }) => {
578
+ if (originalFuncStr.includes(identifier)) {
579
+ ar.push(libScript);
580
+ }
581
+ return ar;
582
+ }, []);
583
+ if (libs.length > 0) {
584
+ originalFuncStr = `(object = {}) => {\n\n${libs.join(
585
+ "\n\n"
586
+ )}\n\nconst main_gas_fakes = ${originalFuncStr}\n\nreturn main_gas_fakes(object);\n}`;
587
+ }
588
+ }
589
+ }
523
590
 
524
591
  const toolHandler = async (opts) => {
525
592
  // Wrap the original function string to execute it with args
@@ -535,7 +602,7 @@ async function registerCustomTools(server, toolsPath) {
535
602
  };
536
603
 
537
604
  server.registerTool(tool.name, extendedSchema, toolHandler);
538
- });
605
+ }
539
606
  }
540
607
 
541
608
  /**
@@ -609,6 +676,296 @@ function buildSandboxConfig(options) {
609
676
  return undefined;
610
677
  }
611
678
 
679
+ // -----------------------------------------------------------------------------
680
+ // PROCESS GAS LIBRARIES
681
+ // -----------------------------------------------------------------------------
682
+
683
+ /**
684
+ * Helper function to wrap spawn in a Promise.
685
+ * This captures stdout and stderr and resolves or rejects based on the exit code.
686
+ */
687
+ function spawnCommand(command, args) {
688
+ return new Promise((resolve, reject) => {
689
+ const child = spawn(command, args);
690
+ let stdout = "";
691
+ let stderr = "";
692
+
693
+ // Collect data from stdout stream
694
+ child.stdout.on("data", (data) => {
695
+ stdout += data.toString();
696
+ });
697
+
698
+ // Collect data from stderr stream
699
+ child.stderr.on("data", (data) => {
700
+ stderr += data.toString();
701
+ });
702
+
703
+ // Handle command not found or other spawn errors
704
+ child.on("error", (err) => {
705
+ reject(err);
706
+ });
707
+
708
+ // Handle process exit
709
+ child.on("close", (code) => {
710
+ if (code === 0) {
711
+ resolve(stdout.trim());
712
+ } else {
713
+ reject(
714
+ new Error(stderr.trim() || `Command failed with exit code ${code}`)
715
+ );
716
+ }
717
+ });
718
+ });
719
+ }
720
+
721
+ async function checkForGcloudCli() {
722
+ try {
723
+ // args: ['--version']
724
+ await spawnCommand("gcloud", ["--version"]);
725
+ } catch (error) {
726
+ console.error(
727
+ "\n[Error] Google Cloud SDK (gcloud CLI) not found or failed to run."
728
+ );
729
+ console.error("Please install it by following the official instructions:");
730
+ console.error("https://cloud.google.com/sdk/gcloud");
731
+ // Only exit if it's strictly required to stop execution here
732
+ process.exit(1);
733
+ }
734
+ }
735
+
736
+ async function getAccessToken(pattern) {
737
+ if (pattern == 1) {
738
+ // Authorization pattern 1
739
+ const auth = await Auth.setAuth(
740
+ ["https://www.googleapis.com/auth/drive.readonly"],
741
+ null,
742
+ true
743
+ );
744
+ auth.cachedCredential = null;
745
+ return await auth.getAccessToken();
746
+ } else {
747
+ // Authorization pattern 2
748
+ await checkForGcloudCli();
749
+ try {
750
+ const accessToken = await spawnCommand("gcloud", [
751
+ "auth",
752
+ "print-access-token",
753
+ ]);
754
+ return accessToken;
755
+ } catch (error) {
756
+ console.error("\nError obtaining access token:");
757
+ console.error(error.message);
758
+ console.error(
759
+ "Please ensure you are authenticated with gcloud CLI. Run 'gcloud auth application-default login'."
760
+ );
761
+ process.exit(1);
762
+ }
763
+ }
764
+ }
765
+
766
+ async function fetchScriptFileFromGoogleDrive(sourcePath, pattern = 1) {
767
+ try {
768
+ const accessToken = await getAccessToken(pattern);
769
+ const url = `https://www.googleapis.com/drive/v3/files/${sourcePath}/export?mimeType=${encodeURIComponent(
770
+ "application/vnd.google-apps.script+json"
771
+ )}`;
772
+
773
+ const response = await fetch(url, {
774
+ headers: { authorization: `Bearer ${accessToken}` },
775
+ });
776
+
777
+ if (!response.ok) {
778
+ throw new Error(
779
+ `HTTP error ${response.status} fetching Drive ID "${sourcePath}".`
780
+ );
781
+ }
782
+
783
+ const text = await response.json();
784
+ if (text.files && text.files.length > 0) {
785
+ // GAS projects can have multiple files; filter for server-side JS and join them.
786
+ return text.files
787
+ .filter((e) => e.type === "server_js")
788
+ .map((e) => e.source)
789
+ .join("\n\n");
790
+ }
791
+ } catch (err) {
792
+ if (pattern == 1) {
793
+ return await fetchScriptFileFromGoogleDrive(sourcePath, 2);
794
+ }
795
+
796
+ // If it wasn't a Drive ID or auth failed, fall through to error
797
+ throw new Error(
798
+ `Could not retrieve script from "${sourcePath}". Ensure it is a valid path, URL, or Drive ID, and that you are authenticated.`
799
+ );
800
+ }
801
+ }
802
+
803
+ /**
804
+ * Fetches the source code for a library from a local file, a URL, or Google Drive.
805
+ *
806
+ * @param {string} sourcePath - The file path, URL, or Drive File ID of the library.
807
+ * @returns {Promise<string>} The source code of the library.
808
+ * @throws {Error} If the source cannot be found, HTTP fails, or auth fails.
809
+ */
810
+ async function fetchLibrarySource(sourcePath) {
811
+ // 1. Check Local File
812
+ if (fs.existsSync(sourcePath)) {
813
+ return fs.readFileSync(sourcePath, "utf8");
814
+ }
815
+
816
+ // 2. Check URL
817
+ // Note: URL.canParse requires Node.js v18.17.0+ or v20+
818
+ if (URL.canParse(sourcePath)) {
819
+ const response = await fetch(sourcePath);
820
+ if (!response.ok) {
821
+ throw new Error(`HTTP error ${response.status} fetching ${sourcePath}`);
822
+ }
823
+ return await response.text();
824
+ }
825
+
826
+ // 3. Check Google Drive (via gcloud)
827
+ try {
828
+ return await fetchScriptFileFromGoogleDrive(sourcePath);
829
+ } catch (err) {
830
+ throw new Error(
831
+ `No valid source code found for "${sourcePath}". Error message: ${err.message}`
832
+ );
833
+ }
834
+ }
835
+
836
+ /**
837
+ * Wraps raw library source code into a GAS-style namespace using an IIFE.
838
+ * It uses AST parsing to export top-level functions and 'var' variables.
839
+ *
840
+ * @param {string} identifier - The library namespace identifier (e.g., "TableApp").
841
+ * @param {string} source - The raw JavaScript source code.
842
+ * @returns {string} The wrapped, executable JavaScript code.
843
+ */
844
+ function generateLibraryWrapper(identifier, source) {
845
+ try {
846
+ const ast = parse(source, { ecmaVersion: 2020 });
847
+
848
+ // Extract top-level FunctionDeclarations and VariableDeclarations (var only)
849
+ const exportNames = ast.body.reduce((names, node) => {
850
+ if (node.type === "FunctionDeclaration") {
851
+ names.push(node.id.name);
852
+ } else if (node.type === "VariableDeclaration" && node.kind === "var") {
853
+ names.push(...node.declarations.map((d) => d.id.name));
854
+ }
855
+ return names;
856
+ }, []);
857
+
858
+ if (exportNames.length === 0) {
859
+ throw new Error(
860
+ `No top-level functions or var variables found to export in library "${identifier}".`
861
+ );
862
+ }
863
+
864
+ return [
865
+ `var ${identifier} = (function () {`,
866
+ `var ${identifier};`,
867
+ source,
868
+ `\n`,
869
+ `if (this && this.${identifier}) { ${identifier} = this.${identifier}; }`,
870
+ `return { ${exportNames.join(", ")} };`,
871
+ `}).call({});`,
872
+ ].join("\n");
873
+
874
+ // Or, use the following script.
875
+ // return [
876
+ // `var ${identifier} = (function () {`,
877
+ // source,
878
+ // `\n`,
879
+ // `return { ${exportNames.join(", ")} };`,
880
+ // `})();`,
881
+ // ].join("\n");
882
+ } catch (err) {
883
+ console.error(`Error processing library "${identifier}": ${err.message}`);
884
+ process.exit(1);
885
+ }
886
+ }
887
+
888
+ /**
889
+ * Processes library arguments, fetches source code, merges duplicates,
890
+ * and generates namespaced wrapper scripts.
891
+ *
892
+ * This function handles the format "Identifier@Source". If the same Identifier
893
+ * is provided multiple times, the sources are concatenated in order.
894
+ *
895
+ * @param {object} options - The CLI options object.
896
+ * @param {string[]} [options.libraries] - Array of library strings (e.g., ["Lib@./file.js"]).
897
+ * @returns {Promise<string|null>} The combined string of all library scripts, or null if none provided.
898
+ */
899
+ async function getLibraries(options) {
900
+ const { libraries } = options;
901
+
902
+ if (!libraries || !Array.isArray(libraries) || libraries.length === 0) {
903
+ return null;
904
+ }
905
+
906
+ // 1. Parse and Fetch (Concurrent)
907
+ // We map arguments to Promises to fetch all libraries in parallel.
908
+ const fetchPromises = libraries.map(async (libArg) => {
909
+ const splitIndex = libArg.indexOf("@");
910
+
911
+ if (splitIndex === -1) {
912
+ throw new Error(
913
+ `Invalid library format: "${libArg}". Expected format: 'Identifier@Source'.`
914
+ );
915
+ }
916
+
917
+ const identifier = libArg.substring(0, splitIndex).trim();
918
+ const sourcePath = libArg.substring(splitIndex + 1).trim();
919
+
920
+ if (!identifier || !sourcePath) {
921
+ throw new Error(
922
+ `Invalid library argument: "${libArg}". Identifier or Source is missing.`
923
+ );
924
+ }
925
+
926
+ try {
927
+ const source = await fetchLibrarySource(sourcePath);
928
+ return { identifier, source };
929
+ } catch (err) {
930
+ throw new Error(`Failed to load library "${identifier}": ${err.message}`);
931
+ }
932
+ });
933
+
934
+ let fetchedLibs;
935
+ try {
936
+ fetchedLibs = await Promise.all(fetchPromises);
937
+ } catch (err) {
938
+ // If any fetch fails, log the error and exit
939
+ console.error(`Error: ${err.message}`);
940
+ process.exit(1);
941
+ }
942
+
943
+ // 2. Merge libraries with the same Identifier
944
+ // Using a Map to maintain insertion order and group sources
945
+ const mergedLibsMap = new Map();
946
+
947
+ for (const { identifier, source } of fetchedLibs) {
948
+ if (mergedLibsMap.has(identifier)) {
949
+ const existingSource = mergedLibsMap.get(identifier);
950
+ mergedLibsMap.set(identifier, existingSource + "\n\n" + source);
951
+ } else {
952
+ mergedLibsMap.set(identifier, source);
953
+ }
954
+ }
955
+
956
+ // 3. Generate Wrappers
957
+ // Convert the merged sources into wrapped IIFE strings
958
+ const wrappedScripts = [];
959
+ for (const [identifier, source] of mergedLibsMap) {
960
+ wrappedScripts.push({
961
+ identifier,
962
+ libScript: generateLibraryWrapper(identifier, source),
963
+ });
964
+ }
965
+
966
+ return wrappedScripts;
967
+ }
968
+
612
969
  /**
613
970
  * Sets up and runs the command-line interface.
614
971
  */
@@ -618,7 +975,11 @@ async function main() {
618
975
  program
619
976
  .name("gas-fakes")
620
977
  .description("A CLI tool to execute Google Apps Script with fakes/mocks.")
621
- .version(VERSION, "-v, --version", "Display the current version");
978
+ .version(
979
+ VERSION,
980
+ "-v, --version",
981
+ "Display the current version of gas-fakes"
982
+ );
622
983
 
623
984
  // Default command to execute a script
624
985
  program
@@ -661,6 +1022,11 @@ async function main() {
661
1022
  `Arguments for the function of Google Apps Script. Provide it as a JSON string. The name of the argument is "args" as a fixed name. For example, when the function of GAS is \`function sample(args) { script }\`, you can provide the arguments like \`-a '{"key": "value"}'\`.`,
662
1023
  null
663
1024
  )
1025
+ .option(
1026
+ "-l, --libraries <string...>",
1027
+ `Libraries. You can run the Google Apps Script with libraries. When you use 2 libraries "Lib1" and "Lib" which are the identifiers of library, provide '--libraries "Lib1@{filename}" --libraries "Lib2@{file URL}"'.`,
1028
+ null
1029
+ )
664
1030
  .action(async (options) => {
665
1031
  const { filename, script, env, gfsettings } = options;
666
1032
  if (!filename && !script) {
@@ -705,6 +1071,8 @@ async function main() {
705
1071
  }
706
1072
  }
707
1073
 
1074
+ const gas_library = await getLibraries(options);
1075
+
708
1076
  await executeGasScript({
709
1077
  filename,
710
1078
  script,
@@ -713,6 +1081,7 @@ async function main() {
713
1081
  sandboxConfig,
714
1082
  gfSettings: settingsPath,
715
1083
  args,
1084
+ gas_library,
716
1085
  });
717
1086
  });
718
1087
 
package/package.json CHANGED
@@ -7,6 +7,7 @@
7
7
  "@mcpher/gas-flex-cache": "^1.1.3",
8
8
  "@modelcontextprotocol/sdk": "^1.20.2",
9
9
  "@sindresorhus/is": "^7.0.1",
10
+ "acorn": "^8.15.0",
10
11
  "archiver": "^7.0.1",
11
12
  "commander": "^14.0.1",
12
13
  "dotenv": "^17.2.3",
@@ -33,7 +34,7 @@
33
34
  },
34
35
  "name": "@mcpher/gas-fakes",
35
36
  "author": "bruce mcpherson",
36
- "version": "1.2.23",
37
+ "version": "1.2.24",
37
38
  "license": "MIT",
38
39
  "main": "main.js",
39
40
  "description": "A proof of concept implementation of Apps Script Environment on Node",
@@ -42,8 +42,7 @@ class FakeAdvForms {
42
42
  "newFormInfo": [
43
43
  "title",
44
44
  "documentTitle",
45
- "description",
46
- "state"
45
+ "description"
47
46
  ],
48
47
  "newUpdateSettingsRequest": [
49
48
  "settings",
@@ -53,7 +52,7 @@ class FakeAdvForms {
53
52
  "emailCollectionType",
54
53
  "quizSettings"
55
54
  ],
56
- "newQuizSettings" : [
55
+ "newQuizSettings": [
57
56
  "isQuiz"
58
57
  ],
59
58
  "newItem": [
@@ -188,7 +187,7 @@ class FakeAdvForms {
188
187
  "properties",
189
188
  "youtubeUri"
190
189
  ],
191
-
190
+
192
191
  }
193
192
 
194
193
  Reflect.ownKeys(propLists).forEach(p => {
@@ -3,15 +3,18 @@ import { newFakeFormItem } from './fakeformitem.js';
3
3
  import { newFakeFormResponse } from './fakeformresponse.js';
4
4
  import { newFakeGridItem } from './fakegriditem.js';
5
5
  import { newFakeCheckboxGridItem } from './fakecheckboxgriditem.js';
6
+ import { DestinationType } from '../enums/formsenums.js';
6
7
  import { newFakeSectionHeaderItem } from './fakesectionheaderitem.js';
7
8
  import { newFakeScaleItem } from './fakescaleitem.js';
8
- import './formitems.js'; // Import for side effects (item class registration)
9
+ //import './formitems.js'; // Import for side effects (item class registration)
9
10
  import { newFakeMultipleChoiceItem } from './fakemultiplechoiceitem.js';
10
11
  import { newFakeCheckboxItem } from './fakecheckboxitem.js';
11
12
  import { newFakeListItem } from './fakelistitem.js';
12
13
  import { newFakePageBreakItem } from './fakepagebreakitem.js';
13
14
  import { newFakeTextItem } from './faketextitem.js';
14
-
15
+ import { signatureArgs } from '../../support/helpers.js';
16
+ import {Utils} from '../../support/utils.js';
17
+ const { is } = Utils
15
18
  export const newFakeForm = (...args) => {
16
19
  return Proxies.guard(new FakeForm(...args));
17
20
  };
@@ -29,14 +32,28 @@ export class FakeForm {
29
32
  // Store the resource provided at creation as the single source of truth for this instance.
30
33
  this.__id = resource.formId;
31
34
  this.__file = DriveApp.getFileById(this.__id);
32
- // Since the API doesn't allow setting the published state, we'll manage it internally for the fake.
33
- // A new form defaults to accepting responses (true).
34
- this.__publishedState = resource.settings?.state !== 'INACTIVE';
35
+ this.__destinationId = resource.linkedSheetId || null;
36
+ this.__destinationType = this.__destinationId ? DestinationType.SPREADSHEET : null;
35
37
  }
36
38
 
37
39
  get __resource() {
38
40
  return Forms.Form.get(this.__id);
39
41
  }
42
+ get __publishSettings() {
43
+ return this.__resource.publishSettings;
44
+ }
45
+ isAcceptingResponses() {
46
+ return this.__publishSettings.isAcceptingResponses;
47
+ }
48
+ isPublished() {
49
+ return this.__publishSettings.isPublished;
50
+ }
51
+ setPublished(enabled) {
52
+ throw new Error('setPublished is not yet implemented in the fake environment.');
53
+ return this;
54
+ }
55
+
56
+
40
57
  saveAndClose() {
41
58
  // this is a no-op in fake environment since it is stateless
42
59
  }
@@ -244,6 +261,24 @@ export class FakeForm {
244
261
  return this.__addItem(itemResource, newFakeTextItem);
245
262
  }
246
263
 
264
+ /**
265
+ * Gets the ID of the form's response destination.
266
+ * @returns {string | null} The destination ID, or null if no destination is set.
267
+ */
268
+ getDestinationId() {
269
+ return this.__resource.linkedSheetId || null;
270
+ }
271
+
272
+ /**
273
+ * Gets the type of the form's response destination.
274
+ * @returns {import('../enums/formsenums.js').DestinationType | null} The destination type, or null if no destination is set.
275
+ */
276
+ getDestinationType() {
277
+ if (this.getDestinationId()) {
278
+ return DestinationType.SPREADSHEET;
279
+ }
280
+ return null;
281
+ }
247
282
 
248
283
 
249
284
  /**
@@ -338,14 +373,6 @@ export class FakeForm {
338
373
  return responses.sort((a, b) => a.getTimestamp() - b.getTimestamp());
339
374
  }
340
375
 
341
- /**
342
- * Gets whether the form is accepting responses.
343
- * @returns {boolean} true if the form is accepting responses; false otherwise.
344
- */
345
- isPublished() {
346
- // ACTIVE is the state for accepting responses. The default if not set is ACTIVE.
347
- return this.__publishedState;
348
- }
349
376
 
350
377
  /**
351
378
  * Sets the name of the form file in Google Drive.
@@ -415,7 +442,32 @@ export class FakeForm {
415
442
  return itemToMove;
416
443
  }
417
444
  /**
418
- * Sets whether the form is accepting responses.
445
+ * Unlinks the form from its response destination.
446
+ * @returns {FakeForm} The form, for chaining.
447
+ */
448
+ removeDestination() {
449
+ // This is not supported by the REST API, so we manage it internally for the fake.
450
+ this.__destinationId = null;
451
+ this.__destinationType = null;
452
+ return this;
453
+ }
454
+ /**
455
+ * Sets the destination for form responses.
456
+ * @param {import('../enums/formsenums.js').DestinationType} type The type of destination.
457
+ * @param {string} id The ID of the destination (spreadsheet ID).
458
+ * @returns {FakeForm} The form, for chaining.
459
+ */
460
+ setDestination(type, id) {
461
+ if (type !== DestinationType.SPREADSHEET) {
462
+ throw new Error('Only SPREADSHEET destination type is supported.');
463
+ }
464
+ // This is not supported by the REST API, so we manage it internally for the fake.
465
+ this.__destinationId = id;
466
+ this.__destinationType = DestinationType.SPREADSHEET;
467
+ return this;
468
+ }
469
+ /**
470
+ * Sets whether the form is published responses.
419
471
  * @param {boolean} enabled true if the form should accept responses; false otherwise.
420
472
  * @returns {FakeForm} The form, for chaining.
421
473
  */
@@ -423,6 +475,7 @@ export class FakeForm {
423
475
  throw new Error('setPublished is not yet implemented in the fake environment.');
424
476
  }
425
477
 
478
+
426
479
  __update(updateRequest) {
427
480
  const batchRequest = Forms.newBatchUpdateFormRequest()
428
481
  .setRequests([updateRequest])
@@ -430,6 +483,16 @@ export class FakeForm {
430
483
  return this;
431
484
  }
432
485
 
486
+ /**
487
+ * Sets whether the form is accepting responses.
488
+ * @param {boolean} enabled true if the form should accept responses; false otherwise.
489
+ * @returns {FakeForm} The form, for chaining.
490
+ */
491
+ setAcceptingResponses(enabled) {
492
+ // The REST API does not expose a way to set this. The fake will manage it internally.
493
+ throw new Error('setAcceptingResponses is not yet implemented in the fake environment.');
494
+ }
495
+
433
496
  /**
434
497
  * Sets the title of the form.
435
498
  * @param {string} title The new title for the form.
@@ -481,10 +544,16 @@ export class FakeForm {
481
544
  * shorten url no longer supported by google
482
545
  * @returns {string} The form URL.
483
546
  */
484
- shortenFormUrl() {
547
+ shortenFormUrl(url) {
548
+ // just validate the atgs would work on apps script
549
+ const { nargs, matchThrow } = signatureArgs(arguments, 'shortenFormUrl');
550
+ if (nargs !== 1 || !is.nonEmptyString(url)) {
551
+ matchThrow();
552
+ }
553
+ // apps script expects a url, but we return the published url and just ignore the url anyway
485
554
  return this.getPublishedUrl()
486
555
  }
487
-
556
+
488
557
  toString() {
489
558
  return 'Form';
490
559
  }
@@ -115,7 +115,7 @@ export class FakeFormItem {
115
115
 
116
116
  getId() {
117
117
  // Live Apps Script returns IDs as decimal numbers, not the hex strings from the API.
118
- return this.__itemId;
118
+ return parseInt(this.__itemId, 16); // Convert to decimal
119
119
  }
120
120
 
121
121
  getIndex() {
@@ -30,10 +30,10 @@ export class FakeFormResponse {
30
30
 
31
31
  /**
32
32
  * Gets the unique ID for this form response.
33
- * @returns {string} the unique ID
33
+ * @returns {number} the unique hex ID converted to a decimal number
34
34
  */
35
35
  getId() {
36
- return this.__resource.responseId;
36
+ return parseInt(this.__resource.responseId, 16);
37
37
  }
38
38
 
39
39
  /**
@@ -32,7 +32,7 @@ export class FakeItemResponse {
32
32
  * @returns {string} the item's ID
33
33
  */
34
34
  getId() {
35
- return this.__item.getId();
35
+ return parseInt(this.__item.getId(), 16); // Convert to decimal
36
36
  }
37
37
 
38
38
  /**
@@ -1,119 +1,127 @@
1
- import { GoogleAuth } from 'google-auth-library'
2
- import is from '@sindresorhus/is'
3
- import { createHash } from 'node:crypto'
4
- import { syncLog } from './workersync/synclogger.js'
1
+ import { GoogleAuth } from "google-auth-library";
2
+ import is from "@sindresorhus/is";
3
+ import { createHash } from "node:crypto";
4
+ import { syncLog } from "./workersync/synclogger.js";
5
5
 
6
- const _authScopes = new Set([])
6
+ const _authScopes = new Set([]);
7
7
 
8
8
  // all this stuff gets populated by the initial synced fxInit
9
- let _auth = null
10
- let _projectId = null
11
- let _tokenInfo = null
9
+ let _auth = null;
10
+ let _projectId = null;
11
+ let _tokenInfo = null;
12
12
  let _accessToken = null;
13
13
  let _tokenExpiresAt = null;
14
- let _manifest = null
15
- let _clasp = null
16
-
17
- let _settings = null
18
- const setManifest = (manifest) => _manifest = manifest
19
- const setClasp = (clasp) => _clasp = clasp
20
- const getManifest = () => _manifest
21
- const getClasp = () => _clasp
22
- const getSettings = () => _settings
23
- const getScriptId = () => getSettings().scriptId
24
- const getDocumentId = () => getSettings().documentId
25
- const setProjectId = (projectId) => _projectId = projectId
26
- const setAccessToken = (accessToken) => _accessToken = accessToken
27
- const setSettings = (settings) => _settings = settings
28
- const getCachePath = () => getSettings().cache
29
- const getPropertiesPath = () => getSettings().properties
30
- const setTokenExpiresAt = (expiresAt) => _tokenExpiresAt = expiresAt;
14
+ let _manifest = null;
15
+ let _clasp = null;
16
+
17
+ let _settings = null;
18
+ const setManifest = (manifest) => (_manifest = manifest);
19
+ const setClasp = (clasp) => (_clasp = clasp);
20
+ const getManifest = () => _manifest;
21
+ const getClasp = () => _clasp;
22
+ const getSettings = () => _settings;
23
+ const getScriptId = () => getSettings().scriptId;
24
+ const getDocumentId = () => getSettings().documentId;
25
+ const setProjectId = (projectId) => (_projectId = projectId);
26
+ const setAccessToken = (accessToken) => (_accessToken = accessToken);
27
+ const setSettings = (settings) => (_settings = settings);
28
+ const getCachePath = () => getSettings().cache;
29
+ const getPropertiesPath = () => getSettings().properties;
30
+ const setTokenExpiresAt = (expiresAt) => (_tokenExpiresAt = expiresAt);
31
31
  const setTokenInfo = (tokenInfo) => {
32
32
  _tokenInfo = tokenInfo;
33
33
  // set expiry time with a 60 second buffer
34
34
  if (tokenInfo && tokenInfo.expires_in) {
35
- setTokenExpiresAt(Date.now() + ((tokenInfo.expires_in - 60) * 1000));
35
+ setTokenExpiresAt(Date.now() + (tokenInfo.expires_in - 60) * 1000);
36
36
  } else {
37
37
  // no expiry info, so we'll have to fetch a new one next time
38
38
  setTokenExpiresAt(0);
39
39
  }
40
- }
40
+ };
41
41
  const getTokenInfo = () => {
42
- if (!_tokenInfo) throw `token info isnt set yet`
43
- return _tokenInfo
44
- }
45
-
46
- const getTimeZone = () => getManifest().timeZone
47
- const getUserId = () => getTokenInfo().sub
48
- const getTokenScopes = () => getTokenInfo().scope
49
- const getHashedUserId = () => createHash('md5').update(getUserId() + 'hud').digest().toString('hex')
42
+ if (!_tokenInfo) throw `token info isnt set yet`;
43
+ return _tokenInfo;
44
+ };
50
45
 
46
+ const getTimeZone = () => getManifest().timeZone;
47
+ const getUserId = () => getTokenInfo().sub;
48
+ const getTokenScopes = () => getTokenInfo().scope;
49
+ const getHashedUserId = () =>
50
+ createHash("md5")
51
+ .update(getUserId() + "hud")
52
+ .digest()
53
+ .toString("hex");
51
54
 
52
55
  const getAccessToken = () => _accessToken;
53
56
 
54
- const isTokenExpired = () => !_accessToken || !_tokenExpiresAt || Date.now() >= _tokenExpiresAt;
57
+ const isTokenExpired = () =>
58
+ !_accessToken || !_tokenExpiresAt || Date.now() >= _tokenExpiresAt;
55
59
  /**
56
60
  * we'll be using adc credentials so no need for any special auth here
57
61
  * the idea here is to keep addign scopes to any auth so we have them all
58
62
  * @param {string[]} [scopes=[]] the required scopes will be added to existing scopes already asked for
63
+ * @param {string} [keyFile=null]
64
+ * @param {boolean} [mcpLoading=false] When the MCP server is loading, this value is true. By this, the invalid values can be hidden while the MCP server is loading. This is important for using Google Antigravity.
59
65
  * @returns {GoogleAuth.auth}
60
66
  */
61
- let _authClient = null
62
- const getAuthClient = () => _authClient
63
- const setAuth = async (scopes = [], keyFile = null) => {
64
- const hasCurrentAuth = hasAuth() && scopes.every(s => _authScopes.has(s));
65
-
66
-
67
+ let _authClient = null;
68
+ const getAuthClient = () => _authClient;
69
+ const setAuth = async (scopes = [], keyFile = null, mcpLoading = false) => {
70
+ const hasCurrentAuth = hasAuth() && scopes.every((s) => _authScopes.has(s));
71
+
67
72
  if (!hasCurrentAuth) {
68
- syncLog(`...initializing auth and discovering project ID`);
69
-
73
+ if (!mcpLoading) {
74
+ syncLog(`...initializing auth and discovering project ID`);
75
+ }
76
+
70
77
  // 1. Create the GoogleAuth manager instance (this instance has getProjectId)
71
78
  _auth = new GoogleAuth({ scopes });
72
-
79
+
73
80
  // 2. Use the manager to get the authenticated client (this is passed to API methods)
74
81
  _authClient = await _auth.getClient();
75
-
82
+
76
83
  // 3. Use the manager to reliably get the project ID
77
84
  _projectId = await _auth.getProjectId();
78
-
85
+
79
86
  if (!_projectId) {
80
- throw new Error('Failed to get project ID from Application Default Credentials.');
87
+ throw new Error(
88
+ "Failed to get project ID from Application Default Credentials."
89
+ );
90
+ }
91
+
92
+ if (!mcpLoading) {
93
+ syncLog(`...discovered and set projectId to ${_projectId}`);
81
94
  }
82
-
83
- syncLog(`...discovered and set projectId to ${_projectId}`);
84
- scopes.forEach(s => _authScopes.add(s));
95
+ scopes.forEach((s) => _authScopes.add(s));
85
96
  }
86
97
  return getAuth();
87
98
  };
88
99
 
89
-
90
100
  /**
91
101
  * if we're doing a fetch on drive API we need a special header
92
102
  */
93
103
  const googify = (options = {}) => {
94
- const { headers } = options
104
+ const { headers } = options;
95
105
 
96
106
  // no auth, therefore no need
97
- if (!headers || !hasAuth()) return options
107
+ if (!headers || !hasAuth()) return options;
98
108
 
99
109
  // if no authorization, we dont need this either
100
- if (!Reflect.has(headers, "Authorization")) return options
110
+ if (!Reflect.has(headers, "Authorization")) return options;
101
111
 
102
112
  // we'll need the projectID for this
103
113
  // note - you must add the x-goog-user-project header, otherwise it'll use some nonexistent project
104
114
  // see https://cloud.google.com/docs/authentication/rest#set-billing-project
105
115
  // this has been syncified
106
- const projectId = getProjectId()
116
+ const projectId = getProjectId();
107
117
  return {
108
118
  ...options,
109
119
  headers: {
110
120
  "x-goog-user-project": projectId,
111
- ...headers
112
- }
113
- }
114
-
115
- }
116
-
121
+ ...headers,
122
+ },
123
+ };
124
+ };
117
125
 
118
126
  /**
119
127
  * this would have been set up when manifest was imported
@@ -121,56 +129,59 @@ const googify = (options = {}) => {
121
129
  */
122
130
  const getProjectId = () => {
123
131
  if (is.null(_projectId) || is.undefined(_projectId)) {
124
- throw new Error('Project id not set - this means that the fxInit wasnt run')
132
+ throw new Error(
133
+ "Project id not set - this means that the fxInit wasnt run"
134
+ );
125
135
  }
126
- return _projectId
127
- }
136
+ return _projectId;
137
+ };
128
138
 
129
- /**
139
+ /**
130
140
  * @returns {Boolean} checks to see if auth has bee initialized yet
131
141
  */
132
- const hasAuth = () => Boolean(_auth)
142
+ const hasAuth = () => Boolean(_auth);
133
143
 
134
144
  /**
135
145
  * @returns {GoogleAuth.auth}
136
146
  */
137
147
  const getAuth = () => {
138
- if (!hasAuth()) throw new Error(`auth hasnt been intialized with setAuth yet`)
139
- return _auth
140
- }
141
-
148
+ if (!hasAuth())
149
+ throw new Error(`auth hasnt been intialized with setAuth yet`);
150
+ return _auth;
151
+ };
142
152
 
143
153
  /**
144
154
  * why is this here ?
145
155
  * because when we syncit, we import auth for each method and it needs this
146
156
  * if it was somewhere else we'd need to import that too.
147
- * we can't serialize a return object
157
+ * we can't serialize a return object
148
158
  * so we just select a few props from it
149
- * @param {SyncApiResponse} result
150
- * @returns
159
+ * @param {SyncApiResponse} result
160
+ * @returns
151
161
  */
152
162
  export const responseSyncify = (result) => {
153
163
  if (!result) {
154
164
  return {
155
165
  status: 503, // Service Unavailable, a good representation for a worker-level failure
156
- statusText: 'Worker Error: No response object received from API call',
157
- error: { message: 'Worker Error: No response object received from API call' }
166
+ statusText: "Worker Error: No response object received from API call",
167
+ error: {
168
+ message: "Worker Error: No response object received from API call",
169
+ },
158
170
  };
159
171
  }
160
172
  return {
161
173
  status: result.status,
162
174
  statusText: result.statusText,
163
175
  responseUrl: result.request?.responseURL,
164
- error: result.data?.error
176
+ error: result.data?.error,
165
177
  };
166
- }
167
-
178
+ };
168
179
 
169
180
  /**
170
181
  * these are the ones that have been so far requested
171
182
  * @returns {Set}
172
183
  */
173
- const getAuthedScopes = () => _authScopes
184
+ const getAuthedScopes = () => _authScopes;
174
185
 
175
186
  export const Auth = {
176
187
  getAuth,
@@ -198,5 +209,5 @@ export const Auth = {
198
209
  getTimeZone,
199
210
  setAccessToken,
200
211
  isTokenExpired,
201
- getAuthClient
202
- }
212
+ getAuthClient,
213
+ };
@@ -20,10 +20,31 @@ export const sxFetch = async (Auth, url, options, responseFields) => {
20
20
 
21
21
  // Always fetch as a buffer to prevent corruption of binary data like images.
22
22
  // The caller (UrlFetchApp/HttpResponse) will be responsible for decoding to text if needed.
23
+ // we need to fiddle with options as apps script is diffeent
24
+ let fixedOptions = {}
25
+ if (options) {
26
+ fixedOptions = { ...options }
27
+ Object.keys(fixedOptions).forEach(k => {
28
+ if (k.match(/Content-Type/i)) {
29
+ fixedOptions.contentType = fixedOptions[k]
30
+ delete fixedOptions[k]
31
+ }
32
+ if (k.match(/payload/i)) {
33
+ fixedOptions.body = fixedOptions[k]
34
+ delete fixedOptions[k]
35
+ }
36
+ if(k.match(/muteHttpExceptions/i)) {
37
+ fixedOptions.throwHttpErrors = !fixedOptions[k]
38
+ delete fixedOptions[k]
39
+ }
40
+ })
41
+ }
42
+
23
43
  const response = await got(url, {
24
- ...options,
44
+ ...fixedOptions,
25
45
  responseType: 'buffer'
26
46
  })
47
+
27
48
  // we cant return the response from this as it cant be serialized
28
49
  // so we;ll extract oout the fields required
29
50
  const result = responseFields.reduce((p, c) => {