@mcpher/gas-fakes 1.2.16 → 1.2.17

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.
Files changed (3) hide show
  1. package/gas-fakes.js +39 -10
  2. package/package.json +2 -1
  3. package/setup.js +394 -0
package/gas-fakes.js CHANGED
@@ -14,6 +14,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
15
  import { z } from "zod";
16
16
 
17
+ // --- Import setup commands ---
18
+ import {
19
+ initializeConfiguration,
20
+ authenticateUser,
21
+ enableGoogleAPIs,
22
+ } from "./setup.js";
23
+
17
24
  // sync the version with gas fakes code since they share a package.json
18
25
  import { createRequire } from "node:module";
19
26
  const require = createRequire(import.meta.url);
@@ -24,7 +31,7 @@ const VERSION = pjson.version;
24
31
  // CONSTANTS & UTILITIES
25
32
  // -----------------------------------------------------------------------------
26
33
 
27
- const CLI_VERSION = "0.0.8";
34
+ const CLI_VERSION = "0.0.10";
28
35
  const MCP_VERSION = "0.0.3";
29
36
  const execAsync = promisify(exec);
30
37
 
@@ -420,6 +427,7 @@ async function main() {
420
427
  .description("A CLI tool to execute Google Apps Script with fakes/mocks.")
421
428
  .version(VERSION, "-v, --version", "Display the current version");
422
429
 
430
+ // Default command to execute a script
423
431
  program
424
432
  .description("Execute a Google Apps Script file or string.")
425
433
  .option("-f, --filename <string>", "Path to the Google Apps Script file.")
@@ -461,17 +469,19 @@ async function main() {
461
469
  null
462
470
  )
463
471
  .action(async (options) => {
464
- if (Object.keys(options).length === 0) {
465
- program.help();
466
- return;
467
- }
468
-
469
472
  const { filename, script, env, gfsettings } = options;
470
473
  if (!filename && !script) {
471
- console.error(
472
- "Error: You must provide a script via --filename or --script."
473
- );
474
- process.exit(1);
474
+ // This action is for the default command. If a known command is passed (like 'init'), this won't run.
475
+ // We check if the command is not one of the others.
476
+ const knownCommands = program.commands.map((cmd) => cmd.name());
477
+ if (!process.argv.slice(2).some((arg) => knownCommands.includes(arg))) {
478
+ console.error(
479
+ "Error: You must provide a script via --filename or --script, or use a specific command (e.g., init, auth, mcp)."
480
+ );
481
+ program.help();
482
+ process.exit(1);
483
+ }
484
+ return;
475
485
  }
476
486
 
477
487
  // Load environment variables
@@ -513,6 +523,25 @@ async function main() {
513
523
  });
514
524
  });
515
525
 
526
+ // --- Setup commands ---
527
+ program
528
+ .command("init")
529
+ .description(
530
+ "Initializes the configuration by creating or updating the .env file."
531
+ )
532
+ .action(initializeConfiguration);
533
+
534
+ program
535
+ .command("auth")
536
+ .description("Runs the Google Cloud authentication and authorization flow.")
537
+ .action(authenticateUser);
538
+
539
+ program
540
+ .command("enableAPIs")
541
+ .description("Enables the required Google Cloud APIs for the project.")
542
+ .action(enableGoogleAPIs);
543
+
544
+ // MCP server command
516
545
  program
517
546
  .command("mcp")
518
547
  .description("Launch gas-fakes as an MCP server.")
package/package.json CHANGED
@@ -18,6 +18,7 @@
18
18
  "keyv": "^5.5.0",
19
19
  "keyv-file": "^5.1.3",
20
20
  "mime": "^4.0.7",
21
+ "prompts": "^2.4.2",
21
22
  "sleep-synchronously": "^2.0.0",
22
23
  "unzipper": "^0.12.3",
23
24
  "zod": "^3.25.76"
@@ -31,7 +32,7 @@
31
32
  },
32
33
  "name": "@mcpher/gas-fakes",
33
34
  "author": "bruce mcpherson",
34
- "version": "1.2.16",
35
+ "version": "1.2.17",
35
36
  "license": "MIT",
36
37
  "main": "main.js",
37
38
  "description": "A proof of concept implementation of Apps Script Environment on Node",
package/setup.js ADDED
@@ -0,0 +1,394 @@
1
+ // setup.js: Setup for gas-fakes.
2
+
3
+ import prompts from "prompts";
4
+ import dotenv from "dotenv";
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import os from "os";
8
+ import { execSync } from "child_process";
9
+
10
+ // --- Utility Functions ---
11
+
12
+ /**
13
+ * Checks if the gcloud CLI is installed and available in the system's PATH.
14
+ * If not, it prints an informative message and exits the script.
15
+ */
16
+ function checkForGcloudCli() {
17
+ try {
18
+ // Execute a simple, non-destructive command to check if gcloud exists.
19
+ // The output is ignored to keep the console clean on success.
20
+ execSync("gcloud --version", { stdio: "ignore" });
21
+ } catch (error) {
22
+ // The command failed, likely because gcloud is not installed or not in the PATH.
23
+ console.error("\n[Error] Google Cloud SDK (gcloud CLI) not found.");
24
+ console.error(
25
+ "This script requires the gcloud CLI to manage authentication and Google Cloud services."
26
+ );
27
+ console.error("Please install it by following the official instructions:");
28
+ console.error("https://cloud.google.com/sdk/gcloud");
29
+ process.exit(1); // Exit the script with an error code.
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Helper function to run a shell command and print its output.
35
+ * @param {string} command The command to execute.
36
+ */
37
+ function runCommand(command) {
38
+ try {
39
+ // Execute the command, inheriting stdio to show output/errors in real-time.
40
+ execSync(command, { stdio: "inherit" });
41
+ } catch (error) {
42
+ console.error(`\nError executing command: ${command}`);
43
+ // The error message from the command is already shown due to 'inherit' stdio.
44
+ process.exit(1);
45
+ }
46
+ }
47
+
48
+ // --- Exported Command Implementations ---
49
+
50
+ /**
51
+ * Handles the 'init' command to configure the .env file.
52
+ */
53
+ export async function initializeConfiguration() {
54
+ // Define the path to the .env file in the current working directory.
55
+ const envPath = path.join(process.cwd(), ".env");
56
+ let existingConfig = {};
57
+
58
+ // --- Load existing values from .env file if it exists ---
59
+ if (fs.existsSync(envPath)) {
60
+ console.log(
61
+ "Found existing .env file. Loading current values as defaults."
62
+ );
63
+ existingConfig = dotenv.config({ path: envPath , quiet: true }).parsed || {};
64
+ }
65
+
66
+ console.log("--------------------------------------------------");
67
+ console.log("Configuring .env for gas-fakes");
68
+ console.log("Press Enter to accept the default value in brackets.");
69
+ console.log("--------------------------------------------------");
70
+
71
+ const questions = [
72
+ {
73
+ type: "text",
74
+ name: "GCP_PROJECT_ID",
75
+ message: "Enter your GCP Project ID",
76
+ initial: existingConfig.GCP_PROJECT_ID || "",
77
+ },
78
+ {
79
+ type: "text",
80
+ name: "DRIVE_TEST_FILE_ID",
81
+ message: "Enter a test Drive file ID for authentication checks",
82
+ initial: existingConfig.DRIVE_TEST_FILE_ID || "",
83
+ },
84
+ {
85
+ type: "text",
86
+ name: "CLIENT_CREDENTIAL_FILE",
87
+ message: "Enter path to OAuth client credentials JSON (optional)",
88
+ initial: existingConfig.CLIENT_CREDENTIAL_FILE || "",
89
+ },
90
+ {
91
+ type: "text",
92
+ name: "DEFAULT_SCOPES",
93
+ message: "Enter default scopes",
94
+ initial:
95
+ existingConfig.DEFAULT_SCOPES ||
96
+ "https://www.googleapis.com/auth/userinfo.email,openid,https://www.googleapis.com/auth/cloud-platform",
97
+ },
98
+ {
99
+ type: "text",
100
+ name: "EXTRA_SCOPES",
101
+ message: "Enter any extra scopes (comma-separated)",
102
+ initial:
103
+ existingConfig.EXTRA_SCOPES ||
104
+ ",https://www.googleapis.com/auth/drive,https://www.googleapis.com/auth/spreadsheets",
105
+ },
106
+ {
107
+ type: "select",
108
+ name: "LOG_DESTINATION",
109
+ message: "Enter logging destination",
110
+ choices: [
111
+ { title: "CONSOLE", value: "CONSOLE" },
112
+ { title: "CLOUD", value: "CLOUD" },
113
+ { title: "BOTH", value: "BOTH" },
114
+ { title: "NONE", value: "NONE" },
115
+ ],
116
+ initial:
117
+ ["CONSOLE", "CLOUD", "BOTH", "NONE"].indexOf(
118
+ existingConfig.LOG_DESTINATION
119
+ ) > -1
120
+ ? ["CONSOLE", "CLOUD", "BOTH", "NONE"].indexOf(
121
+ existingConfig.LOG_DESTINATION
122
+ )
123
+ : 0,
124
+ },
125
+ {
126
+ type: "select",
127
+ name: "STORE_TYPE",
128
+ message: "Enter storage type",
129
+ choices: [
130
+ { title: "FILE", value: "FILE" },
131
+ { title: "UPSTASH", value: "UPSTASH" },
132
+ ],
133
+ initial:
134
+ ["FILE", "UPSTASH"].indexOf(existingConfig.STORE_TYPE?.toUpperCase()) >
135
+ -1
136
+ ? ["FILE", "UPSTASH"].indexOf(existingConfig.STORE_TYPE.toUpperCase())
137
+ : 0,
138
+ },
139
+ ];
140
+
141
+ const responses = await prompts(questions);
142
+
143
+ if (responses.STORE_TYPE === "UPSTASH") {
144
+ console.log(
145
+ "Upstash storage selected. Please provide your Redis credentials."
146
+ );
147
+ const upstashQuestions = [
148
+ {
149
+ type: "text",
150
+ name: "UPSTASH_REDIS_REST_URL",
151
+ message: "Enter your Upstash Redis REST URL",
152
+ initial: existingConfig.UPSTASH_REDIS_REST_URL || "",
153
+ },
154
+ {
155
+ type: "text",
156
+ name: "UPSTASH_REDIS_REST_TOKEN",
157
+ message: "Enter your Upstash Redis REST Token",
158
+ initial: existingConfig.UPSTASH_REDIS_REST_TOKEN || "",
159
+ },
160
+ ];
161
+ const upstashResponses = await prompts(upstashQuestions);
162
+ Object.assign(responses, upstashResponses);
163
+ }
164
+
165
+ const finalConfig = { ...existingConfig, ...responses };
166
+
167
+ console.log("--------------------------------------------------");
168
+ console.log(`Writing configuration to ${envPath}...`);
169
+
170
+ let envContent = `
171
+ # Google Cloud Project ID (required)
172
+ GCP_PROJECT_ID="${finalConfig.GCP_PROJECT_ID || ""}"
173
+
174
+ # Path to OAuth client credentials for restricted scopes (optional)
175
+ CLIENT_CREDENTIAL_FILE="${finalConfig.CLIENT_CREDENTIAL_FILE || ""}"
176
+
177
+ # A test file ID for checking authentication (optional)
178
+ DRIVE_TEST_FILE_ID="${finalConfig.DRIVE_TEST_FILE_ID || ""}"
179
+
180
+ # Storage configuration for PropertiesService and CacheService ('FILE' or 'UPSTASH')
181
+ STORE_TYPE="${finalConfig.STORE_TYPE || "FILE"}"
182
+
183
+ # Logging destination for Logger.log() ('CONSOLE', 'CLOUD', 'BOTH', 'NONE')
184
+ LOG_DESTINATION="${finalConfig.LOG_DESTINATION || "CONSOLE"}"
185
+
186
+ # Scopes for authentication
187
+ # these are the scopes set by default - take some of these out if you want to minimize access
188
+ DEFAULT_SCOPES="${finalConfig.DEFAULT_SCOPES || ""}"
189
+ EXTRA_SCOPES="${finalConfig.EXTRA_SCOPES || ""}"
190
+ `.trim();
191
+
192
+ if (
193
+ finalConfig.UPSTASH_REDIS_REST_URL &&
194
+ finalConfig.UPSTASH_REDIS_REST_TOKEN
195
+ ) {
196
+ envContent += `
197
+
198
+ # Upstash credentials (only used if STORE_TYPE is 'UPSTASH')
199
+ UPSTASH_REDIS_REST_URL="${finalConfig.UPSTASH_REDIS_REST_URL}"
200
+ UPSTASH_REDIS_REST_TOKEN="${finalConfig.UPSTASH_REDIS_REST_TOKEN}"
201
+ `;
202
+ }
203
+
204
+ fs.writeFileSync(envPath, envContent);
205
+
206
+ console.log("Setup complete. Your .env file has been updated.");
207
+ console.log("--------------------------------------------------");
208
+ }
209
+
210
+ /**
211
+ * Handles the 'auth' command to authenticate with Google Cloud.
212
+ */
213
+ export function authenticateUser() {
214
+ // First, check if gcloud CLI is available.
215
+ checkForGcloudCli();
216
+
217
+ const rootDirectory = process.cwd();
218
+ const envPath = path.join(rootDirectory, ".env");
219
+
220
+ if (!fs.existsSync(envPath)) {
221
+ console.error(`Error: .env file not found at '${envPath}'`);
222
+ console.error("Please run './gas-fakes.js init' first.");
223
+ process.exit(1);
224
+ }
225
+
226
+ dotenv.config({ path: envPath });
227
+
228
+ const {
229
+ GCP_PROJECT_ID,
230
+ DEFAULT_SCOPES,
231
+ EXTRA_SCOPES,
232
+ CLIENT_CREDENTIAL_FILE,
233
+ AC,
234
+ } = process.env;
235
+
236
+ if (!GCP_PROJECT_ID) {
237
+ console.error("Error: GCP_PROJECT_ID is not set in your .env file.");
238
+ process.exit(1);
239
+ }
240
+
241
+ const defaultScopes =
242
+ DEFAULT_SCOPES ||
243
+ "https://www.googleapis.com/auth/userinfo.email,openid,https://www.googleapis.com/auth/cloud-platform";
244
+ const extraScopes =
245
+ EXTRA_SCOPES ||
246
+ "https://www.googleapis.com/auth/drive,https://www.googleapis.com/auth/spreadsheets";
247
+
248
+ let scopes = defaultScopes;
249
+ if (extraScopes && extraScopes.length > 0) {
250
+ scopes += (extraScopes.startsWith(",") ? "" : ",") + extraScopes;
251
+ }
252
+
253
+ const driveAccessFlag = "--enable-gdrive-access";
254
+
255
+ console.log(`...requesting scopes ${scopes}`);
256
+
257
+ let clientFlag = "";
258
+ if (CLIENT_CREDENTIAL_FILE) {
259
+ console.log("...attempting to use enhanced client credentials");
260
+
261
+ let clientPath = CLIENT_CREDENTIAL_FILE;
262
+ if (!path.isAbsolute(clientPath)) {
263
+ clientPath = path.join(rootDirectory, clientPath);
264
+ }
265
+
266
+ if (fs.existsSync(clientPath)) {
267
+ clientFlag = `--client-id-file="${clientPath}"`;
268
+ } else {
269
+ console.error(
270
+ `Error: Client credential file specified in .env not found at '${clientPath}'`
271
+ );
272
+ process.exit(1);
273
+ }
274
+ } else {
275
+ console.log(
276
+ "\n...CLIENT_CREDENTIAL_FILE is not set. Using default Application Default Credentials (ADC)."
277
+ );
278
+ console.log(
279
+ "...if you have requested any sensitive scopes, you'll see 'This app is blocked message.'"
280
+ );
281
+ console.log(
282
+ "...To allow them see - https://github.com/brucemcpherson/gas-fakes/blob/main/GETTING_STARTED.md\n"
283
+ );
284
+ }
285
+
286
+ const projectId = GCP_PROJECT_ID;
287
+ const activeConfig = AC || "default";
288
+
289
+ console.log("Revoking previous credentials...");
290
+ try {
291
+ execSync("gcloud auth revoke --quiet", { stdio: "ignore" });
292
+ } catch (e) {
293
+ /* ignore */
294
+ }
295
+ try {
296
+ execSync("gcloud auth application-default revoke --quiet", {
297
+ stdio: "ignore",
298
+ });
299
+ } catch (e) {
300
+ /* ignore */
301
+ }
302
+
303
+ console.log(`Ensuring gcloud configuration '${activeConfig}' exists...`);
304
+ try {
305
+ execSync(`gcloud config configurations describe "${activeConfig}"`, {
306
+ stdio: "ignore",
307
+ });
308
+ console.log(`Configuration '${activeConfig}' already exists.`);
309
+ } catch (error) {
310
+ console.log(`Configuration '${activeConfig}' not found. Creating it...`);
311
+ runCommand(`gcloud config configurations create "${activeConfig}"`);
312
+ }
313
+
314
+ console.log(`Activating gcloud configuration: ${activeConfig}`);
315
+ runCommand(`gcloud config configurations activate "${activeConfig}"`);
316
+
317
+ console.log(`Setting project to: ${projectId}`);
318
+ runCommand(`gcloud config set project ${projectId}`);
319
+ runCommand(`gcloud config set billing/quota_project ${projectId}`);
320
+
321
+ console.log("Initiating user login...");
322
+ runCommand(`gcloud auth login ${driveAccessFlag}`);
323
+
324
+ console.log("Initiating Application Default Credentials (ADC) login...");
325
+ runCommand(
326
+ `gcloud auth application-default login --scopes="${scopes}" ${clientFlag}`
327
+ );
328
+ runCommand(`gcloud auth application-default set-quota-project ${projectId}`);
329
+
330
+ // --- Verification ---
331
+ console.log("\nVerifying configuration...");
332
+
333
+ const gcloudConfigDir =
334
+ process.env.CLOUDSDK_CONFIG || path.join(os.homedir(), ".config", "gcloud");
335
+ const activeConfigPath = path.join(gcloudConfigDir, "active_config");
336
+
337
+ let currentConfig = "unknown";
338
+ if (fs.existsSync(activeConfigPath)) {
339
+ currentConfig = fs.readFileSync(activeConfigPath, "utf8").trim();
340
+ } else {
341
+ console.warn(
342
+ `Warning: Could not find active_config file at ${activeConfigPath}`
343
+ );
344
+ }
345
+
346
+ const currentProject = execSync("gcloud config get project")
347
+ .toString()
348
+ .trim();
349
+ console.log(
350
+ `Active config is ${currentConfig} - project is ${currentProject}`
351
+ );
352
+
353
+ console.log("\nFetching token information...");
354
+ const userToken = execSync("gcloud auth print-access-token")
355
+ .toString()
356
+ .trim();
357
+ const appDefaultToken = execSync(
358
+ "gcloud auth application-default print-access-token"
359
+ )
360
+ .toString()
361
+ .trim();
362
+
363
+ console.log("\n...user token scopes");
364
+ runCommand(
365
+ `curl https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=${userToken}`
366
+ );
367
+
368
+ console.log("\n...application default token scopes");
369
+ runCommand(
370
+ `curl https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=${appDefaultToken}`
371
+ );
372
+ console.log("\nAuthentication process finished.");
373
+ }
374
+
375
+ /**
376
+ * Handles the 'enableAPIs' command to enable necessary Google Cloud services.
377
+ */
378
+ export function enableGoogleAPIs() {
379
+ // First, check if gcloud CLI is available.
380
+ checkForGcloudCli();
381
+
382
+ const services = [
383
+ "drive.googleapis.com",
384
+ "sheets.googleapis.com",
385
+ "forms.googleapis.com",
386
+ "docs.googleapis.com",
387
+ "gmail.googleapis.com",
388
+ "logging.googleapis.com",
389
+ ];
390
+
391
+ console.log("Enabling necessary Google Cloud services...");
392
+ runCommand(`gcloud services enable ${services.join(" ")}`);
393
+ console.log("Services enabled successfully.");
394
+ }