@mcpher/gas-fakes 1.2.9 → 1.2.10

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/exgcp.sh ADDED
@@ -0,0 +1,31 @@
1
+ #!/bin/bash
2
+
3
+ # This script reads the GCP_PROJECT_ID from a .env file
4
+ # and exports it as GOOGLE_CLOUD_PROJECT for the current shell session.
5
+ #
6
+ # Usage: source . ./exgcp.sh
7
+
8
+ # Define the path to your .env file relative to the script's location
9
+ ENV_FILE="$(dirname "$0")/.env"
10
+
11
+ # Check if the .env file exists
12
+
13
+ if [ ! -f "$ENV_FILE" ]; then
14
+ echo "Error: .env file not found at path: $ENV_FILE"
15
+ # Use 'return' instead of 'exit' so it doesn't close the user's terminal when sourced
16
+ return 1
17
+ fi
18
+
19
+ # Read the GCP_PROJECT_ID, remove quotes, and handle potential carriage returns
20
+ GCP_PROJECT_ID_VALUE=$(grep -E '^GCP_PROJECT_ID=' "$ENV_FILE" | cut -d '=' -f2 | tr -d '"\r')
21
+
22
+ # Check if a value was extracted
23
+ if [ -z "$GCP_PROJECT_ID_VALUE" ]; then
24
+ echo "Error: GCP_PROJECT_ID not found or is empty in $ENV_FILE."
25
+ return 1
26
+ fi
27
+
28
+ # Export the variable for the current session
29
+ export GOOGLE_CLOUD_PROJECT="$GCP_PROJECT_ID_VALUE"
30
+
31
+ echo "Successfully exported: GOOGLE_CLOUD_PROJECT=$GOOGLE_CLOUD_PROJECT"
package/gas-fakes.js CHANGED
@@ -1,309 +1,482 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * cli for gas-fakes
5
- */
3
+ // -----------------------------------------------------------------------------
4
+ // IMPORTS
5
+ // -----------------------------------------------------------------------------
6
+
6
7
  import fs from "fs";
7
8
  import path from "path";
9
+ import { exec } from "child_process";
10
+ import { promisify } from "util";
8
11
  import { Command } from "commander";
9
12
  import dotenv from "dotenv";
10
13
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
- import z from "zod";
13
- import { exec } from "child_process";
14
- import { promisify } from "util";
15
+ import { z } from "zod";
15
16
 
16
- const version = "0.0.3";
17
-
18
- const program = new Command();
19
-
20
- program
21
- .name("gas-fakes")
22
- .description("CLI tool for gas-fakes")
23
- .version(version, "-v, --version", "display the current version");
24
-
25
- program
26
- .description("Execute Google Apps Script using gas-fakes.")
27
- .option(
28
- "-f, --filename <string>",
29
- "filename of the file including Google Apps Script. When this is used, the option --script is ignored."
30
- )
31
- .option(
32
- "-e, --env <path>",
33
- "provide path to your .env file for special options.",
34
- "./.env"
35
- )
36
- .option(
37
- "-g, --gfsettings <path>",
38
- "provide path to your gasfakes.json file for script options.",
39
- "./gasfakes.json"
40
- )
41
- .option(
42
- "-s, --script <string>",
43
- "provide Google Apps Script as a string. When this is used, the option --filename is ignored."
44
- )
45
- .option("-x, --sandbox", "run Google Apps Script in a sandbox.")
46
- .option(
47
- "-w, --whitelist <string>",
48
- "whitelist of file IDs. Set the file IDs in comma-separated list. In this case, the files of the file IDs are used for both read and write. When this is used, the script is run in a sandbox."
49
- )
50
- .option(
51
- "-j, --json <string>",
52
- `JSON string including parameters for managing a sandbox. Enclose it with ' or ". When this is used, the option --whitelist is ignored. When this is used, the script is run in a sandbox.`
53
- )
54
- .option(
55
- "-d, --display",
56
- `display the created script for executing with gas-fakes. Default is false.`,
57
- false
58
- )
59
- .action((options) => {
60
- if (Object.keys(options).length == 0) {
61
- program.help();
62
- } else {
63
- const {
64
- filename,
65
- script,
66
- sandbox,
67
- whitelist,
68
- json,
69
- display,
70
- env,
71
- gfsettings,
72
- } = options;
73
- const obj = { sandbox: !!sandbox, display };
74
-
75
- if (!filename && !script && !obj.script) {
76
- console.error(
77
- "error: Provide the filename or the script of Google Apps Script."
78
- );
79
- process.exit();
80
- }
17
+ // -----------------------------------------------------------------------------
18
+ // CONSTANTS & UTILITIES
19
+ // -----------------------------------------------------------------------------
81
20
 
82
- if (env) {
83
- const envPath = path.resolve(process.cwd(), env);
84
- console.log("...using env file in", envPath);
85
- dotenv.config({ path: envPath, quiet: true });
86
- }
21
+ const VERSION = "0.0.5";
22
+ const MCP_VERSION = "0.0.3";
23
+ const execAsync = promisify(exec);
87
24
 
88
- // note this must come after any env file fiddling.
89
- if (gfsettings) {
90
- const gfPath = path.resolve(process.cwd(), gfsettings);
91
- console.log("...using gasfakes settings file in", gfPath);
92
- obj.gfSettings = gfPath;
93
- // override whatever is in env
94
- process.env.GF_SETTINGS_PATH = gfPath;
95
- }
25
+ /**
26
+ * Replaces escaped newline characters ('\\n') with actual newlines,
27
+ * while ignoring newlines inside string literals.
28
+ * @param {string} text The script text to process.
29
+ * @returns {string} The processed text.
30
+ */
31
+ function normalizeScriptNewlines(text) {
32
+ const regex = /("[^"]*")|('[^']*')|(`[^`]*`)|(\\n)/g;
33
+ return text.replace(regex, (match, g1, g2, g3, g4) => (g4 ? "\n" : match));
34
+ }
96
35
 
97
- if (filename) {
98
- obj.filename = filename;
99
- }
100
- if (!obj.script && script) {
101
- obj.script = script;
102
- }
36
+ // -----------------------------------------------------------------------------
37
+ // SANDBOX SCRIPT GENERATION
38
+ // -----------------------------------------------------------------------------
103
39
 
104
- // for sandbox
105
- if (whitelist) {
106
- const ar = whitelist.split(",").map((e) => e.trim());
107
- if (ar.length > 0) {
108
- obj.whitelistItems = ar;
109
- }
110
- }
111
- if (json) {
112
- try {
113
- const temp = JSON.parse(json);
114
- obj.json_sandbox = temp;
115
- } catch (err) {
116
- console.error("error: Invalid JSON.");
117
- process.exit();
118
- }
119
- }
120
- loadScript(obj);
40
+ /**
41
+ * Generates the GAS script snippet for whitelisting services.
42
+ * @param {Array<Object>} services The whitelistServices configuration.
43
+ * @returns {string[]} An array of script lines.
44
+ */
45
+ function generateServiceWhitelistScript(services) {
46
+ if (!services || services.length === 0) return [];
47
+
48
+ return services.flatMap(({ className, methodNames }, index) => {
49
+ if (!className) {
50
+ console.error("Error: Class name not found in whitelistServices.");
51
+ process.exit(1);
52
+ }
53
+ const serviceVar = `service${index + 1}`;
54
+ const lines = [
55
+ `const ${serviceVar} = behavior.sandboxService.${className};`,
56
+ ];
57
+ if (methodNames && methodNames.length > 0) {
58
+ const methods = methodNames.map((name) => `"${name}"`).join(", ");
59
+ lines.push(`${serviceVar}.setMethodWhitelist([${methods}]);`);
121
60
  }
61
+ return lines;
122
62
  });
63
+ }
123
64
 
124
- const execAsync = promisify(exec);
125
- program
126
- .command("mcp")
127
- .description("Launch gas-fakes as the MCP server")
128
- .action(mcp_server);
129
-
130
- program.showHelpAfterError("(add --help for additional information)");
131
- program.parse();
132
-
133
- function __getImportScript(o) {
134
- const { scriptText, sandbox, whitelistItems, json_sandbox } = o;
135
- if (scriptText.trim() == "") {
136
- console.error("error: Google Apps Script was not found.");
137
- process.exit();
65
+ /**
66
+ * Generates the GAS script snippet for blacklisting services.
67
+ * @param {string[]} services The blacklistServices configuration.
68
+ * @returns {string[]} An array of script lines.
69
+ */
70
+ function generateServiceBlacklistScript(services) {
71
+ if (!services || services.length === 0) return [];
72
+ return services.map(
73
+ (service) => `behavior.sandboxService.${service}.enabled = false;`
74
+ );
75
+ }
76
+
77
+ /**
78
+ * Generates the GAS script snippet for whitelisting Drive items.
79
+ * @param {Array<Object>} items The whitelistItems configuration.
80
+ * @returns {string} A script string.
81
+ */
82
+ function generateItemWhitelistScript(items) {
83
+ if (!items || items.length === 0) return "";
84
+
85
+ const whitelistItemsString = items
86
+ .map(({ itemId = "", read = true, write = false, trash = false }) => {
87
+ if (!itemId) {
88
+ console.error("Error: itemId not found in whitelistItems.");
89
+ process.exit(1);
90
+ }
91
+ return `behavior.newIdWhitelistItem("${itemId}").setRead(${read}).setWrite(${write}).setTrash(${trash})`;
92
+ })
93
+ .join(",\n ");
94
+
95
+ return `behavior.setIdWhitelist([${whitelistItemsString}]);`;
96
+ }
97
+
98
+ /**
99
+ * Constructs the setup script for a sandboxed environment based on configuration.
100
+ * @param {object} sandboxConfig The sandbox configuration object.
101
+ * @returns {string[]} An array of GAS script lines for setup.
102
+ */
103
+ function generateSandboxSetupScript(sandboxConfig) {
104
+ const script = [
105
+ "const behavior = ScriptApp.__behavior;",
106
+ "behavior.sandboxMode = true;",
107
+ "behavior.strictSandbox = true;",
108
+ ];
109
+
110
+ const { whitelistServices, blacklistServices, whitelistItems } =
111
+ sandboxConfig;
112
+
113
+ script.push(...generateServiceWhitelistScript(whitelistServices));
114
+ script.push(...generateServiceBlacklistScript(blacklistServices));
115
+
116
+ const itemWhitelist = generateItemWhitelistScript(whitelistItems);
117
+ if (itemWhitelist) {
118
+ script.push(itemWhitelist);
138
119
  }
139
- let gasScriptStr = "";
140
- const gasScriptAr = [];
141
- if (json_sandbox) {
142
- gasScriptAr.push(
143
- `const behavior = ScriptApp.__behavior;`,
144
- `behavior.sandboxMode = true;`,
145
- `behavior.strictSandbox = true;`
146
- );
147
- const { whitelistItems, whitelistServices, blacklistServices } =
148
- json_sandbox;
149
- if (whitelistServices && whitelistServices.length > 0) {
150
- const bl = whitelistServices.flatMap(({ className, methodNames }, i) => {
151
- if (!className) {
152
- console.error(
153
- "error: Class name was not found in whitelistServices."
154
- );
155
- process.exit();
156
- }
157
- const k = `s${i + 1}`;
158
- const temp = [`const ${k} = behavior.sandboxService.${className};`];
159
- if (methodNames && methodNames.length > 0) {
160
- temp.push(
161
- `${k}.setMethodWhitelist([${methodNames.map((e) => `"${e}"`)}]);`
162
- );
163
- }
164
- return temp;
165
- });
166
- gasScriptAr.push(...bl);
167
- }
168
- if (blacklistServices && blacklistServices.length > 0) {
169
- const bl = blacklistServices.map(
170
- (e) => `behavior.sandboxService.${e}.enabled = false;`
171
- );
172
- gasScriptAr.push(bl);
173
- }
174
- if (whitelistItems && whitelistItems.length > 0) {
175
- const wl = whitelistItems
176
- .map(({ itemId = "", read = true, write = false, trash = false }) => {
177
- if (!itemId) {
178
- console.error("error: itemId was not found in whitelistItems.");
179
- process.exit();
180
- }
181
- return `behavior.newIdWhitelistItem("${itemId}").setRead(${read}).setWrite(${write}).setTrash(${trash})`;
182
- })
183
- .join(",");
184
- gasScriptAr.push(`behavior.setIdWhitelist([${wl}]);`);
185
- }
186
- gasScriptAr.push(`\n\n${scriptText}\n\n`, `ScriptApp.__behavior.trash();`);
120
+
121
+ return script;
122
+ }
123
+
124
+ /**
125
+ * Generates the final, executable script string.
126
+ * @param {object} options
127
+ * @param {string} options.scriptText The user's Google Apps Script.
128
+ * @param {boolean} options.useSandbox Whether to enable basic sandbox mode.
129
+ * @param {object} [options.sandboxConfig] Detailed sandbox configuration.
130
+ * @returns {{mainScript: string, gasScript: string}} The script to be executed and the user-facing script part.
131
+ */
132
+ function generateExecutionScript({ scriptText, useSandbox, sandboxConfig }) {
133
+ if (!scriptText || scriptText.trim() === "") {
134
+ console.error("Error: Google Apps Script is empty or was not found.");
135
+ process.exit(1);
136
+ }
137
+
138
+ let gasScriptLines = [];
139
+
140
+ if (sandboxConfig) {
141
+ gasScriptLines.push(...generateSandboxSetupScript(sandboxConfig));
142
+ gasScriptLines.push(`\n\n${scriptText}\n\n`);
143
+ gasScriptLines.push("ScriptApp.__behavior.trash();");
144
+ } else if (useSandbox) {
145
+ gasScriptLines.push("ScriptApp.__behavior.sandBoxMode = true;");
146
+ gasScriptLines.push(`\n\n${scriptText}\n\n`);
147
+ gasScriptLines.push("ScriptApp.__behavior.trash();");
187
148
  } else {
188
- if (sandbox && (!whitelistItems || whitelistItems.length === 0)) {
189
- gasScriptAr.push(
190
- sandbox ? `ScriptApp.__behavior.sandBoxMode = true;` : "",
191
- `\n\n${scriptText}\n\n`,
192
- sandbox ? `ScriptApp.__behavior.trash();` : ""
193
- );
194
- } else if (whitelistItems && whitelistItems.length > 0) {
195
- const wl = whitelistItems
196
- .map((id) => `behavior.newIdWhitelistItem("${id}").setWrite(true)`)
197
- .join(",");
198
- gasScriptAr.push(
199
- `const behavior = ScriptApp.__behavior;`,
200
- `behavior.sandboxMode = true;`,
201
- `behavior.strictSandbox = true;`,
202
- `behavior.setIdWhitelist([${wl}]);``\n\n${scriptText}\n\n`,
203
- `ScriptApp.__behavior.trash();`
204
- );
205
- } else {
206
- gasScriptAr.push(scriptText);
207
- }
149
+ gasScriptLines.push(scriptText);
208
150
  }
209
- const importScriptAr = [
210
- `async function runGas() {`,
211
- `await import("./main.js");`, // This will trigger the fxInit call
212
- ...gasScriptAr,
213
- `};`,
214
- ``,
215
- `runGas();`,
216
- ];
217
- return {
218
- mainScript: importScriptAr.join("\n"),
219
- gasScript: gasScriptAr.join("\n"),
220
- };
151
+
152
+ const gasScript = gasScriptLines.join("\n");
153
+ const mainScript = [
154
+ "async function runGas() {",
155
+ ' await import("./main.js"); // This will trigger the fxInit call',
156
+ gasScript,
157
+ "}",
158
+ "runGas();",
159
+ ].join("\n");
160
+
161
+ return { mainScript, gasScript };
221
162
  }
222
163
 
223
- async function loadScript(o) {
224
- const { filename, script, display } = o;
164
+ // -----------------------------------------------------------------------------
165
+ // SCRIPT EXECUTION
166
+ // -----------------------------------------------------------------------------
167
+
168
+ /**
169
+ * Loads, prepares, and executes the user's Google Apps Script.
170
+ * @param {object} options The processed CLI options.
171
+ */
172
+ async function executeGasScript(options) {
173
+ const { filename, script, display, gfSettings, useSandbox, sandboxConfig } =
174
+ options;
175
+
225
176
  const scriptText = filename ? fs.readFileSync(filename, "utf8") : script;
226
- const { mainScript, gasScript } = __getImportScript({
227
- scriptText: scriptText.replace(/\\n/g, "\n"),
228
- ...o,
177
+
178
+ const { mainScript, gasScript } = generateExecutionScript({
179
+ scriptText: normalizeScriptNewlines(scriptText),
180
+ useSandbox,
181
+ sandboxConfig,
229
182
  });
183
+
230
184
  if (display) {
231
- console.log(`\n--- script ---\n${gasScript}\n--- /script ---\n`);
185
+ console.log(
186
+ `\n--- Generated GAS ---\n${gasScript}\n--- End Generated GAS ---\n`
187
+ );
232
188
  }
233
- const gasFunc = new Function(mainScript);
234
- // The script needs access to the settings path variable we just created
189
+
190
+ // Inject the settings path as a global for the script to access.
235
191
  Object.defineProperty(globalThis, "settingsPath", {
236
- value: o.gfSettings,
192
+ value: gfSettings,
237
193
  writable: true,
238
194
  configurable: true,
239
195
  });
240
- gasFunc();
196
+
197
+ const gasFunction = new Function(mainScript);
198
+ await gasFunction();
241
199
  }
242
200
 
243
- async function mcp_server() {
201
+ // -----------------------------------------------------------------------------
202
+ // MCP SERVER
203
+ // -----------------------------------------------------------------------------
204
+
205
+ /**
206
+ * Defines and runs the MCP server for gas-fakes.
207
+ */
208
+ async function startMcpServer() {
244
209
  const server = new McpServer({
245
210
  name: "gas-fakes-mcp",
246
- version: "0.0.1",
211
+ version: MCP_VERSION,
247
212
  });
248
213
 
249
- const { name, schema, func } = {
250
- name: "run-gas-by-gas-fakes",
251
- schema: {
252
- description:
253
- "Use this to safely run Google Apps Script in a sandbox using gas-fakes.",
254
- inputSchema: {
255
- script: z.string().describe(`Provide Google Apps Script as a string.`),
256
- sandbox: z
257
- .boolean()
258
- .describe("Use to run Google Apps Script in a sandbox."),
259
- whitelist: z
260
- .string()
261
- .describe(
262
- "Use this to use the specific files and folders on Google Drive. whitelist of file IDs. Set the file IDs in comma-separated list. In this case, the files of the file IDs are used for both read and write. When this is used, the script is run in a sandbox."
263
- )
264
- .optional(),
265
- json: z
266
- .string()
267
- .describe(
268
- `Use this to manage the sandbox more if the detailed information about the sandbox is provided. JSON string including parameters for managing a sandbox. Enclose it with ' or ". When this is used, the option "whitelist" is ignored. When this is used, the script is run in a sandbox.`
269
- )
270
- .optional(),
271
- },
272
- },
273
- func: async (options = {}) => {
274
- const { sandbox, whitelist, json } = options;
275
- try {
276
- const opts = [
277
- { v: sandbox, k: "-x" },
278
- { v: whitelist, k: "-w" },
279
- { v: json, k: "-j" },
280
- ].reduce((ar, { v, k }) => {
281
- if (v) {
282
- ar.push(k != "-x" ? `${k} ${v}` : `${k}`);
283
- }
284
- return ar;
285
- }, []);
286
- const scriptArg = JSON.stringify(options.script.toString());
287
- const c = `gas-fakes ${opts.join(" ")} -s ${scriptArg.replace(
288
- /\\n/g,
289
- "\n"
290
- )}`;
291
- const { stdout } = await execAsync(c);
292
- return {
293
- content: [{ type: "text", text: stdout || "Done." }],
294
- isError: false,
295
- };
296
- } catch (err) {
297
- return {
298
- content: [{ type: "text", text: err.message }],
299
- isError: true,
300
- };
301
- }
214
+ const mcpToolSchema = {
215
+ description: [
216
+ `Use this to safely run Google Apps Script in a sandbox using gas-fakes.`,
217
+ `# Important`,
218
+ `- Use the extension of the Google Apps Script files as \`js\`. Don't use \`gs\``,
219
+ `- When you provide the generated Google Apps Script to the tool "gas-fakes" of the MCP server "gas-development-kit-extension", please be careful of the following rule. For example, when you generated a Google Apps Script like \`function sample() { script }\`, please add \`sample();\` to execute the function. Or, you can also create a Google Apps Script without enclosing the script with \`function sample() { script }\`.`,
220
+ ].join("\n"),
221
+ inputSchema: {
222
+ filename: z
223
+ .string()
224
+ .describe(
225
+ `Provide a filename with the path of the file, including Google Apps Script. Write the Google Apps Script into a file and use this.`
226
+ ),
227
+ sandbox: z
228
+ .boolean()
229
+ .describe("Use to run Google Apps Script in a sandbox."),
230
+ whitelistRead: z
231
+ .string()
232
+ .describe(
233
+ "Whitelist of file IDs for readonly access (comma-separated). Enables sandbox mode."
234
+ )
235
+ .optional(),
236
+ whitelistReadWrite: z
237
+ .string()
238
+ .describe(
239
+ "Whitelist of file IDs for read/write access (comma-separated). Enables sandbox mode."
240
+ )
241
+ .optional(),
242
+ whitelistReadWriteTrash: z
243
+ .string()
244
+ .describe(
245
+ "Whitelist of file IDs for read/write/trash access (comma-separated). Enables sandbox mode."
246
+ )
247
+ .optional(),
248
+ json: z
249
+ .object({
250
+ whitelistItems: z
251
+ .array(
252
+ z.object({
253
+ itemId: z
254
+ .string()
255
+ .describe("The file or folder ID on Google Drive."),
256
+ read: z.boolean().optional().default(true),
257
+ write: z.boolean().optional().default(false),
258
+ trash: z.boolean().optional().default(false),
259
+ })
260
+ )
261
+ .describe("A list of items to be whitelisted."),
262
+ whitelistServices: z
263
+ .array(
264
+ z.object({
265
+ className: z
266
+ .string()
267
+ .describe("The class name of the GAS service."),
268
+ methodNames: z
269
+ .array(z.string())
270
+ .describe(
271
+ "A list of method names for the class to be whitelisted."
272
+ )
273
+ .optional(),
274
+ })
275
+ )
276
+ .describe("A list of services to be whitelisted.")
277
+ .optional(),
278
+ blacklistServices: z
279
+ .array(z.string())
280
+ .describe("A list of GAS services to be blacklisted.")
281
+ .optional(),
282
+ })
283
+ .describe("A JSON object for advanced sandbox configuration.")
284
+ .optional(),
302
285
  },
303
286
  };
304
287
 
305
- server.registerTool(name, schema, func);
288
+ const mcpToolFunc = async (options = {}) => {
289
+ const {
290
+ filename,
291
+ sandbox,
292
+ whitelistRead,
293
+ whitelistReadWrite,
294
+ whitelistReadWriteTrash,
295
+ json,
296
+ } = options;
297
+
298
+ if (!filename) {
299
+ return {
300
+ content: [
301
+ { type: "text", text: "Error: `filename` is a required parameter." },
302
+ ],
303
+ isError: true,
304
+ };
305
+ }
306
+
307
+ try {
308
+ const cliArgs = [];
309
+ cliArgs.push(`-f "${filename}"`);
310
+ if (sandbox) cliArgs.push("-x");
311
+ if (whitelistRead) cliArgs.push(`-w "${whitelistRead}"`);
312
+ if (whitelistReadWrite) cliArgs.push(`--ww "${whitelistReadWrite}"`);
313
+ if (whitelistReadWriteTrash)
314
+ cliArgs.push(`--wt "${whitelistReadWriteTrash}"`);
315
+ if (json) cliArgs.push(`-j '${JSON.stringify(json)}'`);
316
+
317
+ const command = `gas-fakes ${cliArgs.join(" ")}`;
318
+ const { stdout } = await execAsync(command);
319
+ return {
320
+ content: [{ type: "text", text: stdout || "Execution finished." }],
321
+ isError: false,
322
+ };
323
+ } catch (err) {
324
+ return {
325
+ content: [{ type: "text", text: err.message }],
326
+ isError: true,
327
+ };
328
+ }
329
+ };
330
+
331
+ server.registerTool("run-gas-by-gas-fakes", mcpToolSchema, mcpToolFunc);
306
332
 
307
333
  const transport = new StdioServerTransport();
308
334
  await server.connect(transport);
309
335
  }
336
+
337
+ // -----------------------------------------------------------------------------
338
+ // CLI DEFINITION & MAIN EXECUTION
339
+ // -----------------------------------------------------------------------------
340
+
341
+ /**
342
+ * Parses sandbox-related CLI options into a structured config object.
343
+ * @param {object} options Raw options from Commander.
344
+ * @returns {object | undefined} A sandbox configuration object or undefined.
345
+ */
346
+ function buildSandboxConfig(options) {
347
+ const { json, whitelistRead, whitelistReadWrite, whitelistReadWriteTrash } =
348
+ options;
349
+
350
+ if (json) {
351
+ try {
352
+ return JSON.parse(json);
353
+ } catch (err) {
354
+ console.error("Error: Invalid JSON provided to --json option.");
355
+ process.exit(1);
356
+ }
357
+ }
358
+
359
+ if (whitelistRead || whitelistReadWrite || whitelistReadWriteTrash) {
360
+ const config = { whitelistItems: [] };
361
+ const parseWhitelist = (idString, permissions) => {
362
+ if (!idString) return;
363
+ idString.split(",").forEach((id) => {
364
+ const trimmedId = id.trim();
365
+ if (trimmedId) {
366
+ config.whitelistItems.push({ itemId: trimmedId, ...permissions });
367
+ }
368
+ });
369
+ };
370
+
371
+ parseWhitelist(whitelistRead, { read: true });
372
+ parseWhitelist(whitelistReadWrite, { read: true, write: true });
373
+ parseWhitelist(whitelistReadWriteTrash, {
374
+ read: true,
375
+ write: true,
376
+ trash: true,
377
+ });
378
+
379
+ return config;
380
+ }
381
+
382
+ return undefined;
383
+ }
384
+
385
+ /**
386
+ * Sets up and runs the command-line interface.
387
+ */
388
+ async function main() {
389
+ const program = new Command();
390
+
391
+ program
392
+ .name("gas-fakes")
393
+ .description("A CLI tool to execute Google Apps Script with fakes/mocks.")
394
+ .version(VERSION, "-v, --version", "Display the current version");
395
+
396
+ program
397
+ .description("Execute a Google Apps Script file or string.")
398
+ .option("-f, --filename <string>", "Path to the Google Apps Script file.")
399
+ .option(
400
+ "-s, --script <string>",
401
+ "A string containing the Google Apps Script."
402
+ )
403
+ .option("-e, --env <path>", "Path to a custom .env file.", "./.env")
404
+ .option(
405
+ "-g, --gfsettings <path>",
406
+ "Path to a gasfakes.json settings file.",
407
+ "./gasfakes.json"
408
+ )
409
+ .option("-x, --sandbox", "Run the script in a basic sandbox.")
410
+ .option(
411
+ "-w, --whitelistRead <string>",
412
+ "Comma-separated file IDs for read-only access (enables sandbox)."
413
+ )
414
+ .option(
415
+ "--ww, --whitelistReadWrite <string>",
416
+ "Comma-separated file IDs for read/write access (enables sandbox)."
417
+ )
418
+ .option(
419
+ "--wt, --whitelistReadWriteTrash <string>",
420
+ "Comma-separated file IDs for read/write/trash access (enables sandbox)."
421
+ )
422
+ .option(
423
+ "-j, --json <string>",
424
+ "JSON string for advanced sandbox configuration (overrides whitelist flags)."
425
+ )
426
+ .option(
427
+ "-d, --display",
428
+ "Display the generated script before execution.",
429
+ false
430
+ )
431
+ .action(async (options) => {
432
+ if (Object.keys(options).length === 0) {
433
+ program.help();
434
+ return;
435
+ }
436
+
437
+ const { filename, script, env, gfsettings } = options;
438
+ if (!filename && !script) {
439
+ console.error(
440
+ "Error: You must provide a script via --filename or --script."
441
+ );
442
+ process.exit(1);
443
+ }
444
+
445
+ // Load environment variables
446
+ const envPath = path.resolve(process.cwd(), env);
447
+ console.log(`...using env file in ${envPath}`);
448
+ dotenv.config({ path: envPath, quiet: true });
449
+
450
+ // Load gasfakes settings
451
+ const settingsPath = path.resolve(process.cwd(), gfsettings);
452
+ console.log(`...using gasfakes settings file in ${settingsPath}`);
453
+ process.env.GF_SETTINGS_PATH = settingsPath;
454
+
455
+ const sandboxConfig = buildSandboxConfig(options);
456
+ const useSandbox = !!options.sandbox || !!sandboxConfig;
457
+
458
+ await executeGasScript({
459
+ filename,
460
+ script,
461
+ display: options.display,
462
+ useSandbox,
463
+ sandboxConfig,
464
+ gfSettings: settingsPath,
465
+ });
466
+ });
467
+
468
+ program
469
+ .command("mcp")
470
+ .description("Launch gas-fakes as an MCP server.")
471
+ .action(startMcpServer);
472
+
473
+ program.showHelpAfterError("(add --help for additional information)");
474
+
475
+ await program.parseAsync(process.argv);
476
+ }
477
+
478
+ // Run the main function
479
+ main().catch((err) => {
480
+ console.error("An unexpected error occurred:", err);
481
+ process.exit(1);
482
+ });
package/package.json CHANGED
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "name": "@mcpher/gas-fakes",
36
36
  "author": "bruce mcpherson",
37
- "version": "1.2.9",
37
+ "version": "1.2.10",
38
38
  "license": "MIT",
39
39
  "main": "main.js",
40
40
  "description": "A proof of concept implementation of Apps Script Environment on Node",
@@ -42,7 +42,7 @@ export class FakeSpreadsheet {
42
42
  const props = [
43
43
  "getSpreadsheetTheme",
44
44
  "setActiveSheet",
45
- "getActiveSheet",
45
+
46
46
  "getBandings",
47
47
  "getDataSources",
48
48
  "addCollaborator",
@@ -171,8 +171,17 @@ export class FakeSpreadsheet {
171
171
  return notYetImplemented(f);
172
172
  };
173
173
  });
174
+
175
+ }
176
+
177
+ // note this is a workaround as we don't have the concept of active sheet in a non bound document
178
+ // so instead we'll just get the first sheet for now
179
+ // TODO - something better
180
+ getActiveSheet() {
181
+ return this.__getFirstSheet();
174
182
  }
175
183
 
184
+
176
185
  addDeveloperMetadata(key, value, visibility) {
177
186
  const { nargs, matchThrow } = signatureArgs(
178
187
  arguments,