<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bruce Mcpherson</title>
    <description>The latest articles on DEV Community by Bruce Mcpherson (@brucemcpherson).</description>
    <link>https://dev.to/brucemcpherson</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1550481%2Fc2487f13-aa4a-4dc6-a892-f250b1ab3b0f.jpg</url>
      <title>DEV Community: Bruce Mcpherson</title>
      <link>https://dev.to/brucemcpherson</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/brucemcpherson"/>
    <language>en</language>
    <item>
      <title>From Kubernetes to a Self-Healing, Low-Cost Infrastructure</title>
      <dc:creator>Bruce Mcpherson</dc:creator>
      <pubDate>Mon, 22 Jun 2026 12:20:53 +0000</pubDate>
      <link>https://dev.to/brucemcpherson/from-kubernetes-to-a-self-healing-low-cost-infrastructure-1da4</link>
      <guid>https://dev.to/brucemcpherson/from-kubernetes-to-a-self-healing-low-cost-infrastructure-1da4</guid>
      <description>&lt;p&gt;I've been running a background project on Kubernetes for a while now. It's not a project that needs 100% uptime,  and neither is it one I wanted to spend a lot of time managing or even checking up on, so Kubernetes with Spot VM's seemed the most cost effective solution, and it's been solid and trouble free. However running a couple of pre-emptible nodes with a managed ingress was still costing $150 a month or so. Way too much for a hobby project. &lt;/p&gt;

&lt;p&gt;This article is about retaining the self healing capability you get with Kubernets, but migrating to a much more cost effective (about $40 a month) approach. I found that without Kubernetes in the the mix, i could get away with a single VM, but of course that doesn't give me recovery from pre-emption, so here's how to get that too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managed instance group
&lt;/h2&gt;

&lt;p&gt;The transition from a high-cost Google Kubernetes Engine (GKE) cluster to a single, highly available Spot VM managed by a Stateful Managed Instance Group (MIG) offers a path to significant cost savings without sacrificing resilience. By leveraging Docker Compose and automated infrastructure orchestration, the platform—comprising microservices such as GraphQL gateways, Elasticsearch processors, and Redis queues—now operates at the lowest possible compute cost while maintaining full recovery capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decoupling Compute from State
&lt;/h2&gt;

&lt;p&gt;The core challenge with using Spot VMs is their preemptible nature. To make this architecture immune to data loss during preemption, it utilizes GCP Stateful MIG Policies alongside stable device-id path targeting.&lt;/p&gt;

&lt;p&gt;A critical component of this decoupling is Storage State Preservation. Standard attachment targets (like /dev/sdb) are prone to swapping order during VM initialization. To guarantee consistency across machine replacements, the persistent volume is targeted via its unchangeable physical serial header: /dev/disk/by-id/google-existing-data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Autonomous Recovery Workflow
&lt;/h2&gt;

&lt;p&gt;When Google Cloud preempts a Spot VM, the system triggers a fully automated self-healing pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Detection &amp;amp; Provisioning: The MIG detects the deletion and instantly provisions a fresh instance node to maintain the target capacity of one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stateful Attachment: The MIG automatically binds the regional static IP and hot-plugs the persistent block storage to the new node at boot time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Guest OS Bootstrapping: A custom startup-script.sh holds execution until hardware attachment is verified. It then mounts the filesystem, installs the Docker Engine, and restarts microservices seamlessly using ./start-all.sh.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This entire process typically brings the platform back online with zero manual intervention within 60–90 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational States: Preemption vs. Scaling Down
&lt;/h2&gt;

&lt;p&gt;It is vital to distinguish between a True Spot Preemption and a Manual Scale Down to 0.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Spot Preemption: The MIG's intent is to keep one machine online. Per-instance configurations are preserved, and the recovery is fully autonomous.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scale Down to 0: This decommission command destroys the unique stateful metadata ties. When scaling back to 1, the new VM will get stuck in a boot loop because the MIG no longer knows to attach the existing disk or IP. Recovery in this scenario requires a manual orchestration script, ./create-mig.sh, to re-bind the regional static IP and existing data disk.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Comparing Infrastructure Models
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Kubernetes (GKE)&lt;/th&gt;
&lt;th&gt;Standalone VM&lt;/th&gt;
&lt;th&gt;Stateful MIG&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compute Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High (Cluster + Nodes)&lt;/td&gt;
&lt;td&gt;Medium (Standard VM)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Lowest (Spot VM)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Preemption Recovery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;td&gt;Manual Recreate&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Fully Automatic&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Volume Mounts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PVCs with GCE PD&lt;/td&gt;
&lt;td&gt;Local Static /dev/sdb&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Stable by-id path&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP Persistence&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;K8s LoadBalancer&lt;/td&gt;
&lt;td&gt;Bound to Instance&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Preserved via Config&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Docker-compose benefit
&lt;/h2&gt;

&lt;p&gt;Previously I was using kubernetes, cloud build and the artifact registry to manage my builds and releases. This meant that testing was a bit awkward, involving minikube, ngrok and various other workaraounds. Now that I've transitioned to docker compose, the exact same scripts and yaml files work both locally on my mac and on my vm, so i have a complete end to end simulation locally. &lt;/p&gt;

&lt;h2&gt;
  
  
  Replacing the Kubernetes Ingress
&lt;/h2&gt;

&lt;p&gt;If you need to access the VM externally, you're going to need to create some kind of ingress. Under GKE I was using a managed ingress, with letsencrypt handling the ssl certificate. On our vanilla VM, we can use a traefix proxy. All my services run on docker, as does the traefik proxy. Here's how to set it up. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;start-traefik.sh&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\033[0;32m'&lt;/span&gt;
&lt;span class="nv"&gt;YELLOW&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\033[1;33m'&lt;/span&gt;
&lt;span class="nv"&gt;RED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\033[0;31m'&lt;/span&gt;
&lt;span class="nv"&gt;NC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\033[0m'&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"Darwin"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Skipping Traefik on Mac (not needed)"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;🚀 Starting Traefik...&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NC&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Ensure network exists&lt;/span&gt;
docker network create fid-network 2&amp;gt;/dev/null

&lt;span class="c"&gt;# Start Traefik&lt;/span&gt;
docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose-traefik.yml up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="nb"&gt;sleep &lt;/span&gt;3

&lt;span class="k"&gt;if &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; http://localhost:80 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"200&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;301&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;302"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;✅ Traefik is running&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NC&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Traefik is listening on ports 80 (HTTP) and 443 (HTTPS)."&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Check logs: docker compose -f docker-compose-traefik.yml logs -f traefik"&lt;/span&gt;
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RED&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;❌ Traefik may not be ready. Check logs.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NC&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;docker-compose-traefik.yml&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefik&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik:v3.2&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fid-traefik&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.address=:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.websecure.address=:443"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.file.directory=/etc/traefik/conf"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.letsencrypt.acme.httpchallenge=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.letsencrypt.acme.email=admin@xliberation.com"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entrypoint.to=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entrypoint.scheme=https"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entrypoint.permanent=true"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./conf:/etc/traefik/conf&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik_certs:/letsencrypt&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;fid-network&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefik_certs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;fid-network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Some example scripts
&lt;/h2&gt;

&lt;p&gt;All of this is a little tricky and precise, so here are some scripts I have used to get my services running, along with a few hints. I'll assume you already have a reserved static address (if you need to expose publicly)&lt;/p&gt;

&lt;h4&gt;
  
  
  Your startup script (startup-script.sh)
&lt;/h4&gt;

&lt;p&gt;This is mine - note the connection string that uses the by-id path. &lt;code&gt;google-exisiting-data&lt;/code&gt; refers to the persistent disk it should attach. This is not the disk name (in my case that name is fid-data), but a standard name that a mig applies to an incoming state fule disk attachment. Note the subsequent mount command that mounts the disk as its correct name. The systemd platform file describes what to do once all that is complete. I have a start-all.sh there that will actually start all my services using the persistent disk fid-data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="c"&gt;# --- 1. Wait for persistent disk to physically attach ---&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Waiting for persistent disk to attach..."&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; /dev/disk/by-id/google-existing-data &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# Give the storage driver a brief moment to stabilize the block mapping&lt;/span&gt;
&lt;span class="nb"&gt;sleep &lt;/span&gt;2

&lt;span class="c"&gt;# --- 2. Mount persistent disk ---&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/disks/fid-data
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; mountpoint &lt;span class="nt"&gt;-q&lt;/span&gt; /mnt/disks/fid-data&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;mount &lt;span class="nt"&gt;-o&lt;/span&gt; discard,defaults /dev/disk/by-id/google-existing-data /mnt/disks/fid-data
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# --- 3. Create symlink to repo on persistent disk ---&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /home/brucemcpherson
&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; brucemcpherson:brucemcpherson /home/brucemcpherson
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-sf&lt;/span&gt; /mnt/disks/fid-data/fidmaster /home/brucemcpherson/fidmaster

&lt;span class="c"&gt;# --- 4. Install Official Modern Docker Engine &amp;amp; Compose ---&lt;/span&gt;
apt-get update &lt;span class="nt"&gt;-qq&lt;/span&gt;
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-qq&lt;/span&gt; ca-certificates curl gnupg

&lt;span class="c"&gt;# Add Docker's official GPG key and repository&lt;/span&gt;
&lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-d&lt;/span&gt; /etc/apt/keyrings
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://download.docker.com/linux/debian/gpg | gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;--yes&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/docker.gpg
&lt;span class="nb"&gt;chmod &lt;/span&gt;a+r /etc/apt/keyrings/docker.gpg

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"deb [arch="&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  "&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;" stable"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/docker.list &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null

&lt;span class="c"&gt;# Install the exact modern packages (restores "docker compose" with a space)&lt;/span&gt;
apt-get update &lt;span class="nt"&gt;-qq&lt;/span&gt;
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-qq&lt;/span&gt; docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

&lt;span class="c"&gt;# --- 5. Configure Docker data root ---&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'"data-root": "/mnt/disks/fid-data/docker"'&lt;/span&gt; /etc/docker/daemon.json 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"data-root": "/mnt/disks/fid-data/docker"}'&lt;/span&gt; | &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/docker/daemon.json
    systemctl restart docker
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# --- 6. Create systemd service file ---&lt;/span&gt;
&lt;span class="c"&gt;# Using 'tee' inside the script ensures no root permission redirection blocks&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' | tee /etc/systemd/system/fid-platform.service &amp;gt; /dev/null
[Unit]
Description=FID Platform (all services)
After=docker.service network.target
Requires=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
User=brucemcpherson
WorkingDirectory=/home/brucemcpherson/fidmaster/vm-docker/local-compose
ExecStartPre=/bin/sleep 5
ExecStart=/bin/bash /home/brucemcpherson/fidmaster/vm-docker/local-compose/start-all.sh
ExecStop=/bin/bash /home/brucemcpherson/fidmaster/vm-docker/local-compose/stop-all.sh
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;systemctl daemon-reload
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;fid-platform

&lt;span class="c"&gt;# --- 7. Start all services ---&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /home/brucemcpherson/fidmaster/vm-docker/local-compose
./start-all.sh

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Startup complete."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Create a managed group template
&lt;/h4&gt;

&lt;p&gt;Note that the template references this startup-script. If you subsequently change the startup-script, you'll need to replace the template.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute instance-templates create fid-vm-template-ephemeral &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;europe-west2 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--machine-type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;e2-standard-4 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--image-family&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;debian-12 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--image-project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;debian-cloud &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--boot-disk-size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50GB &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--boot-disk-type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pd-ssd &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--provisioning-model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;SPOT &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--metadata-from-file&lt;/span&gt; startup-script&lt;span class="o"&gt;=&lt;/span&gt;startup-script.shgcloud 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Create a managed group (create-mig.sh)
&lt;/h4&gt;

&lt;p&gt;The size=1 means i want a single VM. This VM will be called your_mig_name-random_chars.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# 1. Define your known environment variables&lt;/span&gt;
&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your project"&lt;/span&gt;
&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"europe-west2"&lt;/span&gt;
&lt;span class="nv"&gt;ZONE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"europe-west2-b"&lt;/span&gt;
&lt;span class="nv"&gt;MIG_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"the name for your new mig"&lt;/span&gt;
&lt;span class="nv"&gt;IP_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your static ip name"&lt;/span&gt;
&lt;span class="nv"&gt;DISK_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your persistent disk name"&lt;/span&gt;

&lt;span class="c"&gt;# Check if the MIG already exists before trying to create it&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;gcloud compute instance-groups managed describe &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MIG_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ZONE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Managed Instance Group '&lt;/span&gt;&lt;span class="nv"&gt;$MIG_NAME&lt;/span&gt;&lt;span class="s2"&gt;' already exists. Skipping creation..."&lt;/span&gt;
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Creating Managed Instance Group '&lt;/span&gt;&lt;span class="nv"&gt;$MIG_NAME&lt;/span&gt;&lt;span class="s2"&gt;'..."&lt;/span&gt;
    gcloud compute instance-groups managed create &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MIG_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ZONE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fid-vm-template-ephemeral &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1

    &lt;span class="c"&gt;# Give GCP a moment to spin up the instance before querying it&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Waiting 15 seconds for instance to initialize..."&lt;/span&gt;
    &lt;span class="nb"&gt;sleep &lt;/span&gt;15
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# 2. Automatically grab the dynamic instance name&lt;/span&gt;
&lt;span class="nv"&gt;INSTANCE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gcloud compute instances list &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"name~'^fid-mig-'"&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"value(name)"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Safety check: Ensure an instance actually exists&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSTANCE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Error: No running instance found starting with '&lt;/span&gt;&lt;span class="nv"&gt;$MIG_NAME&lt;/span&gt;&lt;span class="s2"&gt;-'."&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Found target instance: &lt;/span&gt;&lt;span class="nv"&gt;$INSTANCE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 3. Handle the per-instance config cleanly (Create if missing, update if exists)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Configuring stateful IP and Disk resources..."&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;gcloud compute instance-groups managed instance-configs describe &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MIG_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSTANCE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ZONE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;CONFIG_ACTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"update"&lt;/span&gt;
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nv"&gt;CONFIG_ACTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"create"&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;gcloud compute instance-groups managed instance-configs &lt;span class="nv"&gt;$CONFIG_ACTION&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MIG_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ZONE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSTANCE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--stateful-external-ip&lt;/span&gt; interface-name&lt;span class="o"&gt;=&lt;/span&gt;nic0,address&lt;span class="o"&gt;=&lt;/span&gt;projects/&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/regions/&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REGION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/addresses/&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IP_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--stateful-disk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;device-name&lt;span class="o"&gt;=&lt;/span&gt;existing-data,source&lt;span class="o"&gt;=&lt;/span&gt;projects/&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/zones/&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ZONE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/disks/&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DISK_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;,auto-delete&lt;span class="o"&gt;=&lt;/span&gt;never

&lt;span class="c"&gt;# 4. Trigger the MIG to apply these stateful settings to the live VM&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Applying configurations to &lt;/span&gt;&lt;span class="nv"&gt;$INSTANCE_NAME&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;
gcloud compute instance-groups managed update-instances &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MIG_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ZONE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--instances&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSTANCE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Done! Dynamic setup complete."&lt;/span&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Ssh to your instance (ssh.sh)
&lt;/h4&gt;

&lt;p&gt;My mig group is called fid-mig, so i can extract the instance name and attach to it like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute ssh &lt;span class="si"&gt;$(&lt;/span&gt;gcloud compute instances list &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"name~'^fid-mig-'"&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"value(name)"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;europe-west2-b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Simulate a pre-emption to test (simulate-preemption.sh)
&lt;/h4&gt;

&lt;p&gt;In this case, just deleting the instance will simulate a preemption. It will come back up and execute your startup without needing intervention. This differs from the deliberate resizing the MIG to 0 which would need manual intervention to restart the VM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="c"&gt;# 1. Find the current instance name&lt;/span&gt;
&lt;span class="nv"&gt;INSTANCE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gcloud compute instances list &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"name~'^fid-mig-'"&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"value(name)"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# 2. Simulate a sudden crash/preemption by deleting the instance body directly&lt;/span&gt;
gcloud compute instances delete &lt;span class="nv"&gt;$INSTANCE_NAME&lt;/span&gt; &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;europe-west2-b &lt;span class="nt"&gt;--quiet&lt;/span&gt;

gcloud compute instance-groups managed list

Take down the VM &lt;span class="o"&gt;(&lt;/span&gt;down-mig.sh&lt;span class="o"&gt;)&lt;/span&gt;

If you&lt;span class="s1"&gt;'re not using it, might as well save the cost. All you have to do is set the Mig size to 0.

gcloud compute instance-groups managed resize fid-mig \
    --size=0 \
    --zone=europe-west2-b \
    --quiet

gcloud compute instance-groups managed list
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Bring up the VM (up-mig.sh)
&lt;/h4&gt;

&lt;p&gt;Setting the size to 1, will reinstate the vm. However at this point it knows nothing about what it's supposed to do, so you also need to run create-mig.sh to get back to a running system.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute instance-groups managed resize fid-mig &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;europe-west2-b &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--quiet&lt;/span&gt;

gcloud compute instance-groups managed list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  links
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ramblings.mcpher.com/kube-to-mig/" rel="noopener noreferrer"&gt;article&lt;/a&gt;&lt;br&gt;
&lt;a href="https://youtu.be/U17bgNQwowg" rel="noopener noreferrer"&gt;video&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>infrastructure</category>
      <category>kubernetes</category>
      <category>sre</category>
    </item>
    <item>
      <title>Combining local and hosted llm to minimize token cost</title>
      <dc:creator>Bruce Mcpherson</dc:creator>
      <pubDate>Tue, 09 Jun 2026 13:13:55 +0000</pubDate>
      <link>https://dev.to/brucemcpherson/combining-local-and-hosted-llm-to-minimize-token-cost-o69</link>
      <guid>https://dev.to/brucemcpherson/combining-local-and-hosted-llm-to-minimize-token-cost-o69</guid>
      <description>&lt;p&gt;My current large project is &lt;a href="https://github.com/brucemcpherson/gas-fakes" rel="noopener noreferrer"&gt;gas-fakes&lt;/a&gt;, which is an emulation that allows local execution, continuous integration, and containerization of native Apps Script code. In other words, we are not just ’emulating Apps Script’ – we are liberating it.&lt;/p&gt;

&lt;p&gt;Initially, AI generated code and testing was not something I was comfortable publishing, so to this point real people have coded and tested the majority of the repo. However, now the architecture and techniques are fully mature the remaining work is largely just busy work implementing and testing the remaining, less used, Apps Script platform methods.&lt;/p&gt;

&lt;p&gt;As of gas-fakes v2.5.3 we are at 4399/6708 methods and 10,500 parity tests on the emulation against the live Apps Script platform. Now feel a little more confident about allowing AI to do some of coding work.&lt;/p&gt;

&lt;p&gt;As an open source developer, my work is voluntary and unpaid, and therefore have to balance the potential token cost at my own personal expense, versus the value of any time saving I might make.&lt;/p&gt;

&lt;p&gt;This article is about combining the planning capability of antigravity, with the a free local model (Gemma running under oMLX on a Mac) doing the grunt work. Like this my Gemini costs are minimal, and the local heavy work is free.&lt;/p&gt;

&lt;p&gt;Note: this article is specific to Mac/AntiGravity combination. You can use a similar technique for other combinations but I’m not covering them here. gas-fakes collaborators will have this set up already implemented in their repo fork (but need to tweak the .gemini/settings to point to their local path for the mcp tools) , and the local model oMLX delegation will be ignored if they don’t have it running.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmzi66tqsalvuyk7aypok.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmzi66tqsalvuyk7aypok.png" alt=" " width="799" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluation of the repo content.
&lt;/h2&gt;

&lt;p&gt;Before we start this is the status of the repo according to a Gemini assessment, before I start to use this local model to help with the grunt work. Clearly my priority is to maintain (or improve) the current quality.&lt;/p&gt;

&lt;p&gt;You can get a detailed analysis &lt;a href="https://github.com/brucemcpherson/gas-fakes/blob/main/notes/ai_scorecard.md" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Executive Summary &amp;amp; Core Metrics
&lt;/h3&gt;


&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Evaluation Dimension&lt;/th&gt;
&lt;th&gt;Grade&lt;/th&gt;
&lt;th&gt;Key Focus Area / Findings&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Architectural Design &amp;amp; Viability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A+&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Exceptional synchronous design mimicking V8 GAS on top of Node’s async landscape.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Parity Tracking &amp;amp; Completeness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Data-driven tracking system mapping thousands of live Apps Script methods via&amp;nbsp;&lt;code&gt;/progress&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Testing, Quality Assurance &amp;amp; Fidelity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Massive test footprint (~10,000+ internal/cyclical validation passes) proving true 1:1 behavioral parity.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Edge-Case &amp;amp; Platform Oddities Handling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A-&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Deeply transparent about platform limits, script execution quirks, and modern auth drift.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ecosystem &amp;amp; Modern Stack Readiness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A+&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Integrated Model Context Protocol (MCP) server,&amp;nbsp;&lt;code&gt;gf_agent&lt;/code&gt;&amp;nbsp;automation tool, and containerization.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h4 id="%F0%9F%92%8E-overall-project-score-94100-enterprise-grade--production-dev-tool"&gt;&lt;span id="Overall_Project_Score_94100_Enterprise_Grade_Production_Dev_Tool"&gt;💎 Overall Project Score:&amp;nbsp;&lt;strong&gt;94/100&lt;/strong&gt;&amp;nbsp;(Enterprise Grade / Production Dev Tool)&lt;/span&gt;&lt;/h4&gt;

&lt;h2&gt;
  
  
  Gemini Directive &amp;amp; Hybrid Planned Hierarchy
&lt;/h2&gt;

&lt;p&gt;We’ll be using a strict, hierarchical delegation model.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Roles
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Strategic Planner (Gemini)&lt;/strong&gt;: The hosted, powerful LLM. Its role is high-level planning, context management, decision-making, and orchestration. It determines what needs to be done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Focused Executor (Local Model)&lt;/strong&gt;: The local, specialized LLM. Its role is high-fidelity, resource-intensive execution of specific tasks. It determines how the task is completed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Delegation Mechanism (query_local_model)
&lt;/h3&gt;

&lt;p&gt;The Planner is equipped with a &lt;a href="https://github.com/brucemcpherson/gas-fakes/blob/main/tools/omlx_mcp_server.cjs" rel="noopener noreferrer"&gt;specific tool&lt;/a&gt;, query_local_model. When the Planner determines that a task requires local computation, it does not attempt to solve it itself. Instead, it generates a structured call to query_local_model, passing the necessary context and instructions to the local MCP Server.&lt;/p&gt;

&lt;p&gt;The local model executes the task and returns the result to the Strategic Planner, which then integrates it into the final response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational Constraints (The Golden Rule)
&lt;/h2&gt;

&lt;p&gt;To prevent unnecessary cloud API usage, we can give the Planner strict directives:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The Planner is strictly forbidden from drafting implementation details, writing production code, or creating tests directly when the query_local_model tool is available. If the task falls within the scope of a specialized, local execution, the Planner must delegate the task to the local Executor.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We specify this as part of the &lt;a href="https://github.com/brucemcpherson/gas-fakes/blob/main/.agents/skills/gas-fakes-dev/SKILL.md" rel="noopener noreferrer"&gt;skills training&lt;/a&gt; – snippet of the important rule below:&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation &amp;amp; Focused Execution (CRITICAL DELEGATION GATE)
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT] ZERO-TOLERANCE DELEGATION GATE You are FORBIDDEN from using write_file or replace to implement logic, write tests, perform refactoring, diagnose/fix debug errors, or draft documentation yourself. MANDATORY SEQUENCE:&lt;br&gt;
Gather context (Research).&lt;br&gt;
CALL omlx/query_local_model with a comprehensive prompt containing specific constraints.&lt;br&gt;
Review and synthesize the output.&lt;br&gt;
Apply changes to files (e.g., in src/, test/, etc.).&lt;br&gt;
EXCEPTION: This mandate only applies if the local model is available and its use has not been explicitly forbidden by the user. If unavailable or forbidden, you may proceed with the tasks using your own weights, but you MUST document the reason in your update_topic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Cost and Token Savings
&lt;/h3&gt;

&lt;p&gt;Hosted LLMs operating under token-based pricing complex can quickly accumulate unaffordable costs. By offloading the heavy lifting to the local model, you reduce the number of hosted tokens you have to pay for.&lt;/p&gt;

&lt;h2&gt;
  
  
  oMLX Setup and Documentation
&lt;/h2&gt;

&lt;p&gt;Since I’m using oMLX to serve my local model, let’s look at how to set that up. If you are not using a Mac, there are other local model orchestrators you can use, but the initial setup of those up is outside the scope of this article.&lt;/p&gt;

&lt;h3&gt;
  
  
  Overview: What is oMLX?
&lt;/h3&gt;

&lt;p&gt;oMLX allows the Planner to offload specific, resource-intensive tasks to a local, specialized LLM (the Executor).&lt;/p&gt;

&lt;p&gt;Instead of relying solely on the hosted API for every request, oMLX acts as a middleware layer. The hosted model dynamically decides when a task is best suited for local execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup and Configuration
&lt;/h3&gt;

&lt;p&gt;The core of the oMLX system is the MCP Server (Model Communication Protocol Server), which acts as the local endpoint for the Focused Executor.&lt;/p&gt;

&lt;h3&gt;
  
  
  The oMLX MCP Server
&lt;/h3&gt;

&lt;p&gt;an mcp tool acts as a local server. This server listens for requests from the hosted LLM (Gemini) and routes them to the locally running model instance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration Methods
&lt;/h3&gt;

&lt;p&gt;You control the server and the overall system behavior via these environment variables. the mcp tool uses these to know where to delegate tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validating that AntiGravity is using the local server
&lt;/h3&gt;

&lt;p&gt;An important step to verify everything is working.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ask the agy cli – ‘are you able to use the local model’&lt;/li&gt;
&lt;li&gt;The mcp server will inform you when it is are using the local model – you’ll see messages like this – &lt;code&gt;omlx/query_local_model(Delegate documentation generation to local model)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check the oMlx dashboard (&lt;a href="http://127.0.0.1:8000/admin/dashboard)-" rel="noopener noreferrer"&gt;http://127.0.0.1:8000/admin/dashboard)-&lt;/a&gt; notice the ‘generating’ comment against the gemma model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F72ry9i5m5m41qquystvr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F72ry9i5m5m41qquystvr.png" alt=" " width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;See this to get started with gas-fakes.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/brucemcpherson/gas-fakes" rel="noopener noreferrer"&gt;gas-fakes&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>gasfakes</category>
      <category>node</category>
      <category>appsscript</category>
    </item>
    <item>
      <title>Bringing Apps Script to the desktop – the why, where and how of gas-fakes</title>
      <dc:creator>Bruce Mcpherson</dc:creator>
      <pubDate>Wed, 03 Jun 2026 13:20:05 +0000</pubDate>
      <link>https://dev.to/brucemcpherson/bringing-apps-script-to-the-desktop-the-why-where-and-how-of-gas-fakes-3ccg</link>
      <guid>https://dev.to/brucemcpherson/bringing-apps-script-to-the-desktop-the-why-where-and-how-of-gas-fakes-3ccg</guid>
      <description>&lt;p&gt;A brief summary on approaching parity of Google Apps Script methods and classes, some of the 'extras' gas-fakes provides for platform variety, production, development and testing and glimpse of some of the techniques used to get there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction: The Challenge of the Scripting Layer
&lt;/h2&gt;

&lt;p&gt;Google Apps Script (GAS) is a powerful tool for extending Google Workspace, automating workflows, and building lightweight backend services. It excels at rapid prototyping and integration within the Google ecosystem. However the limitations of the GAS environment (its proprietary runtime, laborious deployment cycle, and lack of modern tooling and debugging) become significant bottlenecks.&lt;/p&gt;

&lt;p&gt;We love the power and integration of GAS, but we require the debuggability and flexibility of a modern Node.js environment.&lt;/p&gt;

&lt;p&gt;This is the problem that &lt;strong&gt;gas-fakes&lt;/strong&gt; is designed to address.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gas-fakes&lt;/code&gt; is an architectural emulation layer designed to bring the entire GAS runtime experience (its APIs, its behaviors, and its constraints) into a robust, controllable Node.js environment. It allows developers to write GAS-like code while leveraging the full power of modern software engineering practices.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fefn2mr3gm86norgc0qdp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fefn2mr3gm86norgc0qdp.png" alt="gas-fakes what" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Path to Parity: Emulating the GAS Runtime
&lt;/h2&gt;

&lt;p&gt;The core challenge in building &lt;code&gt;gas-fakes&lt;/code&gt; is the fundamental mismatch between the GAS execution model and the Node.js event loop. GAS is inherently asynchronous, relying on a managed, proprietary execution environment. Node.js, is designed for high-throughput, asynchronous execution and non-blocking I/O.&lt;/p&gt;

&lt;p&gt;Our architectural approach to achieving parity focuses on accurate API simulation and execution model transformation.&lt;/p&gt;

&lt;h3&gt;
  
  
  The API Contract: Discovery Documents and REST
&lt;/h3&gt;

&lt;p&gt;The foundation of GAS is its reliance on Google's ecosystem of REST APIs, exposed through its own managed runtime. We've mapped the exact schema, parameters, and expected responses of every GAS service (e.g., &lt;code&gt;SpreadsheetApp&lt;/code&gt;, &lt;code&gt;GmailApp&lt;/code&gt;). This allows &lt;code&gt;gas-fakes&lt;/code&gt; to treat the GAS environment as a highly structured, predictable API contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Execution Model: Synchronous Emulation via Worker Threads
&lt;/h3&gt;

&lt;p&gt;The most complex hurdle is the transformation of GAS's asynchronous API calls (which often feel synchronous to the developer) into a predictable, synchronous-feeling flow within Node.js.&lt;/p&gt;

&lt;p&gt;We achieve this by employing &lt;strong&gt;Worker Threads&lt;/strong&gt; and &lt;strong&gt;Atomics&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Worker Isolation:&lt;/strong&gt; When a GAS method is called (e.g., &lt;code&gt;SpreadsheetApp.getActiveSpreadsheet().getRange().getValue()&lt;/code&gt;), the request is routed to a dedicated Worker Thread. This isolates the simulated GAS execution from the main Node.js event loop, preventing blocking while maintaining the illusion of synchronous execution.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Asynchronous-to-Synchronous Bridge:&lt;/strong&gt; The Worker Thread executes the simulated API call. Instead of returning a standard Promise, we use &lt;code&gt;Atomics&lt;/code&gt; to manage shared memory state between the Worker and the main thread. This allows the main thread to effectively "wait" for the combined result from the Worker, mimicking the blocking behavior of the original GAS runtime, while allowing the worker to execute multiple asynchronous activities. &lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Importance of Behavioral Oddities
&lt;/h3&gt;

&lt;p&gt;A perfect API contract is insufficient. GAS has numerous subtle, undocumented behaviors such as rate limiting quirks, specific error codes, divergences from the associated API behavior, ID transformations, and timing dependencies that need to be understood and emulated for true parity.&lt;/p&gt;

&lt;p&gt;A significant part of the &lt;code&gt;gas-fakes&lt;/code&gt; development process is the meticulous documentation and reproduction of these &lt;strong&gt;behavioral oddities&lt;/strong&gt;. We don't just emulate the &lt;em&gt;happy path&lt;/em&gt;; we emulate the &lt;em&gt;edge cases&lt;/em&gt; and the &lt;em&gt;developer experience&lt;/em&gt; of the live GAS environment, ensuring that code written in &lt;code&gt;gas-fakes&lt;/code&gt; behaves identically to how it would in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧪 Rigorous Testing Regime: &lt;code&gt;gas-fakes&lt;/code&gt; fidelity assurance
&lt;/h3&gt;

&lt;p&gt;To validate the claim of behavioral parity, &lt;code&gt;gas-fakes&lt;/code&gt; operates under a massive, dual-environment testing regime. We maintain a comprehensive suite of &lt;strong&gt;over 10,500 tests&lt;/strong&gt; (as of version 2.5.3) that are executed across two distinct environments:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;The Local &lt;code&gt;gas-fakes&lt;/code&gt; Emulator:&lt;/strong&gt; This allows for rapid, isolated unit and integration testing of the simulated runtime, ensuring the internal logic and API contracts are sound.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The Live Google Apps Script Environment:&lt;/strong&gt; Crucially, every test suite is also executed against the actual, live GAS runtime.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This dual-environment verification ensures that the emulation is not merely "close," but &lt;strong&gt;behaviorally identical&lt;/strong&gt; to production GAS, including the precise handling of complex error states, rate limiting, and obscure edge cases that only manifest in the live Google ecosystem.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Transparency and Auditing: The Documentation Layer
&lt;/h2&gt;

&lt;p&gt;Beyond achieving functional parity, &lt;code&gt;gas-fakes&lt;/code&gt; is engineered with a core commitment to engineering transparency. We recognize that for enterprise adoption, developers require not just a working emulator, but a fully auditable and self-documenting environment.&lt;/p&gt;

&lt;p&gt;To address this, we have built a sophisticated documentation layer that provides unprecedented visibility into the emulation process.&lt;/p&gt;

&lt;h3&gt;
  
  
  📚 Embedded API Documentation
&lt;/h3&gt;

&lt;p&gt;The repository includes a locally accessible and searchable version of the official Google Apps Script documentation. This documentation is integrated directly into the development environment, allowing developers to reference precise API definitions, parameter types, and expected behaviors without needing to switch context or leave their local codebase. This eliminates the friction of external documentation lookups, accelerating the development cycle while maintaining technical accuracy.&lt;/p&gt;

&lt;h3&gt;
  
  
  📊 Fidelity Progress Summary
&lt;/h3&gt;

&lt;p&gt;To manage the monumental task of API parity, we provide an automated &lt;strong&gt;Fidelity Progress Summary&lt;/strong&gt;. This system offers a clear, high-level overview of the implementation status for every class and method across all supported services. Developers can instantly see whether a specific function is &lt;code&gt;Completed&lt;/code&gt;, &lt;code&gt;In Progress&lt;/code&gt;, or &lt;code&gt;Not Started&lt;/code&gt;, providing a transparent roadmap of the emulation effort and allowing them to gauge the maturity of the API they are using.&lt;/p&gt;

&lt;p&gt;As of version 2.5.3, 4399 of Apps Scripts total of 6708 are implemented. &lt;/p&gt;

&lt;h3&gt;
  
  
  🔍 Deep Implementation Links: The Audit Trail
&lt;/h3&gt;

&lt;p&gt;A unique feature of &lt;code&gt;gas-fakes&lt;/code&gt; is deep implementation transparency. The documentation includes direct, clickable links to the &lt;strong&gt;exact line of source code&lt;/strong&gt; where every single method is implemented within the emulator. This feature allows for instant verification and auditing of the emulation logic. If a developer questions the behavior of &lt;code&gt;SpreadsheetApp.getRange()&lt;/code&gt;, they can instantly trace the call through the documentation to the specific line of code in &lt;code&gt;gas-fakes&lt;/code&gt; that dictates its behavior, providing a high level of trust and debuggability.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Beyond Parity: The gas-fakes Advantage
&lt;/h2&gt;

&lt;p&gt;While achieving parity is a huge task, another value of &lt;code&gt;gas-fakes&lt;/code&gt; lies in the capabilities it enables. Capabilities that are fundamentally impossible or prohibitively difficult within the constraints of the live Google Apps Script environment. This includes the elimination of theat troublesome 6 minute execution time limit on Live Apps Script.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gas-fakes&lt;/code&gt; transforms the Apps Script language into a modern application development platform by separating its syntax from the place it traditionally runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  🚀 GAS Syntax for Regular Node Apps: Simplified Workspace Access
&lt;/h3&gt;

&lt;p&gt;A transformative features of &lt;code&gt;gas-fakes&lt;/code&gt; is its ability to inject the familiar, high-level syntax of Google Apps Script directly into a standard Node.js runtime. &lt;/p&gt;

&lt;p&gt;Even if you have no intention of ever deploying to Google Apps Script, you can use the simple, intuitive GAS syntax (e.g., &lt;code&gt;SpreadsheetApp&lt;/code&gt;, &lt;code&gt;DriveApp&lt;/code&gt;) in your regular Node.js apps. This provides a much simpler interface for accessing Workspace resources compared to the complex, low-level parameters of the raw REST APIs. It reduces cognitive load, improves maintainability, and abstracts away the complexity of OAuth scopes and request formatting.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔑 Auth Provisioning &amp;amp; Token Reuse: A Unified Credential Manager
&lt;/h3&gt;

&lt;p&gt;Managing authentication tokens across multiple services is an operational burden. &lt;code&gt;gas-fakes auth&lt;/code&gt; acts as a powerful, centralized credential manager. It provisions OAuth tokens for multiple backends (Google, KSuite, MS Graph) scoped to your provided manifest which can then be easily retrieved and &lt;strong&gt;reused&lt;/strong&gt; by other parts of your Node application or even by separate CLI tools. This creates a streamlined, single source of truth for credentials.&lt;/p&gt;

&lt;h3&gt;
  
  
  ☁️ Containerization and Multi-Cloud Deployment
&lt;/h3&gt;

&lt;p&gt;The entire GAS runtime emulation can be packaged using &lt;strong&gt;Docker&lt;/strong&gt;. This allows developers to deploy and run GAS-like logic in modern, serverless, and scalable multi-cloud environments. By leveraging the container image, &lt;code&gt;gas-fakes&lt;/code&gt; is fully compatible with leading cloud platforms, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Google Cloud Run&lt;/strong&gt;, &lt;strong&gt;Azure Container Apps&lt;/strong&gt;, &lt;strong&gt;AWS Lambda&lt;/strong&gt;, &lt;strong&gt;IBM Cloud&lt;/strong&gt;, and &lt;strong&gt;Kubernetes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This capability fundamentally breaks the vendor lock-in associated with proprietary GAS deployment, allowing the same business logic to be executed in any serverless or containerized architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  🚀 CLI Workflow and Initialization
&lt;/h3&gt;

&lt;p&gt;While the core functionality of &lt;code&gt;gas-fakes&lt;/code&gt; provides a local execution environment, the initial setup and integration with live cloud services are handled through a streamlined Command Line Interface (CLI). This workflow is designed to minimize friction, automate complex configuration tasks, and ensure that your local testing environment perfectly mirrors the permissions and dependencies of your production Apps Script project.&lt;/p&gt;

&lt;h4&gt;
  
  
  🛠️ Streamlined Project Setup (&lt;code&gt;gas-fakes init&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;gas-fakes init&lt;/code&gt; command serves as the foundational step for any new project. It automates the tedious process of environment configuration, allowing developers to focus immediately on coding. It automatically generates a local &lt;code&gt;.env&lt;/code&gt; file, which securely stores necessary configuration variables and prompts the user to select the target cloud backends (e.g., Google Workspace, KSuite, MS Graph).&lt;/p&gt;

&lt;h4&gt;
  
  
  🔑 Authentication and Token Management (&lt;code&gt;gas-fakes auth&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;Connecting a local environment to live cloud services requires managing complex OAuth flows and token lifecycles. The &lt;code&gt;gas-fakes auth&lt;/code&gt; command abstracts this complexity, providing a robust mechanism for secure, persistent authentication across all supported backends. It handles initial authorization guiding the user through browser redirects and manages secure token storage and refresh logic.&lt;/p&gt;

&lt;h4&gt;
  
  
  🔬 Automatic Scope Discovery: Precision Permissions
&lt;/h4&gt;

&lt;p&gt;The CLI automatically reads the &lt;code&gt;appsscript.json&lt;/code&gt; manifest file from your project. By parsing this file, &lt;code&gt;gas-fakes&lt;/code&gt; automatically infers and registers the exact OAuth scopes necessary for local execution. This ensures that your local environment is provisioned with the precise permissions required by the live project, guaranteeing parity without requiring developers to manually track or update scope lists.  &lt;/p&gt;

&lt;p&gt;&lt;code&gt;gas-fakes&lt;/code&gt; even supports existing published Apps Script libraries. If they are mentioned in your manifest, they can be accessed remotely from live Apps Script and executed locally. &lt;/p&gt;

&lt;h3&gt;
  
  
  🚀 Local Web Server and RPC Testing
&lt;/h3&gt;

&lt;p&gt;In live GAS, testing a Web App requires deployment and a live URL. In &lt;code&gt;gas-fakes&lt;/code&gt;, you can use its cli to instantiate the entire GAS environment locally, deploy your code to a simulated endpoint, and test complex &lt;code&gt;google.script.run&lt;/code&gt; interactions and HTML Service templating with full local debugging tools without the need for any actual deployments. This accelerates the development feedback loop, with code changes showing up live in your local environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  🌐 Multi-Backend Architecture
&lt;/h3&gt;

&lt;p&gt;Live GAS is tightly coupled to Google services. &lt;code&gt;gas-fakes&lt;/code&gt; decouples the business logic from the data source. By simply switching a configuration property, your GAS-like code can run against &lt;strong&gt;Google Services&lt;/strong&gt;, &lt;strong&gt;KSuite&lt;/strong&gt;, or &lt;strong&gt;MS Graph&lt;/strong&gt;. This enables hybrid architectures and sovereign cloud, with increased parity for non-Google backends currently being prioritized.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 The Automation Layer: CLI, &lt;code&gt;gf_agent&lt;/code&gt;, and &lt;code&gt;togas&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;While the sandbox provides the secure environment, the &lt;strong&gt;&lt;code&gt;gas-fakes&lt;/code&gt; CLI&lt;/strong&gt; and the specialized &lt;strong&gt;&lt;code&gt;gf_agent&lt;/code&gt;&lt;/strong&gt; provide the intelligence and accessibility needed for modern automation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;The CLI&lt;/strong&gt;: A robust command-line tool for running scripts, starting local servers, and managing the sandbox.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;gf_agent&lt;/code&gt;&lt;/strong&gt;: This AI-powered companion acts as a translator, bridging the gap between natural language intent and executable code. For example, a request like &lt;em&gt;"Summarize my last 5 emails and put them in a new spreadsheet"&lt;/em&gt; is instantly converted into optimized Apps Script and executed by your AI agent. This is a self learning skills agaent which can both enhance its own knowledge, and optionally contribute towards community skills.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;togas&lt;/code&gt; &amp;amp; Clasp Integration&lt;/strong&gt;: The &lt;code&gt;togas&lt;/code&gt; command acts as a high-level orchestrator for deployment. It automates the process of bundling and synchronizing local files with a live GAS project. It builds upon and enhances the core functionality of &lt;strong&gt;&lt;code&gt;clasp&lt;/code&gt;&lt;/strong&gt;, providing a streamlined local-to-cloud workflow and making the necessary adjustments to local 'Node specific' ES syntax.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Sandbox-Agent Synergy&lt;/strong&gt;: This combination enables safe, instant Workspace automation. The agent handles the &lt;em&gt;what&lt;/em&gt;, and the sandbox ensures the &lt;em&gt;how&lt;/em&gt;, guaranteeing that code only touches whitelisted resources without the overhead of building complex specialized agents or MCP servers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🛠️ Modern Tooling and Developer Experience
&lt;/h3&gt;

&lt;p&gt;A big pain point for GAS developers is the lack of modern tooling. &lt;code&gt;gas-fakes&lt;/code&gt; eliminates this by providing a full Node.js development stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;NPM Ecosystem:&lt;/strong&gt; Utilize any modern NPM package, allowing access to thousands of specialized libraries.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Advanced Debugging:&lt;/strong&gt; Leverage industry-standard Node.js debuggers (e.g., VS Code, AntiGravity) to step through code, inspect variables, and trace execution paths—a luxury unavailable in the GAS runtime—while simultaneously inspecting client-side HTML Service code in the Chrome debugger in its original structure, line numberings and format.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🛡️ Granular Sandbox and Security
&lt;/h3&gt;

&lt;p&gt;The live GAS environment offers a broad permission model. &lt;code&gt;gas-fakes&lt;/code&gt; provides a fine-grained, developer-controlled sandbox:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;File-Level Whitelisting:&lt;/strong&gt; Define exactly which files or modules the script is allowed to access.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Service-Level Permission Controls:&lt;/strong&gt; Explicitly define which simulated services (e.g., &lt;code&gt;GmailApp&lt;/code&gt;, &lt;code&gt;DriveApp&lt;/code&gt;) the script is permitted to call, allowing for highly secure, auditable execution environments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This optional level of granularity gives protection against Vibe coding hallucination.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔗 Hybrid Interoperability: Bridging Local and Live
&lt;/h3&gt;

&lt;p&gt;You often need to maintain state across environments. &lt;code&gt;gas-fakes&lt;/code&gt; supports &lt;strong&gt;Hybrid Interoperability&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By integrating with external services like Redis or Upstash, &lt;code&gt;gas-fakes&lt;/code&gt; allows the local development environment to share cache data, properties, and session state with the live, deployed GAS instance. This means your local tests are not isolated; they are running against a realistic, persistent state, ensuring seamless transition from development to production. We provide a drop-in replacement property and cache service library for live Apps Script, so you can share exactly the same stores between your local environment and the live deployed GAS.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion: Apps Script as a 'Lingua Franca'
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gas-fakes&lt;/code&gt; elevates Apps Script, a powerful yet constrained scripting language, to the level of a modern, maintainable, and scalable application framework—a lingua franca for Google Workspace integration. It allows teams to write the code they know, test it with the fidelity they require, and deploy it with the control they deserve.&lt;/p&gt;

&lt;p&gt;We are not just emulating GAS; we are liberating it.&lt;/p&gt;

&lt;p&gt;Links&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ramblings.mcpher.com/the-why-and-how-of-gas-fakes/" rel="noopener noreferrer"&gt;This full article&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/brucemcpherson/gas-fakes/" rel="noopener noreferrer"&gt;This repo&lt;/a&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>google</category>
      <category>javascript</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Run Apps Script on an Office 365 back end with gas-fakes</title>
      <dc:creator>Bruce Mcpherson</dc:creator>
      <pubDate>Mon, 16 Mar 2026 14:41:35 +0000</pubDate>
      <link>https://dev.to/brucemcpherson/run-apps-script-on-an-office-365-back-end-with-gas-fakes-1964</link>
      <guid>https://dev.to/brucemcpherson/run-apps-script-on-an-office-365-back-end-with-gas-fakes-1964</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/brucemcpherson/gas-fakes" rel="noopener noreferrer"&gt;gas-fakes&lt;/a&gt; emulates the Google Apps Script (GAS) environment natively within a Node.js runtime. By translating standard GAS service calls into granular API requests, it provides a high-fidelity, local sandbox for debugging, automated testing, and execution without the constraints of the Google Cloud IDE.&lt;/p&gt;

&lt;h2&gt;
  
  
  Microsoft as an Apps Script backend
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ramblings.mcpher.com/apps-script-with-ksuite/" rel="noopener noreferrer"&gt;Apps Script: A ‘Lingua Franca’ for the Multi-Cloud Era&lt;/a&gt; introduces the concept of replacing Apps Script's regular Workspace backend with ksuite. You write Apps Script code as normal, but behind the scenes gas-fakes translates the code into ksuite API requests.&lt;/p&gt;

&lt;p&gt;I’m now adding the Microsoft Graph (Msgraph) backend. This represents a strategic evolution for the project. This addition allows developers to apply the familiar Apps Script programming model directly to the Microsoft 365 ecosystem.&lt;/p&gt;

&lt;p&gt;By acting as a “lingua franca” for workspace platforms, gas-fakes enables you to treat the underlying productivity suite as a pluggable component.&lt;/p&gt;

&lt;p&gt;This allows for the maintenance of a single business logic codebase that can target both Google Workspace and Microsoft 365.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring the Microsoft Graph Backend
&lt;/h2&gt;

&lt;p&gt;As usual, handling auth is the trickiest part of all this. However, the gas-fakes cli handles initializing and authentication against Azure to allow access to Msgraph. Just provide a normal apps script manifest that contains all the scopes you want to use.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gas-fakes init&lt;/code&gt; and &lt;code&gt;auth&lt;/code&gt; will handle setting up the necessary app registration (this is similar to the Google Service Account). Access delegation is a little like Google domain wide delegation (DWD).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gas-fakes&lt;/code&gt; automatically translates the google oauthScopes section in your manifest into their Azure equivalents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initializing the platforms you ever want to use
&lt;/h3&gt;

&lt;p&gt;Simply list the backends you want to be able to use in the init phase. The default is of course just “google”.&lt;/p&gt;

&lt;p&gt;For this example, we want Apps Script to be able to access all 3 supported backends from the same project. You need az (the azure cli ) and gcloud (the google cli) installed. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;gas-fakes init -b "google,msgraph,ksuite"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This will ask a series of questions and create any required registrations and service accounts, and create variables in your selected .env file&lt;/p&gt;

&lt;h3&gt;
  
  
  Authing the platform your project want to use
&lt;/h3&gt;

&lt;p&gt;The auth phase will read the information you provided in the init phase, and execute the selected auth process for the selected back end platforms.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gas-fakes auth -b "msgraph,google"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This will set any permissions and scopes on the service accounts and registrations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keyless auth
&lt;/h3&gt;

&lt;p&gt;In both google and microsoft, the authentication processes are ‘keyless’ – meaning gas-fakes doesn’t store service account credentials locally.&lt;/p&gt;

&lt;p&gt;With Google, you have the choice of Application Default Credential (&lt;code&gt;–auth-type adc&lt;/code&gt;) or the default domain wide delegation (the default &lt;code&gt;–-auth-type dwd&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Microsoft is a kind of hybrid. Caveat: there are many variations depending on whether you have a SPO license, and using a business or consumer license. There are additional complications around multi tenants and other weird things.&lt;/p&gt;

&lt;p&gt;I don’t have any of those. So I have only been able at this time to test the consumer account track in the auth process. This means you may get an occassional consent screen popping up occassionally even after the auth stage as the consumer track does not fully support delegation in the way that google does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Development Experience
&lt;/h2&gt;

&lt;p&gt;When targeting the msgraph backend, &lt;code&gt;gas-fakes&lt;/code&gt; maps familiar GAS-style service synchronous calls to the Microsoft Graph API. You can now target OneDrive and Excel data without bothering to learn the nuances of the Microsoft Graph SDK.&lt;/p&gt;

&lt;p&gt;Because the environment emulates the global GAS objects, the same code to process a Google Sheet can be applied to an Excel workbook on OneDrive without modification.&lt;/p&gt;

&lt;p&gt;At the time of writing , I’ve only implemented a subset of the msgraph methods for OneDrive and Excel. I’ll add more over time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: recursively list the entire contents of multiple platforms
&lt;/h3&gt;

&lt;p&gt;Here’s an example app showing how you can combine platforms using the same Apps Script code. Here we are recursively list all the files in Drive, OneDrive and Ksuite&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mcpher/gas-fakes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// run explore on each platform&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msgraph&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rootFolder0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DriveApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRootFolder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--- msgraph Recursive Explorer ---&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;explore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rootFolder0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ksuite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rootFolder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DriveApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRootFolder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--- KSuite Recursive Explorer ---&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;explore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rootFolder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rootFolder2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DriveApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRootFolder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--- Google Workspace Recursive Explorer ---&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;explore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rootFolder2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;explore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;indent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;indent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;FOLDER: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt; (ID: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Show files in this folder&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFiles&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasNext&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;indent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; FILE: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt; (ID: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Drill into subfolders&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;folders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFolders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;folders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasNext&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;explore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;folders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;dual &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example: copying folder contents between platforms
&lt;/h3&gt;

&lt;p&gt;Here we use the same code to copy all the files in a given folder between various combinations of platforms, and validate that each file content has been successfully written&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mcpher/gas-fakes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;demoTransfer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// copy the files from ksuite&lt;/span&gt;
  &lt;span class="nf"&gt;copyFiles&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;sourcePlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ksuite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetPlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sourceFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gas-fakes-assets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;from-ksuite-to-google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// and back again&lt;/span&gt;
  &lt;span class="nf"&gt;copyFiles&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;sourcePlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetPlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ksuite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sourceFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;from-ksuite-to-google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;from-google-to-ksuite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// now copy them from google to ms-graph&lt;/span&gt;
  &lt;span class="nf"&gt;copyFiles&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;sourcePlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetPlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msgraph&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sourceFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;from-ksuite-to-google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;from-google-to-ms-graph&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// and back again&lt;/span&gt;
  &lt;span class="nf"&gt;copyFiles&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;sourcePlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msgraph&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetPlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ksuite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sourceFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;from-google-to-ms-graph&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;from-ms-graph-to-ksuite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// check that the final files in ksuite match the original&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sourceBlobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getBlobs&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;sourcePlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ksuite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sourceFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gas-fakes-assets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finalBlobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getBlobs&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;sourcePlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ksuite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sourceFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;from-ms-graph-to-ksuite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// check blobs by checking their digest&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sourceBlobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;finalBlobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`expected &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sourceBlobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; blobs but got &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;finalBlobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;sourceBlobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sourceDigest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;base64Encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;computeDigest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DigestAlgorithm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MD5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBytes&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finalDigest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;base64Encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;computeDigest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DigestAlgorithm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MD5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;finalBlobs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;getBytes&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sourceDigest&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;finalDigest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt; blob mismatch with &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;finalBlobs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getBlobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;sourcePlatform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sourceFolderName&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// set which platform to use&lt;/span&gt;
  &lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sourcePlatform&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sourceFolders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DriveApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFoldersByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sourceFolderName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sourceFolders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasNext&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Source folder &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sourceFolderName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; not found`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// get the files in that source folder&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sourceFolders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getFiles&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blobsToCopy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasNext&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;blobsToCopy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBlob&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;blobsToCopy&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;copyFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;sourcePlatform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetPlatform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sourceFolderName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetFolderName&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blobsToCopy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getBlobs&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;sourcePlatform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sourceFolderName&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// now use an alternative platform&lt;/span&gt;
  &lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetPlatform&lt;/span&gt;

  &lt;span class="c1"&gt;// create the folder if it doesn't exist&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetFolders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DriveApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFoldersByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetFolderName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;targetFolder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetFolders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasNext&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;targetFolders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DriveApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createFolder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetFolderName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// now copy the blobs to the target folder&lt;/span&gt;
  &lt;span class="nx"&gt;blobsToCopy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;targetFolder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;blobsToCopy&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;demoTransfer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Advanced Feature: Leveraging Native Apps Script Libraries
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gas-fakes&lt;/code&gt; further bridges the platform gap by allowing the execution of native Apps Script libraries within the Node.js emulation layer. &lt;code&gt;gas-fakes&lt;/code&gt; manages the loading and execution of external library dependencies. Any libraries mentioned in your Apps Script manifest will be loaded and available.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 3: Using a live apps script library across platforms
&lt;/h3&gt;

&lt;p&gt;In this case, we’ll use the &lt;a href="https://ramblings.mcpher.com/vuejs-apps-script-add-ons/helper-for-fiddler/" rel="noopener noreferrer"&gt;bmPreFiddler&lt;/a&gt; library to manipulate sheet contents in both platforms. Again we are leveraging &lt;code&gt;gas-fakes&lt;/code&gt; sandbox to both clean up and limit access to intended files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mcpher/gas-fakes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Creates a spreadsheet on the specified platform.
 * @param {Object} params
 * @param {string} params.platform - The target platform (e.g., 'google', 'msgraph').
 * @param {string} params.title - The name of the new spreadsheet.
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createSpreadsheet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;google&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SpreadsheetApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Created spreadsheet &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt; on &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ss&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Sets the active platform for ScriptApp.
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setPlatform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Copies a sheet's data between two platforms and verifies the result.
 * @param {Object} params
 * @param {Object} params.source - Source details {platform, id, sheetName}.
 * @param {Object} params.target - Target details {platform, title}.
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;copySheetBetweenPlatforms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Get a fiddler for the source&lt;/span&gt;
  &lt;span class="nf"&gt;setPlatform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fiddler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bmPreFiddler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PreFiddler&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getFiddler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Create the output spreadsheet on the target platform&lt;/span&gt;
  &lt;span class="nf"&gt;setPlatform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSpreadsheet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Get a fiddler for the destination&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dstFiddler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bmPreFiddler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PreFiddler&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getFiddler&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;createIfMissing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Copy the data and dump to target&lt;/span&gt;
  &lt;span class="nx"&gt;dstFiddler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fiddler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getData&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;dumpValues&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// Verify that both sheets match using fingerprints&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bmPreFiddler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PreFiddler&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getFiddler&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dstFiddler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSheet&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getParent&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dstFiddler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSheet&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fingerPrint&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;fiddler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fingerPrint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bingo: Data matches perfectly&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error: Data fingerprint mismatch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="c1"&gt;// load any libraries&lt;/span&gt;
&lt;span class="nx"&gt;LibHandlerApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// enable sandbox mode&lt;/span&gt;
&lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__behavior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sandBoxMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// create some spreadsheets with data and copy between them&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1h9IGIShgVBVUrUjjawk5MaCEQte_7t32XeEP1Z5jXKQ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;google&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;airport list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// add that to sanbox for read without marking it for trashing&lt;/span&gt;
&lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__behavior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;whitelistFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;copySheetBetweenPlatforms&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msgraph&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test-msgraph-libraries&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}})&lt;/span&gt;

&lt;span class="c1"&gt;// cleanup any files created&lt;/span&gt;
&lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__behavior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trash&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Help us develop gas-fakes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gas-fakes&lt;/code&gt; is an open-source project. We encourage developers to collaborate, contribute to the extension of supported services, and help refine this bridge between the world’s most popular workspace platforms.&lt;/p&gt;

&lt;p&gt;This would be especially helpful if you have Microsoft knowledge and would like to help develop the msgraph connection. Ping me on &lt;a href="mailto:bruce@mcpher.com"&gt;bruce@mcpher.com&lt;/a&gt; if you want to get involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/brucemcpherson/gas-fakes" rel="noopener noreferrer"&gt;gas-fakes&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/brucemcpherson/gas-fakes-containers" rel="noopener noreferrer"&gt;gas-fakes-containers&lt;/a&gt;&lt;br&gt;
More gas-fakes articles: &lt;a href="https://mcpher.com" rel="noopener noreferrer"&gt;desktop liberation&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What is &lt;code&gt;gas-fakes&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;gas-fakes is a powerful emulation layer that lets you run Apps Script projects on Node.js as if they were native. By translating GAS service calls into granular Google API requests, it provides a secure, high-speed sandbox for local debugging and automated testing.&lt;/p&gt;

&lt;p&gt;Built for the modern stack, it features plug-and-play containerization—allowing you to package your scripts as portable microservices or isolated workers. Coupled with automated identity management, gas-fakes handles the heavy lifting of OAuth and credential cycling, enabling your scripts to act on behalf of users or service accounts without manual intervention. It’s the missing link for building robust, scalable Google Workspace automations and AI-driven workflows.&lt;/p&gt;
&lt;h3&gt;
  
  
  Watch the video
&lt;/h3&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/oEjpIrkYpEM"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

</description>
      <category>appsscript</category>
      <category>workspace</category>
      <category>googlecloud</category>
      <category>office365</category>
    </item>
    <item>
      <title>Yes – you can execute native Apps Script with Office 365 back end</title>
      <dc:creator>Bruce Mcpherson</dc:creator>
      <pubDate>Mon, 16 Mar 2026 14:32:32 +0000</pubDate>
      <link>https://dev.to/brucemcpherson/yes-you-can-execute-native-apps-script-with-office-365-back-end-3pjo</link>
      <guid>https://dev.to/brucemcpherson/yes-you-can-execute-native-apps-script-with-office-365-back-end-3pjo</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/brucemcpherson/gas-fakes" rel="noopener noreferrer"&gt;gas-fakes&lt;/a&gt; emulates the Google Apps Script (GAS) environment natively within a Node.js runtime. By translating standard GAS service calls into granular API requests, it provides a high-fidelity, local sandbox for debugging, automated testing, and execution without the constraints of the Google Cloud IDE.&lt;/p&gt;

&lt;h2&gt;
  
  
  Microsoft as an Apps Script backend
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ramblings.mcpher.com/apps-script-with-ksuite/" rel="noopener noreferrer"&gt;Apps Script: A ‘Lingua Franca’ for the Multi-Cloud Era&lt;/a&gt; introduces the concept of replacing Apps Script's regular Workspace backend with ksuite. You write Apps Script code as normal, but behind the scenes gas-fakes translates the code into ksuite API requests.&lt;/p&gt;

&lt;p&gt;I’m now adding the Microsoft Graph (Msgraph) backend. This represents a strategic evolution for the project. This addition allows developers to apply the familiar Apps Script programming model directly to the Microsoft 365 ecosystem.&lt;/p&gt;

&lt;p&gt;By acting as a “lingua franca” for workspace platforms, gas-fakes enables you to treat the underlying productivity suite as a pluggable component.&lt;/p&gt;

&lt;p&gt;This allows for the maintenance of a single business logic codebase that can target both Google Workspace and Microsoft 365.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring the Microsoft Graph Backend
&lt;/h2&gt;

&lt;p&gt;As usual, handling auth is the trickiest part of all this. However, the gas-fakes cli handles initializing and authentication against Azure to allow access to Msgraph. Just provide a normal apps script manifest that contains all the scopes you want to use.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gas-fakes init&lt;/code&gt; and &lt;code&gt;auth&lt;/code&gt; will handle setting up the necessary app registration (this is similar to the Google Service Account). Access delegation is a little like Google domain wide delegation (DWD).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gas-fakes&lt;/code&gt; automatically translates the google oauthScopes section in your manifest into their Azure equivalents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initializing the platforms you ever want to use
&lt;/h3&gt;

&lt;p&gt;Simply list the backends you want to be able to use in the init phase. The default is of course just “google”.&lt;/p&gt;

&lt;p&gt;For this example, we want Apps Script to be able to access all 3 supported backends from the same project. You need az (the azure cli ) and gcloud (the google cli) installed. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;gas-fakes init -b "google,msgraph,ksuite"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This will ask a series of questions and create any required registrations and service accounts, and create variables in your selected .env file&lt;/p&gt;

&lt;h3&gt;
  
  
  Authing the platform your project want to use
&lt;/h3&gt;

&lt;p&gt;The auth phase will read the information you provided in the init phase, and execute the selected auth process for the selected back end platforms.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gas-fakes auth -b "msgraph,google"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This will set any permissions and scopes on the service accounts and registrations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keyless auth
&lt;/h3&gt;

&lt;p&gt;In both google and microsoft, the authentication processes are ‘keyless’ – meaning gas-fakes doesn’t store service account credentials locally.&lt;/p&gt;

&lt;p&gt;With Google, you have the choice of Application Default Credential (&lt;code&gt;–auth-type adc&lt;/code&gt;) or the default domain wide delegation (the default &lt;code&gt;–-auth-type dwd&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Microsoft is a kind of hybrid. Caveat: there are many variations depending on whether you have a SPO license, and using a business or consumer license. There are additional complications around multi tenants and other weird things.&lt;/p&gt;

&lt;p&gt;I don’t have any of those. So I have only been able at this time to test the consumer account track in the auth process. This means you may get an occassional consent screen popping up occassionally even after the auth stage as the consumer track does not fully support delegation in the way that google does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Development Experience
&lt;/h2&gt;

&lt;p&gt;When targeting the msgraph backend, &lt;code&gt;gas-fakes&lt;/code&gt; maps familiar GAS-style service synchronous calls to the Microsoft Graph API. You can now target OneDrive and Excel data without bothering to learn the nuances of the Microsoft Graph SDK.&lt;/p&gt;

&lt;p&gt;Because the environment emulates the global GAS objects, the same code to process a Google Sheet can be applied to an Excel workbook on OneDrive without modification.&lt;/p&gt;

&lt;p&gt;At the time of writing , I’ve only implemented a subset of the msgraph methods for OneDrive and Excel. I’ll add more over time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: recursively list the entire contents of multiple platforms
&lt;/h3&gt;

&lt;p&gt;Here’s an example app showing how you can combine platforms using the same Apps Script code. Here we are recursively list all the files in Drive, OneDrive and Ksuite&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mcpher/gas-fakes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// run explore on each platform&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msgraph&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rootFolder0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DriveApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRootFolder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--- msgraph Recursive Explorer ---&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;explore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rootFolder0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ksuite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rootFolder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DriveApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRootFolder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--- KSuite Recursive Explorer ---&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;explore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rootFolder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rootFolder2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DriveApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRootFolder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--- Google Workspace Recursive Explorer ---&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;explore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rootFolder2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;explore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;indent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;indent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;FOLDER: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt; (ID: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Show files in this folder&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFiles&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasNext&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;indent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; FILE: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt; (ID: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Drill into subfolders&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;folders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFolders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;folders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasNext&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;explore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;folders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;dual &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example: copying folder contents between platforms
&lt;/h3&gt;

&lt;p&gt;Here we use the same code to copy all the files in a given folder between various combinations of platforms, and validate that each file content has been successfully written&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mcpher/gas-fakes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;demoTransfer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// copy the files from ksuite&lt;/span&gt;
  &lt;span class="nf"&gt;copyFiles&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;sourcePlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ksuite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetPlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sourceFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gas-fakes-assets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;from-ksuite-to-google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// and back again&lt;/span&gt;
  &lt;span class="nf"&gt;copyFiles&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;sourcePlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetPlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ksuite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sourceFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;from-ksuite-to-google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;from-google-to-ksuite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// now copy them from google to ms-graph&lt;/span&gt;
  &lt;span class="nf"&gt;copyFiles&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;sourcePlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetPlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msgraph&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sourceFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;from-ksuite-to-google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;from-google-to-ms-graph&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// and back again&lt;/span&gt;
  &lt;span class="nf"&gt;copyFiles&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;sourcePlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msgraph&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetPlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ksuite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sourceFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;from-google-to-ms-graph&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;from-ms-graph-to-ksuite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// check that the final files in ksuite match the original&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sourceBlobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getBlobs&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;sourcePlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ksuite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sourceFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gas-fakes-assets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finalBlobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getBlobs&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;sourcePlatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ksuite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sourceFolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;from-ms-graph-to-ksuite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// check blobs by checking their digest&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sourceBlobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;finalBlobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`expected &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sourceBlobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; blobs but got &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;finalBlobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;sourceBlobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sourceDigest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;base64Encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;computeDigest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DigestAlgorithm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MD5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBytes&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finalDigest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;base64Encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;computeDigest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DigestAlgorithm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MD5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;finalBlobs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;getBytes&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sourceDigest&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;finalDigest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt; blob mismatch with &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;finalBlobs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getBlobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;sourcePlatform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sourceFolderName&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// set which platform to use&lt;/span&gt;
  &lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sourcePlatform&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sourceFolders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DriveApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFoldersByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sourceFolderName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sourceFolders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasNext&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Source folder &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sourceFolderName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; not found`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// get the files in that source folder&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sourceFolders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getFiles&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blobsToCopy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasNext&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;blobsToCopy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBlob&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;blobsToCopy&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;copyFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;sourcePlatform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetPlatform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sourceFolderName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetFolderName&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blobsToCopy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getBlobs&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;sourcePlatform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sourceFolderName&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// now use an alternative platform&lt;/span&gt;
  &lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetPlatform&lt;/span&gt;

  &lt;span class="c1"&gt;// create the folder if it doesn't exist&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetFolders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DriveApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFoldersByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetFolderName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;targetFolder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetFolders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasNext&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;targetFolders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DriveApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createFolder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetFolderName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// now copy the blobs to the target folder&lt;/span&gt;
  &lt;span class="nx"&gt;blobsToCopy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;targetFolder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;blobsToCopy&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;demoTransfer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Advanced Feature: Leveraging Native Apps Script Libraries
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gas-fakes&lt;/code&gt; further bridges the platform gap by allowing the execution of native Apps Script libraries within the Node.js emulation layer. &lt;code&gt;gas-fakes&lt;/code&gt; manages the loading and execution of external library dependencies. Any libraries mentioned in your Apps Script manifest will be loaded and available.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 3: Using a live apps script library across platforms
&lt;/h3&gt;

&lt;p&gt;In this case, we’ll use the &lt;a href="https://ramblings.mcpher.com/vuejs-apps-script-add-ons/helper-for-fiddler/" rel="noopener noreferrer"&gt;bmPreFiddler&lt;/a&gt; library to manipulate sheet contents in both platforms. Again we are leveraging &lt;code&gt;gas-fakes&lt;/code&gt; sandbox to both clean up and limit access to intended files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mcpher/gas-fakes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Creates a spreadsheet on the specified platform.
 * @param {Object} params
 * @param {string} params.platform - The target platform (e.g., 'google', 'msgraph').
 * @param {string} params.title - The name of the new spreadsheet.
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createSpreadsheet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;google&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SpreadsheetApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Created spreadsheet &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt; on &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ss&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Sets the active platform for ScriptApp.
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setPlatform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Copies a sheet's data between two platforms and verifies the result.
 * @param {Object} params
 * @param {Object} params.source - Source details {platform, id, sheetName}.
 * @param {Object} params.target - Target details {platform, title}.
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;copySheetBetweenPlatforms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Get a fiddler for the source&lt;/span&gt;
  &lt;span class="nf"&gt;setPlatform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fiddler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bmPreFiddler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PreFiddler&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getFiddler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Create the output spreadsheet on the target platform&lt;/span&gt;
  &lt;span class="nf"&gt;setPlatform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSpreadsheet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Get a fiddler for the destination&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dstFiddler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bmPreFiddler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PreFiddler&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getFiddler&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;createIfMissing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Copy the data and dump to target&lt;/span&gt;
  &lt;span class="nx"&gt;dstFiddler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fiddler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getData&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;dumpValues&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// Verify that both sheets match using fingerprints&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bmPreFiddler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PreFiddler&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getFiddler&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dstFiddler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSheet&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getParent&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dstFiddler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSheet&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fingerPrint&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;fiddler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fingerPrint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bingo: Data matches perfectly&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error: Data fingerprint mismatch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// load any libraries&lt;/span&gt;
&lt;span class="nx"&gt;LibHandlerApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// enable sandbox mode&lt;/span&gt;
&lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__behavior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sandBoxMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// create some spreadsheets with data and copy between them&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1h9IGIShgVBVUrUjjawk5MaCEQte_7t32XeEP1Z5jXKQ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;google&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;airport list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// add that to sanbox for read without marking it for trashing&lt;/span&gt;
&lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__behavior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;whitelistFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;copySheetBetweenPlatforms&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msgraph&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test-msgraph-libraries&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}})&lt;/span&gt;

&lt;span class="c1"&gt;// cleanup any files created&lt;/span&gt;
&lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__behavior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trash&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Help us develop gas-fakes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gas-fakes&lt;/code&gt; is an open-source project. We encourage developers to collaborate, contribute to the extension of supported services, and help refine this bridge between the world’s most popular workspace platforms.&lt;/p&gt;

&lt;p&gt;This would be especially helpful if you have Microsoft knowledge and would like to help develop the msgraph connection. Ping me on &lt;a href="mailto:bruce@mcpher.com"&gt;bruce@mcpher.com&lt;/a&gt; if you want to get involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/brucemcpherson/gas-fakes" rel="noopener noreferrer"&gt;gas-fakes&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/brucemcpherson/gas-fakes-containers" rel="noopener noreferrer"&gt;gas-fakes-containers&lt;/a&gt;&lt;br&gt;
More gas-fakes articles: &lt;a href="https://mcpher.com" rel="noopener noreferrer"&gt;desktop liberation&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What is &lt;code&gt;gas-fakes&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;gas-fakes is a powerful emulation layer that lets you run Apps Script projects on Node.js as if they were native. By translating GAS service calls into granular Google API requests, it provides a secure, high-speed sandbox for local debugging and automated testing.&lt;/p&gt;

&lt;p&gt;Built for the modern stack, it features plug-and-play containerization—allowing you to package your scripts as portable microservices or isolated workers. Coupled with automated identity management, gas-fakes handles the heavy lifting of OAuth and credential cycling, enabling your scripts to act on behalf of users or service accounts without manual intervention. It’s the missing link for building robust, scalable Google Workspace automations and AI-driven workflows.&lt;/p&gt;
&lt;h3&gt;
  
  
  Watch the video
&lt;/h3&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/oEjpIrkYpEM"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

</description>
      <category>automation</category>
      <category>javascript</category>
      <category>microsoft</category>
      <category>node</category>
    </item>
  </channel>
</rss>
