{"_id":"@baublet/mote","_rev":"4-d91000f3e263e5cc7525ea77343cdca2","name":"@baublet/mote","dist-tags":{"latest":"0.0.4"},"versions":{"0.0.1":{"name":"@baublet/mote","version":"0.0.1","license":"MIT","_id":"@baublet/mote@0.0.1","maintainers":[{"name":"baublet","email":"baublet@gmail.com"}],"bin":{"mote":"dist/mote.js"},"dist":{"shasum":"3a8deccae4280eb1d205cf0a8f9ec6617f05e60a","tarball":"https://registry.npmjs.org/@baublet/mote/-/mote-0.0.1.tgz","fileCount":9,"integrity":"sha512-cFPtEq/EqdDezBhmdffoQ5MmebVK+Tgr5jHLed0PFoQnsdha+lsh2QYsrxXKpFDwqyzaMuZPmoGnZ8bzEzCjiA==","signatures":[{"sig":"MEUCIBJDtfXFGo8bUIME0rmkLeiu3RyvvUXWTSSMa9JHlrIeAiEA4NXoOaKjzc1xwCkazaSDxr2jPuqt6MMHUtxbpE+VJyc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":342475},"type":"module","engines":{"node":">=20.11.0"},"gitHead":"e5906c0b23860369c91a0b794831dc2368458764","scripts":{"test":"node --test --test-concurrency=1 tests/*.test.js","build":"esbuild bin/mote.js --bundle --platform=node --target=node20 --format=esm --banner:js=\"import{createRequire}from'node:module';var require=createRequire(import.meta.url);\" --outfile=dist/mote.js","typecheck":"tsc","prepublishOnly":"npm run build"},"_npmUser":{"name":"baublet","email":"baublet@gmail.com"},"_npmVersion":"11.6.2","description":"Instrument, automate, track, and checkpoint personal dev environment VMs on GCP","directories":{},"_nodeVersion":"24.11.1","dependencies":{},"_hasShrinkwrap":false,"devDependencies":{"esbuild":"^0.27.3","commander":"^12.0.0","cli-table3":"^0.6.5","typescript":"^5.7.0","@types/node":"^22.0.0","write-file-atomic":"^7.0.0"},"_npmOperationalInternal":{"tmp":"tmp/mote_0.0.1_1771474292467_0.34390402405825626","host":"s3://npm-registry-packages-npm-production"}},"0.0.2":{"name":"@baublet/mote","version":"0.0.2","license":"MIT","_id":"@baublet/mote@0.0.2","maintainers":[{"name":"baublet","email":"baublet@gmail.com"}],"bin":{"mote":"dist/mote.js"},"dist":{"shasum":"ecde57db894bce14d828aff7e07cc35933651eeb","tarball":"https://registry.npmjs.org/@baublet/mote/-/mote-0.0.2.tgz","fileCount":9,"integrity":"sha512-lDC9c1uDCRhIis1fPA8pLUchQK2y3gepI0Gsgxv7CoMrYCQE0WWGvtQMWAwolYlccUOZVZ9w6XeLb0J44+H4vw==","signatures":[{"sig":"MEQCIDNv3P6i7XPqJeLcI0lGfKPrVEals6lTTTw1QUvNSwXSAiBe0XNWYuS4o2zVLZmLv+S6V5x+sGYtr/zHbWCseoB5Yw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":342475},"type":"module","engines":{"node":">=20.11.0"},"gitHead":"91e1a96436b26daf37dfc989750a2051291cb26d","scripts":{"test":"node --test --test-concurrency=1 tests/*.test.js","build":"esbuild bin/mote.js --bundle --platform=node --target=node20 --format=esm --banner:js=\"import{createRequire}from'node:module';var require=createRequire(import.meta.url);\" --outfile=dist/mote.js","typecheck":"tsc","prepublishOnly":"npm run build"},"_npmUser":{"name":"baublet","email":"baublet@gmail.com"},"_npmVersion":"10.9.4","description":"Instrument, automate, track, and checkpoint personal dev environment VMs on GCP","directories":{},"_nodeVersion":"24.11.1","dependencies":{},"_hasShrinkwrap":false,"devDependencies":{"esbuild":"^0.27.3","commander":"^12.0.0","cli-table3":"^0.6.5","typescript":"^5.7.0","@types/node":"^22.0.0","write-file-atomic":"^7.0.0"},"_npmOperationalInternal":{"tmp":"tmp/mote_0.0.2_1771474539875_0.5852098614298571","host":"s3://npm-registry-packages-npm-production"}},"0.0.3":{"name":"@baublet/mote","version":"0.0.3","license":"MIT","_id":"@baublet/mote@0.0.3","maintainers":[{"name":"baublet","email":"baublet@gmail.com"}],"bin":{"mote":"dist/mote.js"},"dist":{"shasum":"24a37261d0a1506257910e96285644d97f045361","tarball":"https://registry.npmjs.org/@baublet/mote/-/mote-0.0.3.tgz","fileCount":9,"integrity":"sha512-Kv8buS6WnTw9ZAz0VakWbjneg1D0BDkZlCOlAWP5aK6kU5rMGvpHCzJoGrdvUwvhSGPUe148YgC/tgH7DYVBrw==","signatures":[{"sig":"MEUCIQCHUBkxduXPMJX9bZF8lW1bvT+utDWf2cf5CYsZHaCaBwIgTKCuByRN1XiAzAlR3ckxbx9AdMfFYL+FGa8IWkWFhrg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":342665},"type":"module","engines":{"node":">=20.11.0"},"gitHead":"90f66beba46b2d4b02f956b0f0d56b0bd32af26a","scripts":{"test":"node --test --test-concurrency=1 tests/*.test.js","build":"esbuild bin/mote.js --bundle --platform=node --target=node20 --format=esm --banner:js=\"import{createRequire}from'node:module';var require=createRequire(import.meta.url);\" --outfile=dist/mote.js","release":"./scripts/release.sh","typecheck":"tsc","prepublishOnly":"npm run build"},"_npmUser":{"name":"baublet","email":"baublet@gmail.com"},"_npmVersion":"10.9.4","description":"Instrument, automate, track, and checkpoint personal dev environment VMs on GCP","directories":{},"_nodeVersion":"24.11.1","dependencies":{},"_hasShrinkwrap":false,"devDependencies":{"esbuild":"^0.27.3","commander":"^12.0.0","cli-table3":"^0.6.5","typescript":"^5.7.0","@types/node":"^22.0.0","write-file-atomic":"^7.0.0"},"_npmOperationalInternal":{"tmp":"tmp/mote_0.0.3_1771475307999_0.5883442084376329","host":"s3://npm-registry-packages-npm-production"}},"0.0.4":{"name":"@baublet/mote","version":"0.0.4","description":"Instrument, automate, track, and checkpoint personal dev environment VMs on GCP","type":"module","bin":{"mote":"dist/mote.js"},"scripts":{"build":"esbuild bin/mote.js --bundle --platform=node --target=node20 --format=esm --banner:js=\"import{createRequire}from'node:module';var require=createRequire(import.meta.url);\" --outfile=dist/mote.js","typecheck":"tsc","test":"node --test --test-concurrency=1 tests/*.test.js","prepublishOnly":"npm run build","release":"./scripts/release.sh"},"dependencies":{},"devDependencies":{"@types/node":"^22.0.0","cli-table3":"^0.6.5","commander":"^12.0.0","esbuild":"^0.27.3","typescript":"^5.7.0"},"engines":{"node":">=20.11.0"},"license":"MIT","_id":"@baublet/mote@0.0.4","gitHead":"9ad5d3036479214cc4dd6ca86cfb97ab49efb413","_nodeVersion":"24.11.1","_npmVersion":"10.9.4","dist":{"integrity":"sha512-3Vgw+O4FXdjqNxGJaciP/0zuoWD6ebZwlhC81fcPZGDfcZsdGGvSaJsMCGjwm7XzZyCKCPkHuTZj+MxUUAjdYA==","shasum":"eb23465393e9458f7b3776dc47f7b2a8ed1c0c87","tarball":"https://registry.npmjs.org/@baublet/mote/-/mote-0.0.4.tgz","fileCount":9,"unpackedSize":323461,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQD7oI/7zo5bTwPQIs5l5WBK3P9Ci0BcvGbW7oBqgwXmAAIhANECda5SS78uzk//7rYAvza81GwaxIFt5K2ZB7JU6zsS"}]},"_npmUser":{"name":"baublet","email":"baublet@gmail.com"},"directories":{},"maintainers":[{"name":"baublet","email":"baublet@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/mote_0.0.4_1771515954100_0.06767444428411129"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-19T04:11:32.302Z","modified":"2026-02-19T15:45:54.369Z","0.0.1":"2026-02-19T04:11:32.645Z","0.0.2":"2026-02-19T04:15:40.083Z","0.0.3":"2026-02-19T04:28:28.219Z","0.0.4":"2026-02-19T15:45:54.261Z"},"license":"MIT","description":"Instrument, automate, track, and checkpoint personal dev environment VMs on GCP","maintainers":[{"name":"baublet","email":"baublet@gmail.com"}],"readme":"# mote\n\n**Instrument, automate, track, and checkpoint personal dev environments on GCP.**\n\nSize slugs. GPU shorthands. Layered config. Minimal dependencies.\n\n## Vision\n\nVMs are a better development experience than containers: full OS, real systemd, easier virtualization, no layered filesystem quirks. Mote gives you a simple interface for steering those VMs in GCP, with an API tailored for LLM use that's also easy for humans.\n\nThe end goal: point an AI agent at a new repo's `LLM.txt`, and it uses Mote to create, configure, snapshot, branch, clone, and tear down dev environments on your behalf. You get reproducible, GPU-capable workspaces you can spin up, fork, and destroy without thinking about infrastructure.\n\n## Install\n\n```bash\nnpm install -g mote   # or: git clone … && npm link\n```\n\nRequires: Node ≥ 20, [gcloud CLI](https://cloud.google.com/sdk) authed.\n\n## Setup\n\n```bash\n# point at your project (visible everywhere, persisted)\nmote config set project my-gcp-project\n\n# optionally change zone\nmote config set zone us-west1-b\n\n# see full config (shows source of each value)\nmote config get\n```\n\n```\nConfiguration\n──────────────────────────────────────────────────\n  project        my-gcp-project               config\n  zone           us-west1-b                   config\n  machine        medium                       default\n  disk           100                          default\n  diskType       pd-balanced                  default\n  image          ubuntu-2404-lts-amd64        default\n  imageProject   ubuntu-os-cloud              default\n  tag            mote-vm                      default\n  exposeMethod   tunnel                       default\n  autoIdleTimeout 180                         default\n  showTips       true                         default\n```\n\nConfig layers: **code defaults → `~/.mote/config.json` → CLI flags**.\nEvery default is overridable at every level.\n\n## Quick start\n\n```bash\nmote create my-app                      # medium (4 vCPU, 16 GB)\nmote run my-app echo hello              # run a command\nmote ssh my-app                         # interactive shell\nmote save my-app \"clean install\"        # checkpoint\nmote restore my-app \"clean\"             # fuzzy match\nmote stop my-app                        # storage-only billing\nmote start my-app                       # resume\nmote rm my-app                          # destroy\n```\n\n## Accessing your mote\n\n### SSH\n\n```bash\nmote ssh my-app                         # interactive shell\nmote ssh my-app                         # aliases: mote console, mote c\n```\n\n### Running commands\n\n```bash\nmote run my-app uname -a                # run a single command\nmote run my-app -- ls -la /etc          # use -- for flags\nmote run my-app --sudo apt update       # run as root\n```\n\n### File transfer\n\n```bash\nmote push my-app ./local-file.txt       # upload to ~ on the mote\nmote push my-app ./src ~/project/src    # upload to specific path\nmote pull my-app ~/data/results.csv     # download to current directory\nmote pull my-app ~/data ./local-data    # download to specific path\n```\n\n### Scripts\n\n```bash\nmote script my-app ./setup.sh           # upload and run a local script\nmote script my-app ./setup.sh --sudo    # run as root\nmote install my-app docker              # run a bundled install script\nmote install --list                     # see available scripts\n```\n\n### Git + SSH keys\n\nCopy your host's `~/.gitconfig` and SSH keys to the mote so you can `git clone` private repos immediately:\n\n```bash\nmote install my-app git\n```\n\nThis copies `~/.gitconfig` and `~/.ssh/id_*` from your machine to the mote, fixes permissions, and adds github.com/gitlab.com/bitbucket.org to `known_hosts`.\n\n> **Note:** This copies your private SSH keys to the VM. For personal dev environments this is fine. If you'd prefer not to copy keys, use SSH agent forwarding instead:\n> ```bash\n> gcloud compute ssh mote-my-app --zone us-central1-a -- -A\n> ```\n> Agent forwarding lets the VM use your local keys without copying them, but only while you're connected.\n\n### Port forwarding\n\nTo access a service running on your mote (e.g. a web server on port 8080), use `mote expose`:\n\n```bash\nmote expose my-app 8080                 # → public HTTPS URL via Cloudflare tunnel\nmote expose my-app 3000,8080            # multiple ports\nmote info my-app                        # see tunnel URLs\nmote unexpose my-app                    # close all\n```\n\nThis creates a [Cloudflare Quick Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/) — automatic HTTPS, no firewall rules, no raw IP exposure. `cloudflared` is installed on the mote automatically on first use. Tunnel URLs are ephemeral and change on restart.\n\nFor a stable IP-based endpoint (opens GCP firewall rules with `0.0.0.0/0` ingress):\n\n```bash\nmote expose my-app 8080 --method firewall\nmote unexpose my-app --method firewall\n```\n\nSet the default method permanently with `mote config set exposeMethod firewall`.\n\nYou can also use standard `gcloud` SSH port forwarding for local-only access without exposing anything publicly:\n\n```bash\ngcloud compute ssh mote-my-app --zone us-central1-a -- -L 8080:localhost:8080\n# now localhost:8080 on your machine → port 8080 on the mote\n```\n\n## Size slugs\n\nInstead of remembering `e2-standard-4`, use a slug:\n\n```bash\nmote create tiny  --machine xs           # e2-small (2 vCPU, 2 GB)\nmote create app   --machine small        # e2-standard-2 (2 vCPU, 8 GB)\nmote create app   --machine medium       # e2-standard-4 (4 vCPU, 16 GB)  ← default\nmote create app   --machine large        # e2-standard-8 (8 vCPU, 32 GB)\nmote create app   --machine xl           # e2-standard-16 (16 vCPU, 64 GB)\nmote create app   --machine 2xl          # e2-standard-32 (32 vCPU, 128 GB)\n```\n\nOr pass any GCP machine type directly:\n\n```bash\nmote create app --machine n2-standard-8  # validated against zone catalog\n```\n\nSet your preferred default:\n\n```bash\nmote config set machine large            # all future motes are 8 vCPU\n```\n\n## GPU slugs\n\n```bash\nmote create ml --gpu t4                  # Tesla T4 → auto n1-standard-8\nmote create ml --gpu l4                  # L4 → auto g2-standard-8\nmote create ml --gpu a100                # A100 → auto a2-highgpu-1g\nmote create ml --gpu t4 --gpu-count 4    # 4× T4\nmote create ml --gpu t4 --machine large  # override machine for GPU\n```\n\nSee all slugs: `mote catalog sizes`\n\n## Cloning\n\nClone a mote to get an identical copy — same disk contents, machine type, GPU config, and schedule settings:\n\n```bash\nmote clone my-app my-app-v2               # full clone (disk + settings + schedule)\nmote clone my-app experiment --stopped     # clone but don't start it\nmote clone my-app my-app-v2 --from \"baseline\"  # clone from a specific save\nmote clone my-app my-app-v2 --no-copy-schedule # clone without schedule/auto-idle settings\n```\n\nBy default, clone copies the source mote's scheduled start/stop times and auto-idle timeout. Use `--no-copy-schedule` to skip that.\n\n## Catalog\n\nMachine types and GPUs are fetched from GCP and cached (24h):\n\n```bash\nmote catalog machines                    # what's in your zone\nmote catalog gpus                        # GPUs in your zone\nmote catalog sizes                       # all slug → type mappings\nmote catalog refresh                     # force refresh cache\nmote catalog machines --zone europe-west1-b   # different zone\n```\n\nIf offline or no project configured, falls back to hardcoded catalog.\n\n## Config\n\n```bash\nmote config get                          # show all (with source labels)\nmote config get project                  # show one key\nmote config set project my-proj          # set\nmote config set zone us-east1-b          # change zone\nmote config set machine large            # change default size\nmote config set disk 100                 # bigger default disk\nmote config unset machine                # revert to code default\nmote config path                         # ~/.mote/config.json\n```\n\n**Override anything per-command:**\n\n```bash\nmote create my-app --project other-proj --zone asia-east1-a --machine xl\n```\n\nPriority: CLI flag > config.json > code default > gcloud default.\n\n## Workspace isolation\n\nBy default mote stores state in `~/.mote/`. Use `--state` to isolate per-project:\n\n```bash\nmote --state ./my-project/.mote/state.json create dev\nmote --state ./my-project/.mote/state.json ls\n```\n\nEach workspace gets its own state, config, and scheduler cron entry.\n\n## Scheduling\n\nMotes cost money while running. The scheduler saves you money by automatically stopping idle VMs and optionally starting/stopping them on a time-based schedule.\n\n### How it works\n\nThe scheduler is a lightweight cron job that runs on your **host machine** (laptop, desktop, whatever you run `mote` from). When you run `mote scheduler install`, it adds a single line to your crontab that calls `mote scheduler tick` once per minute. Each tick:\n\n1. Loops through your running motes\n2. SSHes in and checks for active users (via the `w` command)\n3. Stops any mote that's been idle longer than the timeout\n4. Starts/stops motes based on their time schedules (if configured)\n\nThe tick command is fast and quiet — it reads `~/.mote/state.json`, makes a few quick SSH calls, and exits. Output goes to `~/.mote/scheduler.log`.\n\n> **Important:** Your host machine must stay on for this to work. If your laptop sleeps or shuts down, the cron job won't run and motes won't be auto-managed.\n\n### Installing the scheduler\n\n```bash\nmote scheduler install\n```\n\nThis does one thing: adds a crontab entry like:\n\n```\n* * * * * /path/to/mote scheduler tick >> ~/.mote/scheduler.log 2>&1\n```\n\nNo daemons, no background processes, no system services — just a standard cron job. You can verify it with `crontab -l` and remove it anytime with `mote scheduler uninstall`.\n\n```bash\nmote scheduler status     # check if installed, when it last ran, mote counts\nmote scheduler uninstall  # remove the cron job\n```\n\n### Auto-idle (default: ON, 3 hours)\n\nOnce the scheduler is installed, it checks each running mote for activity every minute. \"Activity\" means someone is logged in via SSH — the scheduler runs `w -hs | wc -l` via SSH and only considers a mote active when more than one session is present (the check's own SSH session counts as one).\n\n- If users are connected: records the mote as active\n- If nobody is connected and the mote has been idle longer than the timeout: warm-stops it\n\nThe default timeout is 3 hours. Change it globally:\n\n```bash\nmote config set autoIdleTimeout 180   # 3 hours (default)\nmote config set autoIdleTimeout 60    # 1 hour\nmote config set autoIdleTimeout 0     # disable auto-idle globally\n```\n\nOr override per-mote:\n\n```bash\nmote schedule my-app --auto-idle 120  # 2h timeout for this mote only\nmote schedule my-app --no-auto-idle   # never auto-stop this mote\n```\n\n### Scheduled stop/start (opt-in)\n\nYou can also set time-based schedules to stop and start motes automatically:\n\n```bash\nmote schedule my-app --stop-at 20:00 --start-at 08:00                    # daily\nmote schedule my-app --stop-at 20:00 --start-at 08:00 --days mon,tue,wed,thu,fri  # weekdays only\nmote schedule my-app --stop-at 20:00 --start-at 08:00 --tz America/New_York       # specific timezone\nmote schedule my-app --clear                                              # remove schedule\nmote schedule my-app                                                      # view current settings\n```\n\n### Testing the scheduler\n\nYou can run a tick manually and override the idle timeout for quick testing:\n\n```bash\nmote scheduler tick                       # run one cycle now\nmote scheduler tick --auto-idle-timeout 1  # use 1-minute timeout (for testing)\n```\n\n## All commands\n\n```\nCONFIG\n  mote config get [key]       Show config\n  mote config set <key> <val> Set a value\n  mote config unset <key>     Revert to default\n  mote config path            Config file location\n\nCATALOG\n  mote catalog machines       Machine types in zone\n  mote catalog gpus           GPU types in zone\n  mote catalog sizes          All slug mappings\n  mote catalog refresh        Force refresh cache\n\nMOTES\n  mote create <name>             Create (--machine, --gpu, --expose, etc.)\n  mote ls                        List (grouped by project)\n  mote run <name> <cmd...>       Run a command\n  mote ssh <name>                Interactive shell (aliases: console, c)\n  mote save <name> [label]       Checkpoint\n  mote saves <name>              List checkpoints\n  mote restore <name> [save]     Restore (id, label, or latest)\n  mote stop <name>               Stop (storage-only)\n  mote start <name>              Start\n  mote expose <name> <ports>     Expose ports (HTTPS tunnel by default)\n  mote unexpose <name> [ports]   Close exposed ports\n  mote push <name> <src> [dst]   Upload files\n  mote pull <name> <src> [dst]   Download files\n  mote clone <src> <name>        Clone a mote (--no-copy-schedule to skip)\n  mote script <name> <path>      Upload + run script\n  mote install [name] [software] Install bundled script (docker, node, vscode, git)\n  mote install --list            List available install scripts\n  mote info <name>               Details\n  mote resize <name>             Change size (--machine, --gpu, --gpu-count, --disk)\n  mote cost [name]               Cost summary (all) or detailed breakdown (one)\n  mote rm <name>                 Destroy\n  mote nuke                      Destroy everything\n\nMAINTENANCE\n  mote gc                        Reconcile state against GCP\n  mote cron                      Cold-store warm-stopped motes\n  mote skill                     Install Claude Code skill into cwd\n\nSCHEDULER\n  mote scheduler install      Install cron job\n  mote scheduler uninstall    Remove cron job\n  mote scheduler status       Show scheduler status\n  mote scheduler tick         Run one scheduler cycle\n\nSCHEDULE\n  mote schedule <name>           View/set per-mote schedule\n```\n\n## Cost\n\n| State | Pay for |\n|---|---|\n| Running | vCPUs + RAM + GPU + disk + IP |\n| Warm-stopped (`mote stop`) | Disk only (~$10/mo for 100 GB pd-balanced) |\n| Cold-stored (`mote stop --freeze`) | Snapshot only (~$2.60/mo for 100 GB) |\n| Saves | Snapshots (~$0.026/GB/mo, incremental) |\n| Removed | $0 |\n\n### Cost command\n\n```bash\nmote cost                         # summary table of all motes\nmote cost my-app                  # detailed breakdown for one mote\n```\n\n`mote cost` (no name) shows a table with current cost and projected monthly for every mote.\n\n`mote cost <name>` shows a per-component rate card (compute, GPU, disk, IP), warm-stopped/cold-stored costs, and — if a schedule is set — a projected blended monthly cost with savings vs 24/7 running.\n\n### Disk resize\n\n```bash\nmote resize my-app --disk 200     # grow disk to 200 GB (GCP only allows growing)\nmote resize my-app --disk 200 --machine large  # resize disk and machine together\n```\n\nDisk resize works in any state. For running/warm-stopped motes, the disk is resized live via `gcloud compute disks resize`. For cold-stored motes, the new size is recorded and applied on next `mote start`.\n\n## Testing\n\n### Prerequisites\n\n1. **Node.js** ≥ 20\n2. **gcloud CLI** installed and authenticated (mote checks this on every command):\n\n```bash\n# install: https://cloud.google.com/sdk/docs/install\ngcloud auth login\ngcloud config set project <YOUR_PROJECT_ID>\n```\n\n3. **Install dependencies:**\n\n```bash\nnpm install\n```\n\n### Typecheck\n\n```bash\nnpm run typecheck    # runs tsc over all JS with JSDoc types\n```\n\n### Manual test sequence\n\nRun these in order. Each step builds on the last.\n\n```bash\n# 1. Preflight — verify gcloud detection works\nnode bin/mote.js --help                # should print help (no gcloud check)\nnode bin/mote.js ls                    # should pass preflight, print \"No motes yet\"\n\n# 2. Config\nnode bin/mote.js config get            # show resolved config with source labels\nnode bin/mote.js config path           # print ~/.mote/config.json path\n\n# 3. Catalog (hits GCP API, verifies auth + project)\nnode bin/mote.js catalog sizes         # list slug mappings (no API call)\nnode bin/mote.js catalog machines      # fetch machine types for default zone\nnode bin/mote.js catalog gpus          # fetch GPU types for default zone\n\n# 4. Create a mote\nnode bin/mote.js create test-mote      # default medium (e2-standard-4, 100 GB)\nnode bin/mote.js ls                    # should show test-mote RUNNING with IP\nnode bin/mote.js info test-mote        # full details\n\n# 5. Run commands on it\nnode bin/mote.js run test-mote uname -a\nnode bin/mote.js run test-mote whoami\n\n# 5b. Install scripts\nnode bin/mote.js install --list            # should list docker, git, node, vscode\nnode bin/mote.js install test-mote git     # copies SSH keys + git config, sets up known_hosts\n\n# 6. Save + restore\nnode bin/mote.js save test-mote \"baseline\"\nnode bin/mote.js saves test-mote       # should show the save\nnode bin/mote.js restore test-mote \"baseline\"\n\n# 7. Stop / start cycle\nnode bin/mote.js stop test-mote\nnode bin/mote.js ls                    # status should be WARM_STOPPED\nnode bin/mote.js start test-mote\nnode bin/mote.js ls                    # status should be RUNNING\n\n# 8. Cleanup\nnode bin/mote.js rm test-mote -y       # destroy VM + disk + saves + firewall rules\nnode bin/mote.js ls                    # should be empty again\n```\n\n### Optional: GPU test\n\nRequires GPU quota in your project/zone:\n\n```bash\nnode bin/mote.js create gpu-test --gpu t4\nnode bin/mote.js run gpu-test nvidia-smi\nnode bin/mote.js rm gpu-test -y\n```\n\n### Optional: Clone + expose test\n\n```bash\nnode bin/mote.js create src-mote\nnode bin/mote.js clone src-mote dst-mote\nnode bin/mote.js expose dst-mote 8080                  # tunnel (default)\nnode bin/mote.js expose dst-mote 443 --method firewall # firewall\nnode bin/mote.js info dst-mote                         # shows both\nnode bin/mote.js unexpose dst-mote\nnode bin/mote.js rm src-mote -y\nnode bin/mote.js rm dst-mote -y\n```\n\n## Architecture\n\n```\nmote/\n  bin/mote.js       CLI entry + command routing (commander)\n  src/\n    commands.js       command handlers\n    config.js         layered config (code → user → CLI)\n    db.js             JSON state store (atomic writes)\n    gcloud.js         thin gcloud CLI wrapper (sync, async, interactive)\n    catalog.js        GCP machine/GPU catalog + 24h cache\n    slugs.js          size/GPU slug resolution\n    pricing.js        cost estimation (hourly + monthly)\n    scheduler.js      cron-based auto-idle + scheduled start/stop\n    reconcile.js      drift detection (state ↔ GCP)\n    tips.js           contextual tips\n    warnLongOp.js     duration warnings for long operations\n  scripts/\n    <name>/vm.sh       VM-side install script (runs with sudo on the mote)\n    <name>/host.sh     host-side script (runs locally before vm.sh)\n```\n\nInstall scripts live in `scripts/<name>/`. Each script can have a `vm.sh` (uploaded and run on the mote with `sudo`), a `host.sh` (run locally on your machine first), or both. Host scripts receive `MOTE_INSTANCE`, `MOTE_ZONE`, and `MOTE_PROJECT` as environment variables for `gcloud` calls.\n\nThree bundled dependencies (`commander`, `cli-table3`, `write-file-atomic`). No native modules. Source runs directly (`node bin/mote.js`); esbuild bundles to `dist/mote.js` for distribution.\n\nState: `~/.mote/state.json`\nConfig: `~/.mote/config.json`\nCache: `~/.mote/cache/<zone>-machines.json`, `<zone>-gpus.json`\n","readmeFilename":"README.md"}