E5CF GitHub - Dicklesworthstone/giil: Zero-setup bash CLI that downloads full-resolution images from iCloud/Dropbox/Google Photos share links, bridging iPhone screenshots to remote AI coding sessions Β· GitHub
[go: up one dir, main page]

Skip to content

Dicklesworthstone/giil

Repository files navigation

Version Platform Runtime License

giil

Get Image [from] Internet Link

A zero-setup CLI that downloads full-resolution images from cloud photo shares

The missing link between your iPhone screenshots and remote AI coding sessions.
Share an image via iCloud, paste the link into your SSH terminal, and your AI assistant can see it instantly.

Single-file bash script with embedded Node.js extractor. Auto-installs all dependencies.
Supports single photos, entire albums, JSON metadata output, and base64 encoding.


curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/giil/main/install.sh?v=3.0.0" | bash


🎯 The Primary Use Case: Remote AI-Assisted Debugging

The scenario: You're SSH'd into a remote server running Claude Code, Codex, or another AI coding assistant. You need to debug a UI issue on your iPhone, but how do you get that screenshot to your remote terminal session?

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   iPhone    │────▢│  iCloud     │────▢│  Photos.app │────▢│  Share Link β”‚
β”‚ Screenshot  β”‚     β”‚   Sync      β”‚     β”‚   (Mac)     β”‚     β”‚   (Copy)    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                                   β”‚
                                                                   β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  AI Agent   │◀────│    giil     │◀────│   Paste     │◀────│ Remote SSH  β”‚
β”‚  Analyzes   β”‚     β”‚  Downloads  β”‚     β”‚   URL       β”‚     β”‚  Terminal   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The workflow:

  1. Screenshot the UI bug on your iPhone
  2. Wait a moment for iCloud to sync to your Mac
  3. Right-click the image in Photos.app β†’ Share β†’ Copy iCloud Link
  4. Paste the link into your remote terminal session
  5. Run giil and the image is now local to your remote machine
# On your remote server (SSH session with Claude Code/Codex)
giil "https://share.icloud.com/photos/0a1Abc_xYz..." --format json

# AI assistant can now analyze the screenshot directly
# {"path": "/tmp/icloud_20240115_143022.jpg", "width": 1170, "height": 2532, ...}

Comparison:

Without giil With giil
Download image locally, SCP to server, tell AI the path One command, AI sees it instantly
Email yourself, download on server, hope it works Paste link, done
Set up complex file sync between devices Just use iCloud's built-in sharing
Break your flow to context-switch between devices Stay in your terminal

This bridges your Apple devices and remote AI coding sessions. No file transfers, no context switching, no friction.


Table of Contents


πŸ’‘ Why giil Exists

Cloud photo sharing services present unique challenges for automation:

Problem Why It's Hard How giil Solves It
JavaScript-heavy SPAs Standard curl/wget can't execute JS or render pages Headless Chromium via Playwright (or direct download for Dropbox)
Dynamic image loading Images load asynchronously from CDN after page render Network interception captures CDN responses
No direct download links URLs are session-specific and expire quickly Clicks native Download button or intercepts live requests
Copy/paste loses quality Manual screenshots result in compressed/cropped images Captures original resolution from source
HEIC format on Apple devices Many tools can't process Apple's HEIC/HEIF format Platform-aware conversion (sips/heif-convert)
Platform fragmentation Each service has different URL patterns and APIs Automatic platform detection with optimized strategies

giil lets you programmatically download full-resolution images from iCloud, Dropbox, Google Photos, and Google Drive share linksβ€”which is otherwise impossible without manual browser interaction.

Typical workflow: Debugging a UI issue with Claude Code or Codex on a remote server? Screenshot on iPhone β†’ iCloud syncs β†’ Share link from Photos.app β†’ Paste into SSH terminal β†’ giil fetches it β†’ AI analyzes the image. No SCP, no email, no friction.


✨ Highlights

Zero-Setup Installation

One-liner installer handles everything:

  • Node.js detection/installation
  • Playwright + Chromium (~200MB, cached)
  • Sharp image processing library
  • Optional gum for beautiful CLI output

Four-Tier Capture Strategy

Maximum reliability through intelligent fallbacks:

  1. Download button β†’ Original file
  2. CDN interception β†’ Full resolution
  3. Element screenshot β†’ Rendered image
  4. Viewport screenshot β†’ Last resort

Album Support

Download entire shared albums with --all:

  • Automatic thumbnail detection
  • Sequential full-resolution capture
  • Collision-free filenames with indices
  • Continues on individual failures

Flexible Output

Multiple output modes for any workflow:

  • File path β†’ Default, for scripting
  • JSON metadata β†’ Path, datetime, dimensions
  • Base64 β†’ Embedding, piping, APIs
  • Album mode β†’ One output per photo

Smart Filenames

EXIF-aware datetime stamping:

  • Extracts DateTimeOriginal from EXIF
  • Falls back to capture timestamp
  • Format: icloud_YYYYMMDD_HHMMSS.jpg
  • Automatic collision avoidance

Image Processing

MozJPEG compression by default:

  • Optimal size/quality balance
  • --preserve to keep original bytes
  • --convert for format conversion
  • HEIC auto-conversion supported

⚑ Quickstart

Installation

One-liner (recommended):

curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/giil/main/install.sh?v=3.0.0" | bash
Manual installation
# Download script
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/giil/main/giil -o ~/.local/bin/giil
chmod +x ~/.local/bin/giil

# Ensure ~/.local/bin is in PATH
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc  # or ~/.bashrc
source ~/.zshrc
Installation options
# Custom install directory
DEST=/opt/bin curl -fsSL .../install.sh | bash

# System-wide installation (requires sudo)
GIIL_SYSTEM=1 curl -fsSL .../install.sh | bash

# Skip PATH modification
GIIL_NO_ALIAS=1 curl -fsSL .../install.sh | bash

# Verified installation with SHA256 checksum
GIIL_VERIFY=1 curl -fsSL .../install.sh | bash

# Install specific version
GIIL_VERSION=2.1.0 curl -fsSL .../install.sh | bash
Checksum verification

For security-conscious installations, giil supports SHA256 checksum verification:

GIIL_VERIFY=1 curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/giil/main/install.sh | bash

How it works:

  1. Downloads giil script from the release
  2. Fetches giil.sha256 checksum file from the same release
  3. Computes SHA256 of downloaded file
  4. Compares against expected checksum
  5. Aborts installation if mismatch detected

Requirements:

  • Either sha256sum (Linux) or shasum (macOS)
  • GitHub release must include giil.sha256 file

Output on success:

βœ“ Checksum verified: a1b2c3d4e5f6...

First Run

giil "https://share.icloud.com/photos/02cD9okNHvVd-uuDnPCH3ZEEA"

Note: First run downloads Playwright Chromium (~200MB). This is cached for future runs in ~/.cache/giil/.


πŸš€ Usage

giil <icloud-photo-url> [options]

Options

Flag Default Description
--output DIR . Output directory for saved images
--preserve off Preserve original bytes (skip MozJPEG compression)
--convert FMT β€” Convert to format: jpeg, png, webp
--quality N 85 JPEG quality 1-100
--format FMT β€” Structured output format: json or toon
--base64 off Output base64 to stdout instead of saving file
--json off Output JSON metadata (alias for --format json)
--all off Download all photos from a shared album
--timeout N 60 Page load timeout in seconds
--debug off Save debug artifacts (screenshot + HTML) on failure
--verbose off Show detailed progress (implies --debug)
--trace off Enable Playwright tracing for deep debugging
--print-url off Output resolved direct URL instead of downloading
--debug-dir DIR . Directory for debug artifacts
--update off Force reinstall of Playwright and dependencies
--version β€” Print version and exit
--help β€” Show help message

Default Behavior: Images are compressed with MozJPEG for optimal size/quality balance. Use --preserve to keep original bytes without recompression.

Supported Platforms

giil automatically detects the sharing platform and uses the optimal download strategy:

Platform URL Patterns Method Browser Required
iCloud share.icloud.com/photos/*
icloud.com/photos/#*
4-tier capture strategy Yes
Dropbox dropbox.com/s/*
dropbox.com/scl/fi/*
Direct curl download (raw=1) No
Google Photos photos.app.goo.gl/*
photos.google.com/share/*
URL extraction + =s0 modifier Yes
Google Drive drive.google.com/file/d/*
drive.google.com/open?id=*
Multi-tier with auth detection Yes

Dropbox Fast Path: Dropbox links are downloaded directly via curl with no browser overheadβ€”typically completes in under 2 seconds.

Google Photos Full-Resolution: giil automatically appends =s0 to CDN URLs to request maximum resolution (the s0 modifier bypasses size restrictions).

Supported URL Formats

iCloud (both formats automatically normalized):

https://share.icloud.com/photos/02cD9okNHvVd-uuDnPCH3ZEEA
https://www.icloud.com/photos/#02cD9okNHvVd-uuDnPCH3ZEEA

Dropbox:

https://www.dropbox.com/s/abc123/photo.jpg
https://www.dropbox.com/scl/fi/xyz789/image.png

Google Photos:

https://photos.app.goo.gl/abc123xyz
https://photos.google.com/share/AF1QipN...

Google Drive:

https://drive.google.com/file/d/1ABC.../view
https://drive.google.com/open?id=1ABC...

πŸ“€ Output Modes

Default: File Path

Returns the absolute path to the saved image on stdout.

giil "https://share.icloud.com/photos/XXX"
# stdout: /current/dir/icloud_20240115_143245.jpg
# stderr: [giil] Saved: /current/dir/icloud_20240115_143245.jpg (234.5 KB, network)

Use in scripts:

IMAGE_PATH=$(giil "https://share.icloud.com/photos/XXX" --output ~/Downloads 2>/dev/null)
echo "Downloaded: $IMAGE_PATH"

JSON Mode: --format json (or --json)

Returns structured metadata for programmatic use. The JSON schema includes metadata for reliable scripting.

giil "https://share.icloud.com/photos/XXX" --format json

Success response:

{
  "ok": true,
  "schema_version": "1",
  "platform": "icloud",
  "path": "/absolute/path/to/icloud_20240115_143245.jpg",
  "datetime": "2024-01-15T14:32:45.000Z",
  "sourceUrl": "https://cvws.icloud-content.com/...",
  "method": "network",
  "size": 245678,
  "width": 4032,
  "height": 3024
}

Error response:

{
  "ok": false,
  "schema_version": "1",
  "platform": "icloud",
  "error": {
    "code": "AUTH_REQUIRED",
    "message": "Login required - link is not publicly shared",
    "remediation": "The file is not publicly shared. The owner must enable public access."
  }
}
Field Description
ok Boolean success indicator (true or false)
schema_version JSON schema version (currently "1")
platform Detected platform: icloud, dropbox, gphotos, gdrive
path Absolute path to saved file
datetime ISO 8601 timestamp (from EXIF or capture time)
sourceUrl CDN URL where image was obtained
method Capture strategy: download, network, element-screenshot, viewport-screenshot, direct
size File size in bytes
width Image width in pixels
height Image height in pixels
error.code Error code (see Exit Codes)
error.message Human-readable error description
error.remediation Suggested fix for the error

Parse with jq:

# Get path (if successful)
giil "https://share.icloud.com/photos/XXX" --format json | jq -r 'if .ok then .path else .error.message end'

# Check success
giil "..." --format json | jq -e '.ok' && echo "Success" || echo "Failed"

TOON Mode: --format toon

Outputs the same metadata envelope as JSON, but encoded as TOON (Token-Optimized Object Notation). Requires the tru binary from toon_rust (set TOON_TRU_BIN or TOON_BIN if not on PATH).

giil "https://share.icloud.com/photos/XXX" --format toon

Album mode: --format toon emits one TOON document per image (separated by a blank line).

Base64 Mode: --base64

Outputs the image as a base64-encoded string (no file saved).

# Decode to file
giil "https://share.icloud.com/photos/XXX" --base64 | base64 -d > image.jpg

# Create data URI
echo "data:image/jpeg;base64,$(giil '...' --base64)" > uri.txt

# Pipe to API
giil "https://share.icloud.com/photos/XXX" --base64 | \
  curl -X POST -d @- https://api.example.com/upload

Combined with JSON:

giil "https://share.icloud.com/photos/XXX" --base64 --format json
{
  "base64": "/9j/4AAQSkZJRg...",
  "datetime": "2024-01-15T14:32:45.000Z",
  "method": "network"
}

URL-Only Mode: --print-url

Extracts and outputs the resolved CDN URL without downloading the image. Useful for debugging, external downloaders, or caching strategies.

giil "https://share.icloud.com/photos/XXX" --print-url
# stdout: https://cvws.icloud-content.com/B/...

Use cases:

  • Debugging: See what CDN URL giil would capture
  • External tools: Pass URL to curl, wget, or another downloader
  • Caching: Store URLs for later batch download
  • Inspection: Verify which CDN is serving the image

Example with curl:

CDN_URL=$(giil "https://share.icloud.com/photos/XXX" --print-url 2>/dev/null)
curl -o photo.jpg "$CDN_URL"

πŸ“š Album Mode

Download every photo from a shared iCloud album with --all.

giil "https://share.icloud.com/photos/XXX" --all --output ~/album

How Album Mode Works

1. Load album page
2. Detect thumbnail grid (11 selector strategies)
3. For each thumbnail:
   a. Click to open full-size view
   b. Wait for image to load
   c. Capture using 4-tier strategy
   d. Process and save with index suffix
   e. Close viewer (button or Escape key)
   f. Continue to next thumbnail
4. Output one path/JSON per photo

Album Output

Default output:

/path/to/album/icloud_20240115_143245_001.jpg
/path/to/album/icloud_20240115_143246_002.jpg
/path/to/album/icloud_20240115_143247_003.jpg

With --format json (or --json):

{"path": "...001.jpg", "method": "download", "width": 4032, ...}
{"path": "...002.jpg", "method": "network", "width": 3024, ...}
{"path": "...003.jpg", "method": "network", "width": 4032, ...}

With --format toon:

ok: true
path: ...001.jpg
method: download
width: 4032

ok: true
path: ...002.jpg
method: network
width: 3024

Album Mode Features

  • Resilient: Continues to next photo if one fails
  • Indexed filenames: _001, _002, etc. for ordering
  • Collision-free: Appends counter if filename exists
  • Progress feedback: Shows Photo 1/N... on stderr

Rate Limiting & Polite Downloading

Album mode implements respectful rate limiting to avoid overwhelming cloud services:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Photo 1    │────▢│   1 second  │────▢│  Photo 2    │────▢ ...
β”‚  Download   β”‚     β”‚    delay    β”‚     β”‚  Download   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Fixed delays:

  • 1 second between album photos β€” Prevents overwhelming iCloud servers
  • Applies after each successful or failed download

Exponential backoff:

  • If the server returns rate-limiting signals, giil backs off exponentially
  • Base multiplier of 2 (1s β†’ 2s β†’ 4s β†’ 8s β†’ ...)
  • Automatic retry with increasing delays

Why this matters:

  • Reduces risk of IP-based rate limiting or temporary bans
  • Prevents triggering anti-abuse measures
  • Allows iCloud to serve other users fairly
  • Improves overall reliability of large album downloads

πŸ”¬ How It Works

High-Level Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   User Input    │────▢│   Bash Wrapper   │────▢│  Node.js Core   β”‚
β”‚  (URL + flags)  β”‚     β”‚  (giil script)   β”‚     β”‚ (extractor.mjs) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚                         β”‚
                               β–Ό                         β–Ό
                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β”‚  Dependency  β”‚         β”‚  Playwright  β”‚
                        β”‚  Management  β”‚         β”‚  (Chromium)  β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                        β”‚
                               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                               β–Ό                        β–Ό                        β–Ό
                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β”‚   Network    β”‚         β”‚   Download   β”‚         β”‚  Screenshot  β”‚
                        β”‚ Interception β”‚         β”‚    Button    β”‚         β”‚   Fallback   β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚                        β”‚                        β”‚
                               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                        β–Ό
                                                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                 β”‚    Sharp     β”‚
                                                 β”‚  Processing  β”‚
                                                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                        β”‚
                                                        β–Ό
                                                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                 β”‚    Output    β”‚
                                                 β”‚ (file/json/  β”‚
                                                 β”‚   base64)    β”‚
                                                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Step-by-Step Process

  1. URL Normalization

    • Converts share.icloud.com/photos/XXX to www.icloud.com/photos/#XXX
    • Both formats load the same iCloud photo viewer
  2. Dependency Bootstrap

    • Checks for Node.js β‰₯18 (installs if missing)
    • Ensures Playwright + Chromium in cache
    • Generates extractor.mjs from embedded template
  3. Browser Launch

    • Spawns headless Chromium via Playwright
    • Sets realistic viewport (1920Γ—1080) and user-agent
    • Enables download interception
  4. Page Navigation

    • Loads iCloud URL with configurable timeout
    • Auto-dismisses cookie banners and overlays
    • Waits for network idle state
  5. Image Capture

    • Executes 4-tier fallback strategy (see below)
    • Selects highest-quality capture method that succeeds
  6. Image Processing

    • Extracts EXIF datetime for filename
    • Converts HEIC/HEIF if necessary
    • Compresses with MozJPEG (or --preserve to keep original bytes)
  7. Output Generation

    • Writes file to disk (or base64 to stdout)
    • Returns path/JSON on stdout

🌐 Browser Emulation

giil uses Playwright to drive a headless Chromium browser that appears indistinguishable from a real user's browser. This is essential for bypassing bot detection on cloud services.

Realistic Browser Configuration

// Browser context settings
{
    viewport: { width: 1920, height: 1080 },
    userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' +
               'AppleWebKit/537.36 (KHTML, like Gecko) ' +
               'Chrome/122.0.0.0 Safari/537.36'
}
Setting Value Purpose
Viewport 1920Γ—1080 Standard desktop resolution
User-Agent Chrome 122 on macOS Modern, common browser fingerprint
Headless true No visible window (server-compatible)

Automatic Overlay Dismissal

Cloud services often show cookie consent banners, subscription prompts, or other overlays that can block image capture. giil automatically dismisses these:

// Button texts that trigger auto-click
['Accept', 'Allow', 'OK', 'Continue', 'Not Now', 'Close', 'Dismiss', 'Got it']

How it works:

  1. After page load, giil scans for visible buttons with these labels
  2. Buttons are clicked in sequence with brief delays
  3. Continues silently if no overlays 802E are found

Network Idle Detection

giil waits for pages to fully stabilize before capturing:

await page.waitForLoadState('networkidle', { timeout: settleTimeout });

What "network idle" means:

  • No network requests for 500ms
  • All images, scripts, and assets loaded
  • Dynamic content has finished rendering

This ensures high-resolution images are fully loaded before capture attempts begin.


🎯 Capture Strategies in Detail

giil implements a four-tier fallback strategy to maximize reliability across different iCloud page states and configurations.

Strategy 1: Download Button (Highest Quality)

// Selectors tried in order:
'button[aria-label="Download"]'
'button[title="Download"]'
'a[aria-label="Download"]'
'[data-testid*="download"]'
'button:has-text("Download")'
'.download-button'
'[class*="download"]'

How it works:

  1. Locate visible Download button using selector cascade
  2. Click and wait for browser download event
  3. Save to temporary file, read into memory
  4. Clean up temp file after processing

Advantages:

  • Obtains original file (no re-encoding losses)
  • Works with HEIC/HEIF originals
  • Highest possible quality

When it fails:

  • Download button not visible or doesn't exist
  • Click doesn't trigger download event within 10s

Strategy 2: Network Interception (Full Resolution)

// CDN detection patterns:
url.includes('cvws.icloud-content.com') ||
url.includes('icloud-content.com') ||
url.includes('lh3.googleusercontent.com/pw/')  // Google Photos

// Content-type filtering:
'image/jpeg', 'image/png', 'image/heic', 'image/heif', 'image/webp'

How it works:

  1. Install response handler before page navigation
  2. Monitor all HTTP responses for CDN patterns
  3. Filter by content-type (image formats only)
  4. Capture image buffers, track largest (>10KB threshold)
  5. Use captured buffer if download button fails

The 10KB threshold:

  • Thumbnails and icons are typically <10KB
  • Full-resolution images are almost always >10KB
  • This prevents capturing preview images instead of originals

CDN selection algorithm:

for each HTTP response:
    if URL matches CDN pattern AND content-type is image:
        if buffer.size > currentLargest.size AND buffer.size > 10KB:
            currentLargest = buffer

Advantages:

  • Captures full-resolution CDN images
  • No screenshot quality loss
  • Works even if UI elements are obscured

When it fails:

  • CDN domain structure changes
  • Image loads before handler installed
  • All captured images below size threshold

Strategy 3: Element Screenshot

// Selectors tried in order:
'img[src*="cvws.icloud-content"]'
'img[src*="icloud-content"]'
'.photo-viewer img'
'.media-viewer img'
'[data-testid="photo"] img'
'main img'
'picture img'
'[role="img"]'

How it works:

  1. Query for image elements using selector cascade
  2. Verify element is visible and β‰₯100Γ—100 pixels
  3. Take PNG screenshot of the element
  4. Convert to JPEG during processing

Advantages:

  • Captures rendered image as displayed
  • Works when network capture misses

When it fails:

  • No matching visible image element
  • Element too small (<100px)

Strategy 4: Viewport Screenshot (Last Resort)

await page.screenshot({ type: 'png', fullPage: false });

How it works:

  1. Capture visible viewport (1920Γ—1080)
  2. Include entire visible area
  3. Convert to JPEG during processing

Advantages:

  • Always succeeds if page loads
  • Useful for debugging page state

Limitations:

  • May include UI chrome
  • Quality depends on viewport size
  • Not ideal for production use

Strategy Selection Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Start Capture                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                   β”‚ Try Download Button β”‚
                   β”‚    (9 selectors)    β”‚
                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚                   β”‚
                 Success              Fail
                    β”‚                   β”‚
                    β–Ό                   β–Ό
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚  Done!   β”‚    β”‚ Check CDN Capture   β”‚
              β”‚ (method: β”‚    β”‚   (buffer >10KB?)   β”‚
              β”‚ download)β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚
                              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                              β”‚                   β”‚
                           Success              Fail
                              β”‚                   β”‚
                              β–Ό                   β–Ό
                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β”‚  Done!   β”‚    β”‚ Try Element Screenshotβ”‚
                        β”‚ (method: β”‚    β”‚    (10 selectors)    β”‚
                        β”‚ network) β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚
                                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                        β”‚                   β”‚
                                     Success              Fail
                                        β”‚                   β”‚
                                        β–Ό                   β–Ό
                                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                  β”‚  Done!   β”‚    β”‚ Viewport Screenshot β”‚
                                  β”‚ (method: β”‚    β”‚   (always works)    β”‚
                                  β”‚ element- β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                  β”‚screenshotβ”‚              β”‚
                                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β–Ό
                                                      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                      β”‚  Done!   β”‚
                                                      β”‚ (method: β”‚
                                                      β”‚ viewport-β”‚
                                                      β”‚screenshotβ”‚
                                                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ”Œ Platform-Specific Optimizations

Each supported platform has custom handling for optimal results:

iCloud

  • 4-tier capture strategy as described above
  • Cookie banner auto-dismissal
  • Album detection and iteration
  • HEIC/HEIF format handling

Dropbox

Dropbox provides a fast path that bypasses Playwright entirely:

# URL transformation:
https://www.dropbox.com/s/abc123/photo.jpg?dl=0
β†’ https://www.dropbox.com/s/abc123/photo.jpg?raw=1
  • Direct curl download (no browser overhead)
  • Typically completes in 1-2 seconds
  • Full original quality preserved
  • Works with any Dropbox shared link format

Google Photos

Google Photos uses URL modifiers for resolution control:

Original CDN URL:
https://lh3.googleusercontent.com/pw/xxx=w1920-h1080

Full-resolution URL (giil applies =s0):
https://lh3.googleusercontent.com/pw/xxx=s0
  • =s0 modifier requests maximum resolution
  • Network interception captures all CDN responses
  • Collects unique base URLs for album mode
  • Automatic full-res download attempt

Google Drive

Multi-tier approach with authentication detection:

  1. Direct download URL (/uc?export=download)
  2. Thumbnail extraction (high-res sz=w4000)
  3. Screenshot fallback

Auth detection: If the file requires login, giil detects the redirect and returns a meaningful error (AUTH_REQUIRED) instead of capturing a login page.


πŸ–ΌοΈ Image Processing Pipeline

Processing Stages

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Raw       │────▢│    EXIF     │────▢│    HEIC     │────▢│   Sharp     β”‚
β”‚   Buffer    β”‚     β”‚  Datetime   β”‚     β”‚ Conversion  β”‚     β”‚   JPEG      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                                   β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”‚
                    β”‚   Output    │◀────│  Filename   β”‚β—€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚   Result    β”‚     β”‚ Generation  β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

EXIF Datetime Extraction

Using the exifr library, giil extracts datetime metadata to create meaningful filenames:

// Priority order:
1. DateTimeOriginal  // When photo was taken (most reliable)
2. CreateDate        // File creation time
3. DateTimeDigitized // When digitized
4. ModifyDate        // Last modification
5. Current time      // Fallback if no EXIF

HEIC/HEIF Conversion

Apple devices often produce HEIC images. giil handles this with platform-aware tools:

Platform Tool Notes
macOS sips Built-in, always available
Linux heif-convert Requires libheif-examples package
# Install HEIC support on Linux:
sudo apt-get install libheif-examples  # Debian/Ubuntu
sudo dnf install libheif-tools         # Fedora

MozJPEG Compression (Default)

By default, giil compresses images with MozJPEG for optimal size/quality balance:

# MozJPEG compression (default)
giil "https://share.icloud.com/photos/..."

# Preserve original bytes (skip compression)
giil "https://share.icloud.com/photos/..." --preserve

# Convert to WebP format
giil "https://share.icloud.com/photos/..." --convert webp

Sharp applies MozJPEG compression:

sharp(buffer).jpeg({
  quality: 85,           // Configurable via --quality
  mozjpeg: true,         // Enable MozJPEG optimizer
  chromaSubsampling: '4:2:0'  // Standard JPEG subsampling
})

Compression characteristics:

  • 40-50% smaller than standard JPEG at equivalent quality
  • 4:2:0 chroma subsampling reduces color data (imperceptible to human eye)
  • Quality 85 provides excellent visual quality with significant size reduction

Filename Generation

icloud_YYYYMMDD_HHMMSS[_NNN][_counter].jpg
        β”‚              β”‚      β”‚
        β”‚              β”‚      └── Collision counter (if file exists)
        β”‚              └── Album index (--all mode only)
        └── Date/time from EXIF or capture time

Examples:

icloud_20240115_143245.jpg          # Single photo
icloud_20240115_143245_001.jpg      # Album photo 1
icloud_20240115_143245_002.jpg      # Album photo 2
icloud_20240115_143245_001_1.jpg    # Collision (file existed)

πŸ” Download Verification

giil implements a multi-layer content validation system to ensure downloads are valid images, not error pages or corrupted data.

The Problem

Cloud services often return HTML error pages with 200 status codes, making it impossible to detect failures from HTTP status alone:

❌ Expected: JPEG image data
βœ“ Received: HTTP 200
❌ Actual content: <html><body>This link has expired</body></html>

Content Validation Pipeline

Every downloaded file passes through three validation checks before being accepted:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Content-Type   │────▢│  Magic Bytes    │────▢│  HTML Error     β”‚
β”‚   Validation    β”‚     β”‚   Detection     β”‚     β”‚   Detection     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚                       β”‚                       β”‚
        β–Ό                       β–Ό                       β–Ό
   Check MIME type         Verify file            Reject HTML
   matches image        signature bytes         error pages

Stage 1: Content-Type Validation

Validates the HTTP Content-Type header matches expected image types:

// Accepted MIME types:
'image/jpeg'
'image/png'
'image/webp'
'image/heic'
'image/heif'
'image/gif'
'application/octet-stream'  // Binary fallback (validated by magic bytes)

Stage 2: Magic Bytes Detection

Verifies the file's binary signature matches known image formats, regardless of what the server claimed:

Format Magic Bytes (hex) Description
JPEG FF D8 FF JFIF/Exif image
PNG 89 50 4E 47 Portable Network Graphics
GIF 47 49 46 38 Graphics Interchange Format
WebP 52 49 46 46...57 45 42 50 RIFF container with WEBP
HEIC/HEIF 00 00 00...66 74 79 70 ISO base media file (ftyp box)
BMP 42 4D Windows Bitmap

Why this matters: A server might claim Content-Type: image/jpeg but actually serve an HTML error page. Magic bytes catch this.

Stage 3: HTML Error Page Detection

Scans the first bytes of content for HTML signatures that indicate an error page was returned instead of an image:

// Rejected patterns:
'<!DOCTYPE'
'<!doctype'
'<html'
'<HTML'
'<head'
'<HEAD'

Edge case handling: Some valid images (especially JPEG) can contain embedded metadata that coincidentally matches HTML patterns. giil validates magic bytes first, so a valid JPEG with HTML-like EXIF comments passes validation.

HEIC-in-JPEG Detection

A special case: Apple devices sometimes wrap HEIC data inside a JPEG container. giil detects this by scanning for the ftypheic signature after the JPEG header and triggers HEIC conversion:

// Detect HEIC hidden in JPEG wrapper
if (startsWithJPEG && containsHeicSignature) {
    // Extract and convert the inner HEIC data
}

Validation in Practice

# giil validates automatically - no flags needed
giil "https://share.icloud.com/photos/XXX"

# If validation fails, you'll see:
# [giil] Error: Downloaded content is not a valid image
# [giil] Received HTML error page instead of image data

🧭 Design Principles

1. Self-Healing Dependencies

giil automatically detects and installs missing components:

User runs giil
      β”‚
      β”œβ”€β”€ Node.js missing?
      β”‚   └── Install via brew/apt/dnf/yum/pacman
      β”‚
      β”œβ”€β”€ Playwright missing?
      β”‚   └── npm install in cache directory
      β”‚
      β”œβ”€β”€ Chromium missing?
      β”‚   └── npx playwright install chromium
      β”‚
      └── All deps present β†’ Run extractor

2. Graceful Degradation

Every operation has fallbacks:

Component Primary Fallback
Image capture Download button Network β†’ Screenshot
HEIC conversion Sharp native System tools (sips/heif-convert)
EXIF datetime DateTimeOriginal Other fields β†’ Current time
Album navigation Close button Escape key
CLI output styling gum ANSI escape codes

3. Single-File Distribution

The entire Node.js extractor is embedded in the bash script as a heredoc:

create_extractor_script() {
    cat > "$script_path" << 'SCRIPT_EOF'
    // ~560 lines of JavaScript
    SCRIPT_EOF
}

Benefits:

  • No separate files to manage
  • Easy to inspect and audit
  • Simple installation (one file)
  • Regenerated fresh each run

4. XDG Compliance

giil respects the XDG Base Directory Specification:

CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
GIIL_HOME="${GIIL_HOME:-$CACHE_HOME/giil}"

5. Separation of Concerns

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Bash Layer                                                      β”‚
β”‚  β€’ CLI parsing and validation                                   β”‚
β”‚  β€’ Dependency detection and installation                        β”‚
β”‚  β€’ URL normalization                                            β”‚
β”‚  β€’ Process orchestration                                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Node.js Layer                                                   β”‚
β”‚  β€’ Browser automation (Playwright)                              β”‚
β”‚  β€’ Network interception                                         β”‚
β”‚  β€’ Image capture strategies                                     β”‚
β”‚  β€’ Image processing (Sharp)                                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ—οΈ Architecture

Component Overview

giil (bash wrapper, ~1,150 LOC)
β”œβ”€β”€ CLI argument parsing
β”œβ”€β”€ OS detection (macOS/Linux)
β”œβ”€β”€ Node.js installation
β”œβ”€β”€ Playwright setup
β”œβ”€β”€ gum installation (optional)
β”œβ”€β”€ URL normalization
β”œβ”€β”€ Extractor script generation
└── Node.js invocation

extractor.mjs (embedded JavaScript, ~1,450 LOC)
β”œβ”€β”€ Playwright browser management
β”œβ”€β”€ Response interception handler
β”œβ”€β”€ Download button detector
β”œβ”€β”€ Screenshot capture
β”œβ”€β”€ EXIF datetime extraction
β”œβ”€β”€ HEIC conversion
β”œβ”€β”€ Sharp image processing
β”œβ”€β”€ Filename generation
└── Output formatting

Dependency Graph

giil
 β”‚
 β”œβ”€β”€ Node.js β‰₯18
 β”‚    └── npm
 β”‚
 β”œβ”€β”€ Playwright ^1.40.0
 β”‚    └── Chromium (headless browser)
 β”‚
 β”œβ”€β”€ Sharp ^0.33.0
 β”‚    β”œβ”€β”€ libvips (native image library)
 β”‚    └── MozJPEG encoder
 β”‚
 β”œβ”€β”€ exifr ^7.1.3
 β”‚    └── EXIF/IPTC/XMP parser
 β”‚
 └── gum (optional)
      └── charmbracelet CLI styling

πŸ§ͺ Testing & Quality Assurance

giil includes a comprehensive testing infrastructure to ensure reliability.

Unit Test Framework

The test suite validates pure JavaScript functions extracted from the embedded extractor:

scripts/tests/
β”œβ”€β”€ run-tests.sh              # Test runner
β”œβ”€β”€ extract-functions.mjs     # Extracts pure functions from giil
β”œβ”€β”€ platform-detection.test.mjs    # Platform URL detection tests
└── (more test files...)

Running tests:

# Run all unit tests
./scripts/tests/run-tests.sh

# Output:
# === giil Unit Tests ===
# [1/2] Verifying function extraction...
#       Extraction OK
# [2/2] Running unit tests...
# βœ“ detectPlatform > iCloud URLs > detects share.icloud.com/photos URLs
# ...
# Done!

Test architecture:

  • Tests use Node.js 18+ native test runner (node:test)
  • Functions are extracted from giil at test time (no separate source files)
  • Each test file independently extracts functions in its before() hook

What's Tested

Test Suite Coverage
Platform Detection URL pattern matching for iCloud, Dropbox, Google Photos, Google Drive
Domain Boundary Checks Rejects fake domains (e.g., fakedropbox.com vs dropbox.com)
Case Insensitivity URL matching works regardless of case
Edge Cases Empty strings, malformed URLs, query parameters

CI Pipeline

GitHub Actions runs quality checks on every push:

# .github/workflows/ci.yml
jobs:
  unit-tests:
    - Setup Node.js 18
    - Run: ./scripts/tests/run-tests.sh

  shellcheck:
    - Lint giil and install.sh
    - Fail on warning-level issues

  syntax-validation:
    - Verify bash syntax
    - Verify embedded JS syntax

  installation-test:
    - Test curl-bash installer
    - Verify giil runs after install

Integration Tests

For testing against real iCloud links:

# Requires GIIL_REAL_TEST_URL environment variable
./scripts/real_link_test.sh

This test:

  1. Downloads from a real iCloud share link
  2. Verifies the file matches expected SHA256 checksum
  3. Validates image dimensions and format

Code Quality

ShellCheck enforces bash best practices:

shellcheck -x giil install.sh

Embedded JavaScript syntax validation:

# Extract and check JS syntax
node --check ~/.cache/giil/extractor.mjs

πŸ—‚οΈ File Locations

Runtime Cache (XDG-compliant)

Path Purpose
~/.cache/giil/ Runtime directory (or $XDG_CACHE_HOME/giil)
~/.cache/giil/node_modules/ Playwright, Sharp, exifr packages
~/.cache/giil/extractor.mjs Generated Node.js extraction script
~/.cache/giil/package.json npm package manifest
~/.cache/giil/.installed Installation marker file
~/.cache/giil/ms-playwright/ Chromium browser cache

Installation Location

Path Purpose
~/.local/bin/giil Main script (default install)
/usr/local/bin/giil System install (GIIL_SYSTEM=1)

Debug Artifacts

When using --debug, on failure:

File Contents
giil_debug_<timestamp>.png Full-page screenshot
giil_debug_<timestamp>.html Page DOM content

🏎️ Performance

Timing Breakdown

Phase First Run Subsequent Runs
Dependency check <1s <1s
Chromium download 30-60s Skipped (cached)
Browser launch 2-3s 2-3s
Page load 3-10s 3-10s
Image capture 1-5s 1-5s
Image processing <1s <1s
Total 40-80s 5-15s

Resource Usage

Resource Typical Usage
Memory (during run) 200-400 MB
Disk (Chromium cache) ~500 MB
Disk (node_modules) ~50 MB
Network (per image) Original image size

Optimization Tips

# Lower quality for faster processing and smaller files
giil "..." --quality 60

# Increase timeout for slow networks
giil "..." --timeout 120

# Force dependency update if issues occur
giil "..." --update

🧭 Troubleshooting

Common Issues

"Node.js not found"

Cause: Node.js not installed or not in PATH.

Fix: giil auto-installs Node.js, but you can also install manually:

# macOS
brew install node

# Ubuntu/Debian
sudo apt-get install nodejs npm

# Fedora
sudo dnf install nodejs npm
Timeout errors

Cause: Slow network or iCloud service issues.

Fixes:

  1. Increase timeout: giil "..." --timeout 120
  2. Check if URL works in browser
  3. Try again later (iCloud may be slow)
"Failed to capture image"

Cause: All capture strategies failed.

Fixes:

  1. Run with --debug to get screenshot and HTML
  2. Check debug screenshot to see page state
  3. Open GitHub issue with debug artifacts
Small/wrong image captured

Cause: Captured thumbnail instead of full resolution.

Fixes:

  1. Should auto-select largest image
  2. Try with --debug to investigate
  3. Report if persistent (include URL)
HEIC conversion fails on Linux

Cause: heif-convert not installed.

Fix:

# Ubuntu/Debian
sudo apt-get install libheif-examples

# Fedora
sudo dnf install libheif-tools

# Arch
sudo pacman -S libheif
Chromium fails to launch

Cause: Missing system dependencies (common on headless servers).

Fix:

# Force reinstall with system deps
giil "..." --update

# Or manually:
cd ~/.cache/giil && npx playwright install --with-deps chromium

Debug Mode

Use --debug to capture diagnostic information:

giil "https://share.icloud.com/photos/XXX" --debug

On failure, this saves:

  • giil_debug_<timestamp>.png - Screenshot of page state
  • giil_debug_<timestamp>.html - Full DOM for inspection

Verbose and Trace Modes

For deeper debugging:

# Verbose: detailed progress logging
giil "..." --verbose

# Trace: Playwright trace recording (generates trace.zip)
giil "..." --trace

# View trace in browser
npx playwright show-trace ~/.cache/giil/trace.zip

πŸ”’ Exit Codes

giil uses semantic exit codes for scripting and error handling:

Code Name Description
0 Success Image captured and saved/output
1 Capture Failure All capture strategies failed
2 Usage Error Invalid arguments or missing URL
3 Dependency Error Node.js, Playwright, or Chromium issue
10 Network Error Timeout, DNS failure, unreachable host
11 Auth Required Login redirect, password required, not publicly shared
12 Not Found Expired link, deleted file, 404
13 Unsupported Type Video, Google Doc, or non-image content
20 Internal Error Bug in giil (please report!)

Scripting with exit codes:

giil "https://share.icloud.com/photos/XXX" 2>/dev/null
case $? in
    0) echo "Success!" ;;
    10) echo "Network issue - retry later" ;;
    11) echo "Link not public - ask owner to share" ;;
    12) echo "Link expired" ;;
    *) echo "Failed with code $?" ;;
esac

Intelligent Error Classification

giil analyzes error messages to provide meaningful exit codes and remediation hints. This happens automaticallyβ€”no configuration needed.

Error message pattern matching:

Error Pattern Classified As Exit Code
timeout, net::err, dns Network Error 10
404, not found, expired Not Found 12
login, auth, password Auth Required 11
video, unsupported Unsupported Type 13
HTML content with image content-type Auth Required 11
HTML magic bytes in response Auth Required 11

Why this matters:

Cloud services often return HTTP 200 with an HTML login page when authentication is required. giil detects this through content validation (magic bytes, HTML detection) and correctly reports it as AUTH_REQUIRED rather than a generic capture failure.

JSON error responses include remediation hints:

{
  "ok": false,
  "error": {
    "code": "AUTH_REQUIRED",
    "message": "Redirect to login page detected",
    "remediation": "The file is not publicly shared. The owner must enable public access."
  }
}

🎨 Terminal Styling

giil integrates with gum for beautiful terminal output when available:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                                                                   β”‚
β”‚   ╔════════════════════════════════════════════════════════════╗ β”‚
β”‚   β•‘                           giil                              β•‘ β”‚
β”‚   β•‘          Get Image [from] Internet Link v3.0.0              β•‘ β”‚
β”‚   β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• β”‚
β”‚                                                                   β”‚
β”‚   ◐ Launching browser...                                          β”‚
β”‚   βœ“ Download button clicked                                       β”‚
β”‚   βœ“ Image processed: 4032Γ—3024, 1.2 MB β†’ 456 KB                  β”‚
β”‚                                                                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Styling Behavior

Environment Output Style
TTY with gum installed Full gum styling (banners, spinners, colors)
TTY without gum ANSI color codes
Non-TTY (piped) Plain text
CI environment ($CI set) Plain text, no gum
GIIL_NO_GUM=1 Force ANSI fallback

Install gum (Optional)

# macOS
brew install gum

# Linux (apt)
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg
echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list
sudo apt update && sudo apt install gum

# Arch
sudo pacman -S gum

🌐 Environment Variables

Runtime Variables

Variable Description Default
XDG_CACHE_HOME Base cache directory ~/.cache
GIIL_HOME giil runtime directory $XDG_CACHE_HOME/giil
PLAYWRIGHT_BROWSERS_PATH Custom Chromium cache $GIIL_HOME/ms-playwright
GIIL_NO_GUM Disable gum installation unset
GIIL_CHECK_UPDATES Enable update checking (set to 1) unset
GIIL_OUTPUT_FORMAT Structured output override (json or toon) unset
NODE_OPTIONS Node.js options unset
CI Detected CI environment (disables gum) unset
TOON_DEFAULT_FORMAT Global TOON default (json or toon) unset
TOON_TRU_BIN Path to the tru binary (preferred) unset
TOON_BIN Alternate path to the tru binary unset
TOON_STATS Emit token stats on stderr (set to 1) unset

Installer Variables

Variable Description Default
DEST Custom install directory ~/.local/bin
GIIL_SYSTEM Install to /usr/local/bin (set to 1) unset
GIIL_NO_ALIAS Skip adding directory to PATH unset
GIIL_VERIFY Verify SHA256 checksum (set to 1) unset
GIIL_VERSION Install specific version latest

Example: Custom cache location

export XDG_CACHE_HOME=/var/cache/myapp
giil "https://share.icloud.com/photos/XXX"
# Uses /var/cache/myapp/giil/

Example: Enable update checking

export GIIL_CHECK_UPDATES=1
giil "https://share.icloud.com/photos/XXX"
# Will notify if a newer version is available (once per day)

Example: Verified installation

GIIL_VERIFY=1 curl -fsSL .../install.sh | bash
# Verifies SHA256 checksum against GitHub release

πŸ“¦ Dependencies

Automatically Installed

Package Version Purpose
Node.js β‰₯18 JavaScript runtime
Playwright ^1.40.0 Browser automation
Chromium (via Playwright) Headless browser
Sharp ^0.33.0 Image processing
exifr ^7.1.3 EXIF metadata parsing
gum latest CLI styling (optional)

System Requirements

Platform Requirements
macOS macOS 10.15+ (Catalina or later)
Linux glibc 2.17+ (Ubuntu 18.04+, Debian 10+)
Node.js v18 or later

πŸ›‘οΈ Security & Privacy

Privacy Guarantees

  • Local execution: All processing happens on your machine
  • No telemetry: No data sent anywhere except to iCloud
  • No authentication stored: Uses iCloud's public share mechanism
  • No cookies saved: Browser context is ephemeral

Security Considerations

  • Sandboxed browser: Chromium runs with --no-sandbox for compatibility but in headless mode with no persistent state
  • No code execution: Only loads iCloud URLs, no JavaScript injection
  • Temp file cleanup: Downloaded files cleaned up after processing

Audit

The entire codebase is contained in a single bash script (~1,150 lines of bash wrapper) with an embedded JavaScript extractor (~1,450 lines):

less ~/.local/bin/giil

πŸ”§ Uninstallation

# Remove script
rm ~/.local/bin/giil

# Remove runtime and cache
rm -rf ~/.cache/giil

# Remove Playwright browsers (if no other Playwright tools)
rm -rf ~/.cache/ms-playwright

🀝 Contributing

About Contributions: Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via gh and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.


πŸ“„ License

MIT License (with OpenAI/Anthropic Rider). See LICENSE for details.


Report Bug Β· Request Feature


Built with Playwright, Sharp, and a healthy disregard for iCloud's lack of an API.

About

Zero-setup bash CLI that downloads full-resolution images from iCloud/Dropbox/Google Photos share links, bridging iPhone screenshots to remote AI coding sessions

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 
0