@mcpher/gas-fakes 1.2.18 → 1.2.19

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
@@ -332,7 +332,7 @@ const getParentsIterator = ({
332
332
  - [named colors](named-colors.md)
333
333
  - [sandbox](sandbox.md)
334
334
 
335
- ## <img src="./logo.png" alt="gas-fakes logo" width="50" align="top"> Further Reading
335
+ ## <img src="./logo.png" alt="gas-fakes logo" width="50" align="top"> Further Reading
336
336
 
337
337
  - [getting started](GETTING_STARTED.md) - how to handle authentication for restricted scopes.
338
338
  - [readme](README.md)
@@ -359,8 +359,9 @@ 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
+ - [Modern Google Apps Script Workflow Building on the Cloud](https://medium.com/google-cloud/modern-google-apps-script-workflow-building-on-the-cloud-2255dbd32ac3)
362
363
  - [Bridging the Gap: Seamless Integration for Local Google Apps Script Development](https://medium.com/@tanaike/bridging-the-gap-seamless-integration-for-local-google-apps-script-development-9b9b973aeb02)
363
364
  - [Next-Level Google Apps Script Development](https://medium.com/google-cloud/next-level-google-apps-script-development-654be5153912)
364
365
  - [Secure and Streamlined Google Apps Script Development with gas-fakes CLI and Gemini CLI Extension](https://medium.com/google-cloud/secure-and-streamlined-google-apps-script-development-with-gas-fakes-cli-and-gemini-cli-extension-67bbce80e2c8)
365
366
  - [Secure and Conversational Google Workspace Automation: Integrating Gemini CLI with a gas-fakes MCP Server](https://medium.com/google-cloud/secure-and-conversational-google-workspace-automation-integrating-gemini-cli-with-a-gas-fakes-mcp-0a5341559865)
366
- - [A Fake-Sandbox for Google Apps Script: A Feasibility Study on Securely Executing Code Generated by Gemini CL](https://medium.com/google-cloud/a-fake-sandbox-for-google-apps-script-a-feasibility-study-on-securely-executing-code-generated-by-cc985ce5dae3)
367
+ - [A Fake-Sandbox for Google Apps Script: A Feasibility Study on Securely Executing Code Generated by Gemini CL](https://medium.com/google-cloud/a-fake-sandbox-for-google-apps-script-a-feasibility-study-on-securely-executing-code-generated-by-cc985ce5dae3)
package/README.md CHANGED
@@ -151,7 +151,7 @@ For inspiration on pushing modified files to the IDE, see the togas.sh bash scri
151
151
 
152
152
  As I mentioned earlier, to take this further, I'm going to need a lot of help to extend the methods and services supported - so if you feel this would be useful to you, and would like to collaborate, please ping me on bruce@mcpher.com and we'll talk.
153
153
 
154
- ## <img src="./logo.png" alt="gas-fakes logo" width="50" align="top"> Further Reading
154
+ ## <img src="./logo.png" alt="gas-fakes logo" width="50" align="top"> Further Reading
155
155
 
156
156
  - [getting started](GETTING_STARTED.md) - how to handle authentication for restricted scopes.
157
157
  - [readme](README.md)
@@ -178,8 +178,9 @@ 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
+ - [Modern Google Apps Script Workflow Building on the Cloud](https://medium.com/google-cloud/modern-google-apps-script-workflow-building-on-the-cloud-2255dbd32ac3)
181
182
  - [Bridging the Gap: Seamless Integration for Local Google Apps Script Development](https://medium.com/@tanaike/bridging-the-gap-seamless-integration-for-local-google-apps-script-development-9b9b973aeb02)
182
183
  - [Next-Level Google Apps Script Development](https://medium.com/google-cloud/next-level-google-apps-script-development-654be5153912)
183
184
  - [Secure and Streamlined Google Apps Script Development with gas-fakes CLI and Gemini CLI Extension](https://medium.com/google-cloud/secure-and-streamlined-google-apps-script-development-with-gas-fakes-cli-and-gemini-cli-extension-67bbce80e2c8)
184
185
  - [Secure and Conversational Google Workspace Automation: Integrating Gemini CLI with a gas-fakes MCP Server](https://medium.com/google-cloud/secure-and-conversational-google-workspace-automation-integrating-gemini-cli-with-a-gas-fakes-mcp-0a5341559865)
185
- - [A Fake-Sandbox for Google Apps Script: A Feasibility Study on Securely Executing Code Generated by Gemini CL](https://medium.com/google-cloud/a-fake-sandbox-for-google-apps-script-a-feasibility-study-on-securely-executing-code-generated-by-cc985ce5dae3)
186
+ - [A Fake-Sandbox for Google Apps Script: A Feasibility Study on Securely Executing Code Generated by Gemini CL](https://medium.com/google-cloud/a-fake-sandbox-for-google-apps-script-a-feasibility-study-on-securely-executing-code-generated-by-cc985ce5dae3)
package/gas-fakes.js CHANGED
@@ -31,7 +31,7 @@ const VERSION = pjson.version;
31
31
  // CONSTANTS & UTILITIES
32
32
  // -----------------------------------------------------------------------------
33
33
 
34
- const CLI_VERSION = "0.0.11";
34
+ const CLI_VERSION = "0.0.12";
35
35
  const MCP_VERSION = "0.0.3";
36
36
  const execAsync = promisify(exec);
37
37
 
@@ -529,6 +529,7 @@ async function main() {
529
529
  .description(
530
530
  "Initializes the configuration by creating or updating the .env file."
531
531
  )
532
+ .option("-e, --env <path>", "Path to a custom .env file.")
532
533
  .action(initializeConfiguration);
533
534
 
534
535
  program
package/package.json CHANGED
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "name": "@mcpher/gas-fakes",
34
34
  "author": "bruce mcpherson",
35
- "version": "1.2.18",
35
+ "version": "1.2.19",
36
36
  "license": "MIT",
37
37
  "main": "main.js",
38
38
  "description": "A proof of concept implementation of Apps Script Environment on Node",
package/setup.js CHANGED
@@ -75,26 +75,51 @@ function runCommand(command) {
75
75
  }
76
76
 
77
77
  // --- Exported Command Implementations ---
78
+ export async function initializeConfiguration(options = {}) {
79
+ let envPath;
78
80
 
79
- /**
80
- * Handles the 'init' command to configure the .env file.
81
- */
82
- export async function initializeConfiguration() {
83
- let envPath = path.join(process.cwd(), ".env");
84
- const searchPath = process.cwd(); // or process.env.HOME
85
- const absoluteSearchPath = path.resolve(searchPath);
86
- const foundFiles = await findEnvFiles(absoluteSearchPath);
87
- if (foundFiles.length > 0) {
88
- // Check .env on the top directory.
89
- const results = foundFiles
90
- .map((file) => file.split("/"))
91
- .sort((a, b) => (a.length > b.length ? 1 : -1));
92
- envPath = results[0].join("/");
81
+ if (options.env) {
82
+ envPath = path.resolve(process.cwd(), options.env);
83
+ console.log(`-> Using specified .env path: ${envPath}`);
84
+ } else {
85
+ const foundFiles = await findEnvFiles(process.cwd());
86
+ if (foundFiles.length > 0) {
87
+ const choices = foundFiles.map((file) => ({
88
+ title: file,
89
+ value: file,
90
+ }));
91
+ choices.push({
92
+ title: "Create a new .env file in the current directory",
93
+ value: "new",
94
+ });
95
+
96
+ const response = await prompts({
97
+ type: "select",
98
+ name: "envPathSelection",
99
+ message: "Found .env file(s). Which one would you like to use?",
100
+ choices: choices,
101
+ });
102
+
103
+ if (typeof response.envPathSelection === "undefined") {
104
+ console.log("Initialization cancelled.");
105
+ return;
106
+ }
107
+
108
+ if (response.envPathSelection === "new") {
109
+ envPath = path.join(process.cwd(), ".env");
110
+ } else {
111
+ envPath = response.envPathSelection;
112
+ }
113
+ } else {
114
+ console.log(
115
+ "No .env file found. A new one will be created in the current directory."
116
+ );
117
+ envPath = path.join(process.cwd(), ".env");
118
+ }
119
+ console.log(`-> Using .env file at: ${envPath}`);
93
120
  }
94
121
 
95
122
  let existingConfig = {};
96
-
97
- // Load existing values from .env file if it exists to use as defaults for prompts.
98
123
  if (existsSync(envPath)) {
99
124
  console.log(
100
125
  "Found existing .env file. Loading current values as defaults."
@@ -105,91 +130,239 @@ export async function initializeConfiguration() {
105
130
  console.log("--------------------------------------------------");
106
131
  console.log("Configuring .env for gas-fakes");
107
132
  console.log("Press Enter to accept the default value in brackets.");
133
+ console.log("Use Space to select/deselect scopes.");
108
134
  console.log("--------------------------------------------------");
109
135
 
110
- const questions = [
136
+ const existingExtraScopes = existingConfig.EXTRA_SCOPES
137
+ ? existingConfig.EXTRA_SCOPES.split(",").filter((s) => s)
138
+ : [];
139
+
140
+ const responses = {};
141
+
142
+ // --- Stage 1: Basic Info ---
143
+ const basicInfoQuestions = [
111
144
  {
112
145
  type: "text",
113
146
  name: "GCP_PROJECT_ID",
114
147
  message: "Enter your GCP Project ID",
115
- initial: existingConfig.GCP_PROJECT_ID || "",
148
+ initial: existingConfig.GCP_PROJECT_ID || process.env.GOOGLE_CLOUD_PROJECT,
116
149
  },
117
150
  {
118
151
  type: "text",
119
152
  name: "DRIVE_TEST_FILE_ID",
120
- message: "Enter a test Drive file ID for authentication checks",
153
+ message: "Enter a test Drive file ID for authentication checks (optional)",
121
154
  initial: existingConfig.DRIVE_TEST_FILE_ID || "",
122
155
  },
123
- {
124
- type: "text",
125
- name: "CLIENT_CREDENTIAL_FILE",
126
- message: "Enter path to OAuth client credentials JSON (optional)",
127
- initial: existingConfig.CLIENT_CREDENTIAL_FILE || "",
128
- },
129
- {
130
- type: "text",
131
- name: "DEFAULT_SCOPES",
132
- message: "Enter default scopes",
133
- initial:
134
- existingConfig.DEFAULT_SCOPES ||
135
- "https://www.googleapis.com/auth/userinfo.email,openid,https://www.googleapis.com/auth/cloud-platform",
136
- },
137
- {
138
- type: "text",
139
- name: "EXTRA_SCOPES",
140
- message: "Enter any extra scopes (comma-separated)",
141
- initial:
142
- existingConfig.EXTRA_SCOPES ||
143
- ",https://www.googleapis.com/auth/drive,https://www.googleapis.com/auth/spreadsheets",
156
+ ];
157
+
158
+
159
+ const basicInfoResponses = await prompts(basicInfoQuestions);
160
+ if (typeof basicInfoResponses.GCP_PROJECT_ID === "undefined") {
161
+ console.log("Initialization cancelled.");
162
+ return;
163
+ }
164
+ Object.assign(responses, basicInfoResponses);
165
+
166
+ // --- Stage 2: Scopes ---
167
+
168
+ const DEFAULT_SCOPES_VALUES = [
169
+ "https://www.googleapis.com/auth/userinfo.email",
170
+ "openid",
171
+ "https://www.googleapis.com/auth/cloud-platform",
172
+ ];
173
+ console.log(
174
+ "\nThe following default scopes are required for basic operations and will be enabled automatically:"
175
+ );
176
+ DEFAULT_SCOPES_VALUES.forEach((scope) => console.log(` - ${scope}`));
177
+ responses.DEFAULT_SCOPES = DEFAULT_SCOPES_VALUES;
178
+
179
+
180
+ const extraScopeQuestion = {
181
+ type: "multiselect",
182
+ name: "EXTRA_SCOPES",
183
+ message: "Select any extra scopes your script requires",
184
+
185
+ // i think we only need to have drive (which we must have for all the others anyway)
186
+ choices: [
187
+ {
188
+ title: "Workspace resources",
189
+ value: "https://www.googleapis.com/auth/drive",
190
+ },
191
+ /*
192
+ {
193
+ title: "Sheets (full access)",
194
+ value: "https://www.googleapis.com/auth/spreadsheets",
195
+ },
196
+ {
197
+ title: "Docs (full access)",
198
+ value: "https://www.googleapis.com/auth/documents",
199
+ },
200
+ {
201
+ title: "Forms (full access)",
202
+ value: "https://www.googleapis.com/auth/forms",
203
+ },
204
+ {
205
+ title: "Gmail (send mail)",
206
+ value: "https://www.googleapis.com/auth/gmail.send",
207
+ },
208
+ {
209
+ title: "Gmail (full access)",
210
+ value: "https://www.googleapis.com/auth/gmail.modify",
211
+ },
212
+ */
213
+ {
214
+ // actually labels are not sensitive
215
+ title: "Gmail labels",
216
+ value: "https://www.googleapis.com/auth/gmail.labels",
217
+ },
218
+ {
219
+ sensitivity: "sensitive",
220
+ title: "Gmail compose",
221
+ value: "https://www.googleapis.com/auth/gmail.compose",
222
+ },
223
+
224
+ ].map((scope) => ({
225
+ ...scope,
226
+ title: scope.sensitivity ? `[${scope.sensitivity}] ${scope.title}` : scope.title,
227
+ // because we always need drive for ant extra scopes
228
+ selected:
229
+ existingExtraScopes.length > 0
230
+ ? existingExtraScopes.includes(scope.value)
231
+ : scope.value === "https://www.googleapis.com/auth/drive",
232
+ })),
233
+ hint: "- Use space to select/deselect. Press Enter to submit.",
234
+ };
235
+
236
+ // to check for any kind of sensitivity
237
+ const sensitiveScopesList = extraScopeQuestion.choices.filter(
238
+ (scope) => scope.sensitivity
239
+ );
240
+
241
+ const extraScopeResponses = await prompts(extraScopeQuestion);
242
+
243
+ if (typeof extraScopeResponses.EXTRA_SCOPES === "undefined") {
244
+ console.log("Initialization cancelled.");
245
+ return;
246
+ }
247
+ Object.assign(responses, extraScopeResponses);
248
+
249
+ const selectedExtraScopes = responses.EXTRA_SCOPES || [];
250
+
251
+ const usesSensitiveScopes = sensitiveScopesList.some((s) =>
252
+ selectedExtraScopes.includes(s.value)
253
+ );
254
+
255
+
256
+ if (usesSensitiveScopes) {
257
+ console.log("\n--------------------------------------------------");
258
+ console.log("You have selected sensitive or restricted scopes. Google requires an OAuth client credential file for these.");
259
+ console.log('See the getting started guide https://github.com/brucemcpherson/gas-fakes/blob/main/GETTING_STARTED.md for how.')
260
+ console.log("--------------------------------------------------");
261
+ }
262
+
263
+
264
+ const clientCredentialQuestion = {
265
+ type: "text",
266
+ name: "CLIENT_CREDENTIAL_FILE",
267
+ message: usesSensitiveScopes
268
+ ? "Enter the path and filename for your OAuth client credentials JSON"
269
+ : "Enter path to OAuth client credentials JSON (optional)",
270
+ initial: existingConfig.CLIENT_CREDENTIAL_FILE || "",
271
+ validate: (input) => {
272
+ const trimmedInput = input.trim();
273
+
274
+ if (usesSensitiveScopes) {
275
+ if (trimmedInput === "") {
276
+ return "This field is required for the selected sensitive scopes.";
277
+ }
278
+ } else {
279
+ if (trimmedInput === "") {
280
+ return true;
281
+ }
282
+ }
283
+
284
+ const resolvedPath = path.resolve(process.cwd(), trimmedInput);
285
+ if (!existsSync(resolvedPath)) {
286
+ return `File not found at '${resolvedPath}'. Please check the path and try again.`;
287
+ }
288
+
289
+ return true;
144
290
  },
145
- {
146
- type: "select",
147
- name: "LOG_DESTINATION",
148
- message: "Enter logging destination",
149
- choices: [
150
- { title: "CONSOLE", value: "CONSOLE" },
151
- { title: "CLOUD", value: "CLOUD" },
152
- { title: "BOTH", value: "BOTH" },
153
- { title: "NONE", value: "NONE" },
154
- ],
155
- initial:
156
- ["CONSOLE", "CLOUD", "BOTH", "NONE"].indexOf(
291
+ };
292
+
293
+ const clientCredentialResponse = await prompts(clientCredentialQuestion);
294
+ if (typeof clientCredentialResponse.CLIENT_CREDENTIAL_FILE === "undefined") {
295
+ console.log("Initialization cancelled.");
296
+ return;
297
+ }
298
+ Object.assign(responses, clientCredentialResponse);
299
+
300
+ // --- Stage 3: Remaining Config ---
301
+ const defaultScopesDisplay = `\n - Default: [${responses.DEFAULT_SCOPES.join(
302
+ ", "
303
+ )}]`;
304
+ const extraScopesDisplay =
305
+ responses.EXTRA_SCOPES && responses.EXTRA_SCOPES.length > 0
306
+ ? `\n - Extra: [${responses.EXTRA_SCOPES.join(", ")}]`
307
+ : "\n - Extra: [None]";
308
+
309
+ const remainingQuestions = [{
310
+ type: "toggle",
311
+ name: "QUIET",
312
+ message: "Run gas-fakes package in quiet mode",
313
+ initial: existingConfig.QUIET === "true" ? true : false,
314
+ },
315
+ {
316
+ type: "select",
317
+ name: "LOG_DESTINATION",
318
+ message: `Selected Scopes:${defaultScopesDisplay}${extraScopesDisplay}\n\nEnter logging destination`,
319
+ choices: [
320
+ { title: "CONSOLE", value: "CONSOLE" },
321
+ { title: "CLOUD", value: "CLOUD" },
322
+ { title: "BOTH", value: "BOTH" },
323
+ { title: "NONE", value: "NONE" },
324
+ ],
325
+ initial:
326
+ ["CONSOLE", "CLOUD", "BOTH", "NONE"].indexOf(
327
+ existingConfig.LOG_DESTINATION
328
+ ) > -1
329
+ ? ["CONSOLE", "CLOUD", "BOTH", "NONE"].indexOf(
157
330
  existingConfig.LOG_DESTINATION
158
- ) > -1
159
- ? ["CONSOLE", "CLOUD", "BOTH", "NONE"].indexOf(
160
- existingConfig.LOG_DESTINATION
161
- )
162
- : 0,
163
- },
164
- {
165
- type: "select",
166
- name: "STORE_TYPE",
167
- message: "Enter storage type",
168
- choices: [
169
- { title: "FILE", value: "FILE" },
170
- { title: "UPSTASH", value: "UPSTASH" },
171
- ],
172
- initial:
173
- ["FILE", "UPSTASH"].indexOf(existingConfig.STORE_TYPE?.toUpperCase()) >
331
+ )
332
+ : 0,
333
+ },
334
+ {
335
+ type: "select",
336
+ name: "STORE_TYPE",
337
+ message: "Enter storage type",
338
+ choices: [
339
+ { title: "FILE", value: "FILE" },
340
+ { title: "UPSTASH", value: "UPSTASH" },
341
+ ],
342
+ initial:
343
+ ["FILE", "UPSTASH"].indexOf(existingConfig.STORE_TYPE?.toUpperCase()) >
174
344
  -1
175
- ? ["FILE", "UPSTASH"].indexOf(existingConfig.STORE_TYPE.toUpperCase())
176
- : 0,
177
- },
345
+ ? ["FILE", "UPSTASH"].indexOf(existingConfig.STORE_TYPE.toUpperCase())
346
+ : 0,
347
+ }
178
348
  ];
179
349
 
180
- const responses = await prompts(questions);
181
-
182
- // If the user cancels (e.g., Ctrl+C), prompts returns undefined for the keys.
183
- if (typeof responses.GCP_PROJECT_ID === "undefined") {
350
+ const remainingResponses = await prompts(remainingQuestions);
351
+ if (typeof remainingResponses.LOG_DESTINATION === "undefined") {
184
352
  console.log("Initialization cancelled.");
185
- return; // Exit the function without writing to file.
353
+ return;
354
+ }
355
+ Object.assign(responses, remainingResponses);
356
+
357
+ // Convert scope arrays to comma-separated strings for saving
358
+ if (Array.isArray(responses.DEFAULT_SCOPES)) {
359
+ responses.DEFAULT_SCOPES = responses.DEFAULT_SCOPES.join(",");
360
+ }
361
+ if (Array.isArray(responses.EXTRA_SCOPES)) {
362
+ responses.EXTRA_SCOPES = responses.EXTRA_SCOPES.join(",");
186
363
  }
187
364
 
188
- // If Upstash is selected, ask for its credentials.
189
365
  if (responses.STORE_TYPE === "UPSTASH") {
190
- console.log(
191
- "Upstash storage selected. Please provide your Redis credentials."
192
- );
193
366
  const upstashQuestions = [
194
367
  {
195
368
  type: "text",
@@ -213,64 +386,70 @@ export async function initializeConfiguration() {
213
386
  Object.assign(responses, upstashResponses);
214
387
  }
215
388
 
216
- console.log("--------------------------------------------------");
217
- console.log(`Writing configuration to ${envPath}...`);
218
-
219
- if (!existsSync(envPath)) {
220
- // --- Create a new .env file from a template ---
221
- let envContent = `
222
- # Google Cloud Project ID (required)
223
- GCP_PROJECT_ID="${responses.GCP_PROJECT_ID || ""}"
224
-
225
- # Path to OAuth client credentials for restricted scopes (optional)
226
- CLIENT_CREDENTIAL_FILE="${responses.CLIENT_CREDENTIAL_FILE || ""}"
227
-
228
- # A test file ID for checking authentication (optional)
229
- DRIVE_TEST_FILE_ID="${responses.DRIVE_TEST_FILE_ID || ""}"
230
-
231
- # Storage configuration for PropertiesService and CacheService ('FILE' or 'UPSTASH')
232
- STORE_TYPE="${responses.STORE_TYPE || "FILE"}"
389
+ // --- Confirmation Step ---
390
+ console.log("\n------------------ Summary ------------------");
391
+ Object.entries(responses).forEach(([key, value]) => {
392
+ if (value !== undefined) console.log(`${key}: ${value}`);
393
+ });
394
+ console.log("-------------------------------------------");
395
+
396
+ const confirmSave = await prompts({
397
+ type: "confirm",
398
+ name: "save",
399
+ message: `Save this configuration to ${envPath}?`,
400
+ initial: true,
401
+ });
402
+
403
+ if (!confirmSave.save) {
404
+ console.log("Configuration discarded. No changes were made.");
405
+ return;
406
+ }
233
407
 
234
- # Logging destination for Logger.log() ('CONSOLE', 'CLOUD', 'BOTH', 'NONE')
235
- LOG_DESTINATION="${responses.LOG_DESTINATION || "CONSOLE"}"
408
+ // --- File Writing Logic ---
409
+ console.log(`Writing configuration to ${envPath}...`);
410
+ const inits = responses.STORE_TYPE !== "UPSTASH" ? { UPSTASH_REDIS_REST_TOKEN: "", UPSTASH_REDIS_REST_URL: "" } : {}
411
+ const finalConfig = { ...existingConfig, ...responses, ...inits };
236
412
 
237
- # Scopes for authentication
238
- # these are the scopes set by default - take some of these out if you want to minimize access
239
- DEFAULT_SCOPES="${responses.DEFAULT_SCOPES || ""}"
240
- EXTRA_SCOPES="${responses.EXTRA_SCOPES || ""}"
241
- `.trim();
413
+ const envContent = Reflect.ownKeys(finalConfig).map((key) => {
242
414
 
243
- if (responses.STORE_TYPE === "UPSTASH") {
415
+ const item = finalConfig[key];
416
+ console.log (key, item)
417
+ return `${key}="${(item.toString() || "").trim()}"`;
418
+ }).join("\n")
419
+ /* replacing this to include existing values
420
+ let envContent = `
421
+ # Google Cloud Project ID (required)
422
+ GCP_PROJECT_ID="${finalConfig.GCP_PROJECT_ID || ""}"
423
+
424
+ # Path to OAuth client credentials for restricted scopes (optional)
425
+ CLIENT_CREDENTIAL_FILE="${finalConfig.CLIENT_CREDENTIAL_FILE || ""}"
426
+
427
+ # A test file ID for checking authentication (optional)
428
+ DRIVE_TEST_FILE_ID="${finalConfig.DRIVE_TEST_FILE_ID || ""}"
429
+
430
+ # Storage configuration for PropertiesService and CacheService ('FILE' or 'UPSTASH')
431
+ STORE_TYPE="${finalConfig.STORE_TYPE || "FILE"}"
432
+
433
+ # Logging destination for Logger.log() ('CONSOLE', 'CLOUD', 'BOTH', 'NONE')
434
+ LOG_DESTINATION="${finalConfig.LOG_DESTINATION || "CONSOLE"}"
435
+
436
+ # Scopes for authentication
437
+ DEFAULT_SCOPES="${finalConfig.DEFAULT_SCOPES || ""}"
438
+ EXTRA_SCOPES="${finalConfig.EXTRA_SCOPES || ""}"
439
+ `.trim();
440
+
441
+ if (finalConfig.STORE_TYPE === "UPSTASH") {
244
442
  envContent += `
245
-
246
- # Upstash credentials (only used if STORE_TYPE is 'UPSTASH')
247
- UPSTASH_REDIS_REST_URL="${responses.UPSTASH_REDIS_REST_URL || ""}"
248
- UPSTASH_REDIS_REST_TOKEN="${responses.UPSTASH_REDIS_REST_TOKEN || ""}"
249
- `;
443
+
444
+ # Upstash credentials (only used if STORE_TYPE is 'UPSTASH')
445
+ UPSTASH_REDIS_REST_URL="${finalConfig.UPSTASH_REDIS_REST_URL || ""}"
446
+ UPSTASH_REDIS_REST_TOKEN="${finalConfig.UPSTASH_REDIS_REST_TOKEN || ""}"
447
+ `;
250
448
  }
251
- writeFileSync(envPath, envContent, "utf8");
252
- } else {
253
- // --- Update the existing .env file ---
254
- let envContent = readFileSync(envPath, "utf8");
255
-
256
- const configToUpdate = { ...responses };
257
-
258
- for (const key of Object.keys(configToUpdate)) {
259
- const value = configToUpdate[key] || "";
260
- const keyRegex = new RegExp(`^\\s*${key}\\s*=.*$`, "m");
261
-
262
- if (keyRegex.test(envContent)) {
263
- envContent = envContent.replace(keyRegex, `${key}="${value}"`);
264
- } else {
265
- if (envContent.length > 0 && !envContent.endsWith("\n")) {
266
- envContent += "\n";
267
- }
268
- envContent += `${key}="${value}"\n`;
269
- }
270
- }
271
- writeFileSync(envPath, envContent, "utf8");
272
- }
449
+ */
450
+ writeFileSync(envPath, envContent + "\n", "utf8");
273
451
 
452
+ console.log("--------------------------------------------------");
274
453
  console.log("Setup complete. Your .env file has been updated.");
275
454
  console.log("--------------------------------------------------");
276
455
  }
@@ -48,7 +48,6 @@ class FakeAdvDocuments extends FakeAdvResource {
48
48
 
49
49
  // Invalidate the cache for this document since we are updating it.
50
50
  docsCacher.clear(documentId);
51
- // console.log (JSON.stringify(requests))
52
51
  const { response, data } = this._call("batchUpdate", {
53
52
  documentId,
54
53
  requestBody: requests
@@ -8,13 +8,12 @@ export const lazyLoaderApp = (app, name, maker) => {
8
8
 
9
9
  // if it hasne been intialized yet then do that
10
10
  if (!app) {
11
- //console.log ('...loading', name)
12
11
  app = maker()
13
12
  }
14
13
  // this is the actual driveApp we'll return from the proxy
15
14
  return app
16
15
  }
17
- //console.log ('...registering', name)
16
+
18
17
  Proxies.registerProxy(name, getApp)
19
18
 
20
19
  }
@@ -2,7 +2,7 @@
2
2
  * @file Provides helper functions for working with fake document elements.
3
3
  */
4
4
 
5
-
5
+ import {slogger } from "../../support/slogger.js";
6
6
  import { Utils } from "../../support/utils.js";
7
7
  const { is } = Utils
8
8
 
@@ -38,7 +38,7 @@ export const getElementProp = (se) => {
38
38
  return { prop: 'body', type: 'BODY_SECTION' };
39
39
  }
40
40
 
41
- console.log(se);
41
+ slogger.error(se);
42
42
  throw new Error('couldnt establish structural element type');
43
43
  }
44
44
 
@@ -220,7 +220,7 @@ export const findItem = (elementMap, type, startIndex, segmentId) => {
220
220
  return f.__type === type && f.startIndex === startIndex;
221
221
  });
222
222
  if (!item) {
223
- console.log(elementMap.values())
223
+ slogger.error(elementMap.values())
224
224
  throw new Error(`Couldnt find element ${type} at ${startIndex} in segment ${segmentId || 'body'}`)
225
225
  }
226
226
  return item
@@ -91,7 +91,7 @@ class ShadowDocument {
91
91
  __twig: footnoteSectionTree,
92
92
  };
93
93
  this.__elementMap.set(footnoteSectionName, footnoteSectionElement);
94
- // console.log('named ranges after document fetch', JSON.stringify(currentNr))
94
+
95
95
 
96
96
  // maps all the elements to their named range
97
97
  const mapElements = (element, branch, segmentId, knownType = null) => {
@@ -127,7 +127,7 @@ class ShadowDocument {
127
127
  }
128
128
 
129
129
  if (!is.integer(element.endIndex) || !is.integer(element.startIndex)) {
130
- console.log(element);
130
+ slogger.error(element);
131
131
  throw new Error(`failed to find endindex/startindex for ${type}`);
132
132
  }
133
133
  // For an empty document, we use static, non-API names to avoid re-indexing issues.
@@ -13,7 +13,7 @@ import { isFolder, notYetImplemented, isFakeFolder, signatureArgs } from '../../
13
13
  import { getParentsIterator } from './driveiterators.js';
14
14
  import { improveFileCache } from "../../support/filecache.js"
15
15
  import { getSharers } from '../../support/filesharers.js';
16
-
16
+ import {slogger } from "../../support/slogger.js";
17
17
  /**
18
18
  * basic fake File meta data
19
19
  * these are shared between folders and files
@@ -34,7 +34,7 @@ export class FakeDriveMeta {
34
34
 
35
35
  __preventRootDamage = (operation) => {
36
36
  if (this.__isRoot) {
37
- console.log (`Can't do ${operation} on root folder`)
37
+ slogger.error (`Can't do ${operation} on root folder`)
38
38
  throw new Error("Access denied: DriveApp")
39
39
  }
40
40
  }
@@ -189,7 +189,7 @@ const writeToCloudOrConsole = (message, loggerInstance) => {
189
189
  headers,
190
190
  })
191
191
  if (response.getResponseCode() !== 200) {
192
- console.log ('logging failure', response.getContentText())
192
+ console.error ('logging failure', response.getContentText())
193
193
  }
194
194
 
195
195
  }
@@ -9,6 +9,7 @@ import { Auth } from '../../support/auth.js'
9
9
  import { Proxies } from '../../support/proxies.js'
10
10
  import { newFakeBehavior } from './behavior.js'
11
11
  import { newCacheDropin , getUserIdFromToken} from '@mcpher/gas-flex-cache'
12
+ import {slogger } from "../../support/slogger.js";
12
13
  /**
13
14
  * fake ScriptApp.getOAuthToken
14
15
  * @return {string} token
@@ -79,7 +80,7 @@ const checkScopesMatch = (required) => {
79
80
  ]
80
81
  const hasIgnore = ignores.includes(s)
81
82
  if (hasIgnore) {
82
- console.log('...ignoring requested scope for adc as google blocks it outside apps script' + s)
83
+ slogger.warn('...ignoring requested scope for adc as google blocks it outside apps script' + s)
83
84
  }
84
85
  // if drive is authorized and drive.readonly is required that's okay too
85
86
  // if drive.readonly is authorized and drive is requested thats not
@@ -1,5 +1,6 @@
1
1
  import { Proxies } from '../../support/proxies.js'
2
2
  import { Utils } from '../../support/utils.js'
3
+ import {slogger } from "../../support/slogger.js";
3
4
 
4
5
  const { is } = Utils
5
6
  const checkArgs = (actual, expect = "boolean") => {
@@ -250,7 +251,7 @@ class FakeBehavior {
250
251
  throw new Error(`Invalid sandbox id parameter (${id}) - must be a non-empty string`);
251
252
  }
252
253
  if (!this.isKnown(id)) {
253
- console.log(`...adding file ${id} to sandbox allowed list`);
254
+ slogger.log(`...adding file ${id} to sandbox allowed list`);
254
255
  this.__createdIds.add(id);
255
256
  }
256
257
  }
@@ -344,7 +345,7 @@ class FakeBehavior {
344
345
  trash() {
345
346
  // this is where we would trash all the created files
346
347
  if (!this.__cleanup) {
347
- console.log('...skipping cleaning up sandbox files')
348
+ slogger.log('...skipping cleaning up sandbox files')
348
349
  return [];
349
350
  }
350
351
 
@@ -357,14 +358,14 @@ class FakeBehavior {
357
358
  }
358
359
  if (d) {
359
360
  d.setTrashed(true);
360
- console.log(`...trashed file ${d.getName()} (${id})`);
361
+ slogger.log(`...trashed file ${d.getName()} (${id})`);
361
362
  acc.push(id);
362
363
  }
363
364
  return acc;
364
365
  }, []);
365
366
 
366
367
  this.__createdIds.clear();
367
- console.log(`...trashed ${trashed.length} sandboxed files`);
368
+ slogger.log(`...trashed ${trashed.length} sandboxed files`);
368
369
  return trashed;
369
370
  }
370
371
  isKnown(id) {
@@ -15,10 +15,8 @@ import { newFakeNamedRange } from "./fakenamedrange.js";
15
15
  import { newFakeProtection } from "./fakeprotection.js";
16
16
  import { newFakeOverGridImage } from "./fakeovergridimage.js";
17
17
 
18
- import { Syncit } from "../../support/syncit.js";
19
18
  import { XMLParser } from "fast-xml-parser";
20
- import { nullable } from "zod/v4";
21
-
19
+ import {slogger } from "../../support/slogger.js";
22
20
  const { is, isEnum } = Utils;
23
21
 
24
22
  export const newFakeSheet = (properties, parent) => {
@@ -1082,7 +1080,7 @@ export class FakeSheet {
1082
1080
  const p = parser.parse(b.getDataAsString());
1083
1081
  o[filename] = p;
1084
1082
  } catch (err) {
1085
- // console.log(err);
1083
+ slogger.error(err)
1086
1084
  }
1087
1085
  return o;
1088
1086
  }, {});
@@ -1,5 +1,5 @@
1
1
  import { Utils } from '../../support/utils.js'
2
-
2
+ import {slogger } from "../../support/slogger.js";
3
3
  const { is, hexToRgb } = Utils
4
4
 
5
5
  export const bandingThemeMap = {
@@ -135,7 +135,7 @@ export const isThemeColor = (color) =>{
135
135
  // Make a gridrange from a range
136
136
  export const makeGridRange = (range) => {
137
137
  if (!isRange(range)) {
138
- console.log(range)
138
+ slogger.error(range)
139
139
  throw new Error(`expected a range but got ${typeof range}`)
140
140
  }
141
141
  return {
@@ -5,9 +5,8 @@ import { Utils } from '../../support/utils.js'
5
5
  import { storeModels } from './gasflex.js'
6
6
  import { newCacheDropin } from '@mcpher/gas-flex-cache'
7
7
  import { notYetImplemented } from '../../support/helpers.js'
8
- import { file } from 'googleapis/build/src/apis/file/index.js'
9
8
  const { is } = Utils
10
-
9
+ import {slogger } from "../../support/slogger.js";
11
10
  /**
12
11
  * what these props mean
13
12
  * store_type = currently upstash or file - it defines the back end and maps to env variable
@@ -185,7 +184,7 @@ class FakeCacheService {
185
184
  export const newFakeService = (kind) => {
186
185
  kind = validateProp (kind, ServiceKind, 'service_kind')
187
186
  const w = whichCache ()
188
- console.log (`...${kind} store service is using store type ${w.type} as backend`)
187
+ slogger.log (`...${kind} store service is using store type ${w.type} as backend`)
189
188
  return Proxies.guard(kind === ServiceKind.CACHE ? new FakeCacheService(w.type) : new FakePropertiesService(w.type))
190
189
  }
191
190
 
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * @example
13
13
  * const MyEnum = newFakeGasEnum('MyEnum', ['KEY_A', 'KEY_B']);
14
- * console.log(MyEnum.KEY_A); // Outputs: "KEY_A"
14
+ * c.log(MyEnum.KEY_A); // Outputs: "KEY_A"
15
15
  * MyEnum.KEY_A = 'new value'; // Throws an error in strict mode
16
16
  *
17
17
  * @param {string} name The name of the enum, used for `toString()` representation (e.g., "ElementType").
@@ -1,7 +1,7 @@
1
1
  import {Utils} from './utils.js'
2
2
  const {is, capital} = Utils
3
3
  import { Proxies } from './proxies.js'
4
-
4
+ import {slogger } from "./slogger.js";
5
5
  /**
6
6
  * @constant
7
7
  * @type {string}
@@ -142,7 +142,7 @@ export const signatureArgs = (received, method, objectType = 'Object') => {
142
142
  try {
143
143
  passedTypes = args.map(f=>is.null(f) ? 'null' : is(f)).map(f=>f==='null'? f :capital(f)).map(f=>f==='Object' ? objectType : f)
144
144
  } catch (err) {
145
- console.log ("...warning failed signature check- probably an unsupported probe. probe of an enum - ignoring", args)
145
+ slogger.warn ("...warning failed signature check- probably an unsupported probe. probe of an enum - ignoring", args)
146
146
  passedTypes=[]
147
147
  }
148
148
  const matchThrow = (mess = method) => {
@@ -171,7 +171,7 @@ export const advClassMaker = (props) => {
171
171
  capped.map((f, i) => {
172
172
 
173
173
  if (done.has(f)) {
174
- console.log('....WARNING duplicate property in advClassMaker', f)
174
+ slogger.warn('....WARNING duplicate property in advClassMaker', f)
175
175
  }
176
176
  done.add(f)
177
177
  ob['get' + f] = () => ob[props[i]]
@@ -74,7 +74,7 @@ const registerProxy = (name, getApp) => {
74
74
  serviceRegistry.add(name);
75
75
  const value = new Proxy({}, getAppHandler(getApp, name))
76
76
  // add it to the global space to mimic what apps script does
77
- // console.log (`setting ${name} to global`)
77
+
78
78
  Object.defineProperty(globalThis, name, {
79
79
  value,
80
80
  enumerable: true,
@@ -103,7 +103,7 @@ const validateProperties = () => {
103
103
  if (
104
104
  // skip any inserted symbos
105
105
  typeof prop !== 'symbol' &&
106
- // sometimes typeof & console.log looks for ths
106
+ // sometimes typeof & c.log looks for ths
107
107
  prop !== 'inspect' &&
108
108
  // this is a mysterious property that APPS script sometimes checks for
109
109
  prop !== '__GS_INTERNAL_isProxy' &&
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @fileoverview a logger that can be silenced by an env variable
3
+ * used for all internal gas-fakes logging
4
+ */
5
+
6
+ const q = process.env.QUIET || false
7
+ export const isQuiet = q.toString().toLowerCase()=== "true";
8
+
9
+ export const slogger = {
10
+ log: isQuiet ? () => {} : console.log,
11
+ error: console.error,
12
+ warn: console.warn,
13
+ info: isQuiet ? () => {} : console.info
14
+ };
@@ -2,7 +2,7 @@
2
2
 
3
3
  import is from '@sindresorhus/is';
4
4
  import { assert } from '@sindresorhus/is'
5
-
5
+ import {slogger} from './slogger.js'
6
6
 
7
7
  const isNU = (item) => is.null(item) || is.undefined(item)
8
8
 
@@ -14,7 +14,7 @@ const fromJson = (text, failOnError = false) => {
14
14
  try {
15
15
  return JSON.parse(text)
16
16
  } catch (err) {
17
- console.log(text)
17
+ slogger.warn(text)
18
18
  if (failOnError) {
19
19
  throw err
20
20
  }
@@ -425,7 +425,7 @@ function stabilizeKeyOrder(obj) {
425
425
  const lobify = (ob, mess = '') => {
426
426
  let lob = ob
427
427
  if (is.object(lob)) lob = stringCircular(lob)
428
- console.log(mess, lob)
428
+ slogger.log(mess, lob)
429
429
  return ob
430
430
  }
431
431
  export const Utils = {
@@ -2,6 +2,7 @@ import { Worker } from 'worker_threads';
2
2
  import { fileURLToPath } from 'url';
3
3
  import path from 'path';
4
4
  import fs from 'node:fs';
5
+ import { slogger} from '../slogger.js'
5
6
 
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = path.dirname(__filename);
@@ -92,7 +93,7 @@ worker.unref();
92
93
  * whether normally or via signals like Ctrl+C.
93
94
  */
94
95
  function cleanup() {
95
- console.log('...terminating worker thread');
96
+ slogger.log('...terminating worker thread');
96
97
  worker.terminate();
97
98
  }
98
99
 
@@ -1,13 +1,16 @@
1
1
  import fs from 'node:fs';
2
-
2
+ import { isQuiet } from '../slogger.js';
3
3
  export const syncLog = (message) => {
4
- fs.writeSync(1, `[Worker] ${message}\n`);
4
+ if (!isQuiet) fs.writeSync(1, `[Worker] ${message}\n`);
5
5
  };
6
6
 
7
7
  export const syncWarn = (message) => {
8
8
  fs.writeSync(1, `[Worker Warn] ${message}\n`);
9
9
  };
10
10
 
11
+ export const syncInfo = (message) => {
12
+ if (!isQuiet) fs.writeSync(1, `[Worker Info] ${message}\n`);
13
+ };
11
14
  export const syncError = (message, error) => {
12
15
  // Providing the error object is optional
13
16
  const errorMessage = error ? `: ${error?.stack || error}` : '';