From 2b1aa2f872b69bfebe4fa91180c61fefb6f2289e Mon Sep 17 00:00:00 2001 From: Jeffrey Aven Date: Wed, 24 Jun 2026 09:40:51 +1000 Subject: [PATCH 1/8] Move get-app from Deno Deploy to Cloudflare Worker Port the get-stackql-deploy.io platform-detect/redirect service from a Deno Deploy app to a Cloudflare Worker. The handler logic is unchanged - all three behaviours (UA-based release redirect, /install.sh installer, fallback redirect) use Web-standard APIs, so only the runtime entrypoint changed from Deno.serve to a Module Worker fetch handler. - src/index.ts: ported handler - wrangler.toml: custom_domain route for the apex (Wrangler manages DNS) - package.json, tsconfig.json, .gitignore: worker tooling - README.md: dev, deploy, and DNS cutover instructions Co-Authored-By: Claude Opus 4.8 (1M context) --- get-app/.gitignore | 3 + get-app/README.md | 97 +++++++++++++++++++++++++++++++ get-app/deno.json | 5 -- get-app/package.json | 16 +++++ get-app/{main.ts => src/index.ts} | 32 +++++----- get-app/tsconfig.json | 13 +++++ get-app/wrangler.toml | 11 ++++ 7 files changed, 157 insertions(+), 20 deletions(-) create mode 100644 get-app/.gitignore create mode 100644 get-app/README.md delete mode 100644 get-app/deno.json create mode 100644 get-app/package.json rename get-app/{main.ts => src/index.ts} (76%) create mode 100644 get-app/tsconfig.json create mode 100644 get-app/wrangler.toml diff --git a/get-app/.gitignore b/get-app/.gitignore new file mode 100644 index 0000000..a933f10 --- /dev/null +++ b/get-app/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.wrangler/ +.dev.vars diff --git a/get-app/README.md b/get-app/README.md new file mode 100644 index 0000000..dcddbeb --- /dev/null +++ b/get-app/README.md @@ -0,0 +1,97 @@ +# get-stackql-deploy.io + +Cloudflare Worker that backs `https://get-stackql-deploy.io`. It detects the +calling platform and points the caller at the correct `stackql-deploy` release +asset on GitHub. + +Behaviour (unchanged from the previous Deno Deploy app): + +- `GET /` - reads the `User-Agent` and `302`-redirects to the matching release + asset (`windows-x86_64.zip`, `macos-universal.tar.gz`, or + `linux-x86_64.tar.gz`). Keeps the + `curl -L https://get-stackql-deploy.io | tar xzf -` one-liner working. +- `GET /install.sh` (and `/install`) - returns a POSIX `sh` installer that runs + `uname` client-side to pick the right OS + arch asset. This is what CLI users + (curl/wget) hit, since their User-Agent carries no OS. +- Any other path - `301`-redirects to `https://stackql-deploy.io`. + +## Develop + +```sh +npm install +npm run dev # wrangler dev - serves on http://localhost:8787 +``` + +Test locally: + +```sh +curl -A "curl/8.4.0" http://localhost:8787/install.sh +curl -sI -A "Mozilla/5.0 (Macintosh)" http://localhost:8787/ # -> 302 to macos asset +curl -sI -A "curl/8.4.0" http://localhost:8787/ # -> 302 to linux asset +``` + +## Deploy + +One-time auth (uses your Cloudflare login): + +```sh +npx wrangler login +``` + +Deploy: + +```sh +npm run deploy # wrangler deploy +``` + +`wrangler.toml` uses a `custom_domain` route for `get-stackql-deploy.io`. On the +first deploy Wrangler creates and manages the proxied DNS record for the apex +automatically - no manual DNS entry required. The `get-stackql-deploy.io` zone +must already exist in the target Cloudflare account. + +Tail live logs: + +```sh +npm run tail +``` + +## Cutover from Deno Deploy + +The zone is already on Cloudflare, so cutover is just pointing the apex at the +Worker instead of Deno Deploy. + +1. Authenticate and deploy the Worker: + + ```sh + npm install + npx wrangler login + npm run deploy + ``` + + Confirm the build output reports the route + `get-stackql-deploy.io (custom domain)`. + +2. In the Cloudflare dashboard for the `get-stackql-deploy.io` zone, check DNS: + - The previous setup pointed the apex at Deno Deploy (a `CNAME` to + `.deno.dev`, or `A`/`AAAA` records). Wrangler's custom-domain route + adds its own managed record for the Worker. If a stale Deno record remains + and blocks the custom domain from attaching, remove the old Deno + `CNAME`/`A`/`AAAA` record for the apex, then re-run `npm run deploy`. + +3. Verify the live site once DNS propagates (usually seconds on Cloudflare): + + ```sh + curl -sI -A "curl/8.4.0" https://get-stackql-deploy.io/ | grep -i location + curl -fsSL https://get-stackql-deploy.io/install.sh | head -5 + curl -L https://get-stackql-deploy.io | tar tzf - | head # full one-liner + ``` + + Confirm responses are served by Cloudflare (response header + `server: cloudflare`) and not Deno Deploy. + +4. Decommission the Deno Deploy project once verified: delete or pause it in the + Deno Deploy dashboard so it no longer bills or risks serving stale content. + The previous Deno source lives in git history if you ever need it. + +No consumers need changing - `get-stackql-deploy.io`, `/install.sh`, and the +`curl | tar` one-liner all keep the same URLs and behaviour. diff --git a/get-app/deno.json b/get-app/deno.json deleted file mode 100644 index a6267e6..0000000 --- a/get-app/deno.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "tasks": { - "dev": "deno run --allow-net main.ts" - } -} diff --git a/get-app/package.json b/get-app/package.json new file mode 100644 index 0000000..1190e29 --- /dev/null +++ b/get-app/package.json @@ -0,0 +1,16 @@ +{ + "name": "get-stackql-deploy", + "version": "1.0.0", + "private": true, + "description": "Cloudflare Worker behind get-stackql-deploy.io that redirects callers to the right stackql-deploy release asset.", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "tail": "wrangler tail" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250601.0", + "typescript": "^5.5.0", + "wrangler": "^4.20.0" + } +} diff --git a/get-app/main.ts b/get-app/src/index.ts similarity index 76% rename from get-app/main.ts rename to get-app/src/index.ts index 2a6fba5..1f26ce7 100644 --- a/get-app/main.ts +++ b/get-app/src/index.ts @@ -54,21 +54,23 @@ chmod +x stackql-deploy echo "Installed ./stackql-deploy ($os/$arch). Run it with ./stackql-deploy or move it onto your PATH." `; -Deno.serve((req: Request) => { - const url = new URL(req.url); +export default { + fetch(req: Request): Response { + const url = new URL(req.url); - // Dedicated installer endpoint for curl / wget (OS + arch detected client-side). - if (url.pathname === "/install.sh" || url.pathname === "/install") { - return new Response(INSTALL_SCRIPT, { - headers: { "content-type": "text/x-shellscript; charset=utf-8" }, - }); - } + // Dedicated installer endpoint for curl / wget (OS + arch detected client-side). + if (url.pathname === "/install.sh" || url.pathname === "/install") { + return new Response(INSTALL_SCRIPT, { + headers: { "content-type": "text/x-shellscript; charset=utf-8" }, + }); + } - if (url.pathname !== "/") { - return Response.redirect("https://stackql-deploy.io", 301); - } + if (url.pathname !== "/") { + return Response.redirect("https://stackql-deploy.io", 301); + } - const ua = req.headers.get("user-agent") ?? ""; - const asset = getAssetName(ua); - return Response.redirect(`${RELEASE_BASE}/${asset}`, 302); -}); + const ua = req.headers.get("user-agent") ?? ""; + const asset = getAssetName(ua); + return Response.redirect(`${RELEASE_BASE}/${asset}`, 302); + }, +} satisfies ExportedHandler; diff --git a/get-app/tsconfig.json b/get-app/tsconfig.json new file mode 100644 index 0000000..091a334 --- /dev/null +++ b/get-app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "types": ["@cloudflare/workers-types"], + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} diff --git a/get-app/wrangler.toml b/get-app/wrangler.toml new file mode 100644 index 0000000..d4ead8d --- /dev/null +++ b/get-app/wrangler.toml @@ -0,0 +1,11 @@ +name = "get-stackql-deploy" +main = "src/index.ts" +compatibility_date = "2025-06-01" +workers_dev = false + +# Serve the worker on the zone apex. `custom_domain = true` makes Wrangler create +# and manage the proxied DNS record for get-stackql-deploy.io automatically, so no +# manual DNS entry is needed. The zone must already exist in the Cloudflare account. +routes = [ + { pattern = "get-stackql-deploy.io", custom_domain = true } +] From 798c0f839be8e0a3de41bfd7cba057f812a8785e Mon Sep 17 00:00:00 2001 From: Jeffrey Aven Date: Wed, 24 Jun 2026 09:42:47 +1000 Subject: [PATCH 2/8] Add package-lock.json for get-app worker Pins the dependency tree for reproducible installs. Co-Authored-By: Claude Opus 4.8 (1M context) --- get-app/package-lock.json | 1528 +++++++++++++++++++++++++++++++++++++ 1 file changed, 1528 insertions(+) create mode 100644 get-app/package-lock.json diff --git a/get-app/package-lock.json b/get-app/package-lock.json new file mode 100644 index 0000000..5e5226e --- /dev/null +++ b/get-app/package-lock.json @@ -0,0 +1,1528 @@ +{ + "name": "get-stackql-deploy", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "get-stackql-deploy", + "version": "1.0.0", + "devDependencies": { + "@cloudflare/workers-types": "^4.20250601.0", + "typescript": "^5.5.0", + "wrangler": "^4.20.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", + "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260623.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260623.1.tgz", + "integrity": "sha512-MvDoIsRTsUJRzAl1/4hDXL839piyyjCeYatBHWgMc12Go7nHxkgbRih+1GJImEiKACSentu410bOupcutqFbpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260623.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260623.1.tgz", + "integrity": "sha512-sNqHQvHPMOj5/BJOadtEZekRPSG5qQ0/ulC30ZRHRLnmx6tj5O4Wb3Nf0oznnI0pmjXhbv6b7+TOpDkaFMjbBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260623.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260623.1.tgz", + "integrity": "sha512-XYTWqTlZlCXqG+Po6awjXtlxw73hb3C39B/PP0sb4H9NI3V0eynq8Q7rXNe7DHJs2pWRfDJihQzpayQvpwf5wQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260623.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260623.1.tgz", + "integrity": "sha512-PC5UDKA8oQB3Gek/Y+ysovdHNjp55CihOQZd7F9xPwpkv9qTBB0mhyHnfoG2YHtW1bb9CNhuwiThaNxegpE4mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260623.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260623.1.tgz", + "integrity": "sha512-OTHLCVYyN0pEfrajpjjnrGg5zA1GDnpNYmMz3x2ESFtH/oXRODsUQBllP7oJpJvMURF3rXSYwAhMojaftGry8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260623.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260623.1.tgz", + "integrity": "sha512-J/0POl0HeLepbwDE5Yx5c7jQrHFkvCEFu3TS+TQsDDlg/vTs5og7wdGP6eNGXOAntgWUrjcvvKTmVLTP7OrnAg==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.17.tgz", + "integrity": "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/miniflare": { + "version": "4.20260623.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260623.0.tgz", + "integrity": "sha512-p2YTeH01jMiMOjO4v9hb/+GJndja5LCxecGOWCaT9F414PRXgdddLDsK6MnqhGBB5tlU/WoBYIG1XZte5pQzOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.34.5", + "undici": "7.28.0", + "workerd": "1.20260623.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/workerd": { + "version": "1.20260623.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260623.1.tgz", + "integrity": "sha512-9SJsdTSsehhqc26TUJIzyi1XgyYeqFym4hinZnWoAP1BkhEoMQ5Ygz7Xw9T+2ecU+y409JBEScBgWTdZ06mBrg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260623.1", + "@cloudflare/workerd-darwin-arm64": "1.20260623.1", + "@cloudflare/workerd-linux-64": "1.20260623.1", + "@cloudflare/workerd-linux-arm64": "1.20260623.1", + "@cloudflare/workerd-windows-64": "1.20260623.1" + } + }, + "node_modules/wrangler": { + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.104.0.tgz", + "integrity": "sha512-xCfbg2Oj93Bc7EMryFaSeRGDgV96dzrWoaK5q2q5XLEvumO4mysNP/1MDue0GUozEJAI6Z6vrGyYPLmfET/0sg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.1", + "blake3-wasm": "2.1.5", + "esbuild": "0.28.1", + "miniflare": "4.20260623.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260623.1" + }, + "bin": { + "cf-wrangler": "bin/cf-wrangler.js", + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20260623.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + } + } +} From 06150b5d2346c113218cb447fcdc42a36fb39f62 Mon Sep 17 00:00:00 2001 From: Jeffrey Aven Date: Wed, 24 Jun 2026 09:48:51 +1000 Subject: [PATCH 3/8] Document apex DNS 409 conflict in get-app cutover steps The first wrangler deploy fails the custom-domain trigger with a 409 because the old Deno apex record is still present. Document the delete-and-redeploy fix and the dashboard override alternative. Co-Authored-By: Claude Opus 4.8 (1M context) --- get-app/README.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/get-app/README.md b/get-app/README.md index dcddbeb..73dd511 100644 --- a/get-app/README.md +++ b/get-app/README.md @@ -71,12 +71,23 @@ Worker instead of Deno Deploy. Confirm the build output reports the route `get-stackql-deploy.io (custom domain)`. -2. In the Cloudflare dashboard for the `get-stackql-deploy.io` zone, check DNS: - - The previous setup pointed the apex at Deno Deploy (a `CNAME` to - `.deno.dev`, or `A`/`AAAA` records). Wrangler's custom-domain route - adds its own managed record for the Worker. If a stale Deno record remains - and blocks the custom domain from attaching, remove the old Deno - `CNAME`/`A`/`AAAA` record for the apex, then re-run `npm run deploy`. +2. Resolve the apex DNS conflict. On the first deploy the Worker script uploads + fine but the custom-domain trigger fails with a `409 Conflict` on + `.../domains/records` and the output reads `No targets deployed` - because the + apex still holds the old Deno Deploy record. To clear it: + - Cloudflare dashboard -> `get-stackql-deploy.io` zone -> DNS -> Records. + - Delete the old Deno record on the apex (name `get-stackql-deploy.io` / `@`) - + a `CNAME` to `.deno.dev`, or `A`/`AAAA` records. Note it first if you + want a rollback path. + - Re-run `npm run deploy`. Wrangler now creates its own managed proxied record + and attaches the custom domain; the output should report + `get-stackql-deploy.io (custom domain)`. + + Deleting the apex record briefly takes the hostname offline until the redeploy + attaches the Worker (seconds on Cloudflare). To avoid any gap, instead use + Workers & Pages -> get-stackql-deploy -> Settings -> Domains & Routes -> Add -> + Custom Domain -> `get-stackql-deploy.io`, which prompts to override the existing + record in a single step. 3. Verify the live site once DNS propagates (usually seconds on Cloudflare): From aa6d1aed628dd8330362475796e4574e396ee896 Mon Sep 17 00:00:00 2001 From: Jeffrey Aven Date: Wed, 24 Jun 2026 10:16:45 +1000 Subject: [PATCH 4/8] Add /install.ps1 PowerShell installer endpoint Serve a PowerShell installer from the worker so Windows users get a one-liner (irm https://get-stackql-deploy.io/install.ps1 | iex) matching the curl | sh flow on Linux/macOS. It downloads and expands the Windows zip into the current directory. Update the worker README and getting-started docs. Co-Authored-By: Claude Opus 4.8 (1M context) --- get-app/README.md | 4 ++++ get-app/src/index.ts | 34 +++++++++++++++++++++++++++++++++ website/docs/getting-started.md | 14 ++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/get-app/README.md b/get-app/README.md index 73dd511..e674b03 100644 --- a/get-app/README.md +++ b/get-app/README.md @@ -13,6 +13,9 @@ Behaviour (unchanged from the previous Deno Deploy app): - `GET /install.sh` (and `/install`) - returns a POSIX `sh` installer that runs `uname` client-side to pick the right OS + arch asset. This is what CLI users (curl/wget) hit, since their User-Agent carries no OS. +- `GET /install.ps1` - returns a PowerShell installer that downloads and expands + the Windows zip into the current directory. Run it with + `irm https://get-stackql-deploy.io/install.ps1 | iex`. - Any other path - `301`-redirects to `https://stackql-deploy.io`. ## Develop @@ -26,6 +29,7 @@ Test locally: ```sh curl -A "curl/8.4.0" http://localhost:8787/install.sh +curl http://localhost:8787/install.ps1 curl -sI -A "Mozilla/5.0 (Macintosh)" http://localhost:8787/ # -> 302 to macos asset curl -sI -A "curl/8.4.0" http://localhost:8787/ # -> 302 to linux asset ``` diff --git a/get-app/src/index.ts b/get-app/src/index.ts index 1f26ce7..29af4ac 100644 --- a/get-app/src/index.ts +++ b/get-app/src/index.ts @@ -54,6 +54,33 @@ chmod +x stackql-deploy echo "Installed ./stackql-deploy ($os/$arch). Run it with ./stackql-deploy or move it onto your PATH." `; +// PowerShell equivalent of the installer for Windows callers, so the awkward +// Invoke-WebRequest + Expand-Archive dance collapses to: +// irm https://get-stackql-deploy.io/install.ps1 | iex +// Windows ships a single x86_64 build; ARM64 Windows runs it under emulation, so +// both architectures resolve to the same asset. +const INSTALL_PS1 = `#Requires -Version 5 +# stackql-deploy installer - https://get-stackql-deploy.io/install.ps1 +# Downloads the latest Windows release binary into the current directory. +# Usage: irm https://get-stackql-deploy.io/install.ps1 | iex +$ErrorActionPreference = 'Stop' + +$base = "${RELEASE_BASE}" +$asset = 'stackql-deploy-windows-x86_64.zip' +$arch = $env:PROCESSOR_ARCHITECTURE +if ($arch -ne 'AMD64' -and $arch -ne 'ARM64') { + throw "stackql-deploy: unsupported Windows architecture: $arch" +} + +$dest = (Get-Location).Path +$zip = Join-Path $dest $asset +Write-Host "Downloading $asset ..." +Invoke-WebRequest -Uri "$base/$asset" -OutFile $zip +Expand-Archive -Path $zip -DestinationPath $dest -Force +Remove-Item $zip +Write-Host "Installed .\\stackql-deploy.exe. Run it with .\\stackql-deploy.exe or move it onto your PATH." +`; + export default { fetch(req: Request): Response { const url = new URL(req.url); @@ -65,6 +92,13 @@ export default { }); } + // PowerShell installer for Windows: irm https://get-stackql-deploy.io/install.ps1 | iex + if (url.pathname === "/install.ps1") { + return new Response(INSTALL_PS1, { + headers: { "content-type": "text/plain; charset=utf-8" }, + }); + } + if (url.pathname !== "/") { return Response.redirect("https://stackql-deploy.io", 301); } diff --git a/website/docs/getting-started.md b/website/docs/getting-started.md index d698579..cc6032f 100644 --- a/website/docs/getting-started.md +++ b/website/docs/getting-started.md @@ -57,6 +57,20 @@ sudo mv stackql-deploy /usr/local/bin/ **PowerShell:** +The install script downloads the latest release and extracts `stackql-deploy.exe` into the current directory: + +```powershell +irm https://get-stackql-deploy.io/install.ps1 | iex +``` + +Then move it onto your PATH: + +```powershell +Move-Item stackql-deploy.exe "$env:LOCALAPPDATA\Microsoft\WindowsApps\" +``` + +Or do it manually: + ```powershell Invoke-WebRequest https://get-stackql-deploy.io -OutFile stackql-deploy.zip Expand-Archive stackql-deploy.zip -DestinationPath . From d2709935a5b407d09eb93330726e31fa64e2d3f5 Mon Sep 17 00:00:00 2001 From: Jeffrey Aven Date: Wed, 24 Jun 2026 10:30:17 +1000 Subject: [PATCH 5/8] Add /install auto-detect and wrong-shell guards to installers - /install now detects the calling shell from the User-Agent and serves the sh installer for curl/wget or the PowerShell installer for irm/iwr. /install.sh and /install.ps1 remain as explicit verbose options. - Fetching install.sh from PowerShell (or install.ps1 from curl/wget) returns a short message in the right shell's language pointing at the correct command, instead of a cascade of interpreter errors. - install.sh detects POSIX-shell-on-Windows (MINGW/MSYS/CYGWIN) and install.ps1 detects PowerShell-on-non-Windows, each pointing at the other installer. - Replace "unsupported OS/arch" wording with friendly "no prebuilt binary for your CPU/system" messages linking the releases and docs pages. Co-Authored-By: Claude Opus 4.8 (1M context) --- get-app/README.md | 37 +++++++++--- get-app/src/index.ts | 138 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 139 insertions(+), 36 deletions(-) diff --git a/get-app/README.md b/get-app/README.md index e674b03..e64f7f4 100644 --- a/get-app/README.md +++ b/get-app/README.md @@ -10,14 +10,33 @@ Behaviour (unchanged from the previous Deno Deploy app): asset (`windows-x86_64.zip`, `macos-universal.tar.gz`, or `linux-x86_64.tar.gz`). Keeps the `curl -L https://get-stackql-deploy.io | tar xzf -` one-liner working. -- `GET /install.sh` (and `/install`) - returns a POSIX `sh` installer that runs - `uname` client-side to pick the right OS + arch asset. This is what CLI users - (curl/wget) hit, since their User-Agent carries no OS. -- `GET /install.ps1` - returns a PowerShell installer that downloads and expands - the Windows zip into the current directory. Run it with - `irm https://get-stackql-deploy.io/install.ps1 | iex`. +- `GET /install` - universal installer. Detects the calling shell from the + User-Agent and serves the matching script: the POSIX `sh` installer for + curl/wget, or the PowerShell installer for `irm`/`iwr`. Use it as + `curl -fsSL https://get-stackql-deploy.io/install | sh` (Linux/macOS) or + `irm https://get-stackql-deploy.io/install | iex` (Windows). +- `GET /install.sh` - always the POSIX `sh` installer. Runs `uname` client-side + to pick the right OS + arch asset. +- `GET /install.ps1` - always the PowerShell installer. Downloads and expands the + Windows zip into the current directory. - Any other path - `301`-redirects to `https://stackql-deploy.io`. +### Wrong-shell guards + +The installers give friendly guidance instead of cryptic interpreter errors when +run in the wrong shell: + +- Fetch `/install.sh` with PowerShell -> served a short PowerShell message + pointing at `irm .../install.ps1 | iex`. +- Fetch `/install.ps1` with curl/wget -> served a short `sh` message pointing at + `curl -fsSL .../install.sh | sh`. +- `install.sh` run in a POSIX shell on Windows (Git Bash/MSYS - `uname` reports + `MINGW*`/`MSYS*`/`CYGWIN*`) -> message pointing at the PowerShell command. +- `install.ps1` run under PowerShell on macOS/Linux (`$PSVersionTable.Platform` + is `Unix`) -> message pointing at the `sh` command. +- Unsupported CPU architectures get a "no prebuilt binary for your CPU" message + with a link to the releases page, not "unsupported". + ## Develop ```sh @@ -28,8 +47,10 @@ npm run dev # wrangler dev - serves on http://localhost:8787 Test locally: ```sh -curl -A "curl/8.4.0" http://localhost:8787/install.sh -curl http://localhost:8787/install.ps1 +curl -A "curl/8.4.0" http://localhost:8787/install # -> sh installer +curl -A "WindowsPowerShell/5.1" http://localhost:8787/install # -> PowerShell installer +curl -A "WindowsPowerShell/5.1" http://localhost:8787/install.sh # -> "use install.ps1" guide +curl -A "curl/8.4.0" http://localhost:8787/install.ps1 # -> "use install.sh" guide curl -sI -A "Mozilla/5.0 (Macintosh)" http://localhost:8787/ # -> 302 to macos asset curl -sI -A "curl/8.4.0" http://localhost:8787/ # -> 302 to linux asset ``` diff --git a/get-app/src/index.ts b/get-app/src/index.ts index 29af4ac..ae89fd3 100644 --- a/get-app/src/index.ts +++ b/get-app/src/index.ts @@ -1,18 +1,28 @@ const GITHUB_REPO = "stackql/stackql-deploy-rs"; const RELEASE_BASE = `https://github.com/${GITHUB_REPO}/releases/latest/download`; +const RELEASES_URL = `https://github.com/${GITHUB_REPO}/releases/latest`; +const DOCS_URL = "https://stackql.io/docs/installing-stackql"; // Browser / known-OS callers send a User-Agent that identifies the platform, so // the root URL can redirect them straight to the correct release asset. CLI -// download tools (curl, wget) send a UA like "curl/8.4.0" with NO operating -// system in it, so we cannot pick a binary for them from the UA. For those we -// expose a dedicated installer at /install.sh that detects OS + arch locally -// with `uname` and downloads the right asset. This is what fixes "exec format -// error" on macOS - the bare root URL used to fall through to the linux x86_64 -// asset for every curl caller regardless of the real platform. +// download tools (curl, wget, PowerShell) don't carry the OS reliably, so we +// expose installer endpoints that detect OS + arch locally instead: +// +// /install - auto-detects the caller's shell from the User-Agent and +// serves the matching installer (sh for curl/wget, PowerShell +// for irm/iwr). This is the universal one-liner. +// /install.sh - always the POSIX sh installer (verbose / explicit). +// /install.ps1 - always the PowerShell installer (verbose / explicit). +// +// The explicit endpoints also guard against being run in the wrong shell: if the +// sh installer is fetched by PowerShell (or the PowerShell installer by +// curl/wget), we serve a short message - written in the language of the shell +// that's about to run it - that points at the right command. Each installer also +// guards at runtime (uname / $PSVersionTable) for cases the User-Agent missed, +// e.g. curl inside Git Bash on Windows. // // The root URL behaviour is unchanged, so the existing -// `curl -L https://get-stackql-deploy.io | tar xzf -` one-liner keeps working -// for linux / windows / browser callers. The script path is purely additive. +// `curl -L https://get-stackql-deploy.io | tar xzf -` one-liner keeps working. function getAssetName(ua: string): string { if (/windows/i.test(ua)) return "stackql-deploy-windows-x86_64.zip"; @@ -20,7 +30,18 @@ function getAssetName(ua: string): string { return "stackql-deploy-linux-x86_64.tar.gz"; } -const INSTALL_SCRIPT = `#!/bin/sh +// True when the caller is PowerShell (Invoke-WebRequest / Invoke-RestMethod), +// which sets a User-Agent like "Mozilla/5.0 (Windows NT...) WindowsPowerShell/5.1". +function isPowerShell(ua: string): boolean { + return /powershell/i.test(ua); +} + +// True when the caller is a POSIX download tool that will pipe the body into sh. +function isPosixShellTool(ua: string): boolean { + return /\bcurl\b|\bwget\b/i.test(ua); +} + +const INSTALL_SH = `#!/bin/sh # stackql-deploy installer - https://get-stackql-deploy.io/install.sh # Detects OS + architecture and downloads the matching release binary into the # current directory. @@ -38,12 +59,27 @@ case "$os" in case "$arch" in x86_64 | amd64) asset="stackql-deploy-linux-x86_64.tar.gz" ;; aarch64 | arm64) asset="stackql-deploy-linux-arm64.tar.gz" ;; - *) echo "stackql-deploy: unsupported Linux architecture: $arch" >&2; exit 1 ;; + *) + echo "stackql-deploy: there's no prebuilt Linux binary for your CPU ($arch)." >&2 + echo "Prebuilt Linux builds cover x86_64 (amd64) and arm64 (aarch64)." >&2 + echo "Browse all downloads: ${RELEASES_URL}" >&2 + exit 1 + ;; esac ;; + MINGW* | MSYS* | CYGWIN* | Windows_NT) + echo "stackql-deploy: this looks like Windows in a POSIX shell ($os)." >&2 + echo "The Windows build installs from PowerShell. Open PowerShell and run:" >&2 + echo "" >&2 + echo " irm https://get-stackql-deploy.io/install.ps1 | iex" >&2 + echo "" >&2 + echo "Already in WSL and want the Linux build? Run this from your WSL shell." >&2 + exit 1 + ;; *) - echo "stackql-deploy: unsupported OS: $os" >&2 - echo "On Windows use PowerShell - see https://stackql.io/docs/installing-stackql" >&2 + echo "stackql-deploy: this installer doesn't recognize your system ($os $arch)." >&2 + echo "See the install guide for every option: ${DOCS_URL}" >&2 + echo "Or download a binary directly: ${RELEASES_URL}" >&2 exit 1 ;; esac @@ -54,22 +90,31 @@ chmod +x stackql-deploy echo "Installed ./stackql-deploy ($os/$arch). Run it with ./stackql-deploy or move it onto your PATH." `; -// PowerShell equivalent of the installer for Windows callers, so the awkward -// Invoke-WebRequest + Expand-Archive dance collapses to: -// irm https://get-stackql-deploy.io/install.ps1 | iex -// Windows ships a single x86_64 build; ARM64 Windows runs it under emulation, so -// both architectures resolve to the same asset. const INSTALL_PS1 = `#Requires -Version 5 # stackql-deploy installer - https://get-stackql-deploy.io/install.ps1 # Downloads the latest Windows release binary into the current directory. # Usage: irm https://get-stackql-deploy.io/install.ps1 | iex $ErrorActionPreference = 'Stop' +# Runtime guard: PowerShell also runs on macOS/Linux, where this Windows build +# won't help. Point those callers at the sh installer instead. +if ($PSVersionTable.Platform -eq 'Unix') { + Write-Host "stackql-deploy: this is the Windows installer, but you're on PowerShell on a non-Windows OS." + Write-Host "Install with:" + Write-Host "" + Write-Host " curl -fsSL https://get-stackql-deploy.io/install.sh | sh" + Write-Host "" + return +} + $base = "${RELEASE_BASE}" $asset = 'stackql-deploy-windows-x86_64.zip' $arch = $env:PROCESSOR_ARCHITECTURE if ($arch -ne 'AMD64' -and $arch -ne 'ARM64') { - throw "stackql-deploy: unsupported Windows architecture: $arch" + Write-Host "stackql-deploy: there's no prebuilt Windows binary for your CPU ($arch)." + Write-Host "Prebuilt Windows builds cover x64 (AMD64) and ARM64 (via emulation)." + Write-Host "Browse all downloads: ${RELEASES_URL}" + return } $dest = (Get-Location).Path @@ -81,29 +126,66 @@ Remove-Item $zip Write-Host "Installed .\\stackql-deploy.exe. Run it with .\\stackql-deploy.exe or move it onto your PATH." `; +// Served when the sh installer is fetched by PowerShell - valid PowerShell that +// just points the user at the Windows one-liner. +const GUIDE_USE_PS1 = `# stackql-deploy - wrong installer for this shell. +Write-Host "stackql-deploy: that's the Linux/macOS installer." +Write-Host "On Windows, install with:" +Write-Host "" +Write-Host " irm https://get-stackql-deploy.io/install.ps1 | iex" +Write-Host "" +`; + +// Served when the PowerShell installer is fetched by curl/wget - valid sh that +// just points the user at the Linux/macOS one-liner. +const GUIDE_USE_SH = `#!/bin/sh +# stackql-deploy - wrong installer for this shell. +echo "stackql-deploy: that's the Windows (PowerShell) installer." +echo "On macOS or Linux, install with:" +echo "" +echo " curl -fsSL https://get-stackql-deploy.io/install.sh | sh" +echo "" +exit 1 +`; + +function shResponse(body: string): Response { + return new Response(body, { + headers: { "content-type": "text/x-shellscript; charset=utf-8" }, + }); +} + +function ps1Response(body: string): Response { + return new Response(body, { + headers: { "content-type": "text/plain; charset=utf-8" }, + }); +} + export default { fetch(req: Request): Response { const url = new URL(req.url); + const ua = req.headers.get("user-agent") ?? ""; - // Dedicated installer endpoint for curl / wget (OS + arch detected client-side). - if (url.pathname === "/install.sh" || url.pathname === "/install") { - return new Response(INSTALL_SCRIPT, { - headers: { "content-type": "text/x-shellscript; charset=utf-8" }, - }); + // Universal installer: pick the script that matches the calling shell. + if (url.pathname === "/install") { + return isPowerShell(ua) ? ps1Response(INSTALL_PS1) : shResponse(INSTALL_SH); } - // PowerShell installer for Windows: irm https://get-stackql-deploy.io/install.ps1 | iex + // Explicit POSIX installer. If PowerShell fetched it, hand back a PowerShell + // message instead of sh it can't run. + if (url.pathname === "/install.sh") { + return isPowerShell(ua) ? ps1Response(GUIDE_USE_PS1) : shResponse(INSTALL_SH); + } + + // Explicit PowerShell installer. If curl/wget fetched it (i.e. it's about to + // be piped into sh), hand back an sh message instead of PowerShell. if (url.pathname === "/install.ps1") { - return new Response(INSTALL_PS1, { - headers: { "content-type": "text/plain; charset=utf-8" }, - }); + return isPosixShellTool(ua) ? shResponse(GUIDE_USE_SH) : ps1Response(INSTALL_PS1); } if (url.pathname !== "/") { return Response.redirect("https://stackql-deploy.io", 301); } - const ua = req.headers.get("user-agent") ?? ""; const asset = getAssetName(ua); return Response.redirect(`${RELEASE_BASE}/${asset}`, 302); }, From 12e330ab0c3249598fd338aa92c9d5d4d0218c13 Mon Sep 17 00:00:00 2001 From: Jeffrey Aven Date: Wed, 24 Jun 2026 10:41:29 +1000 Subject: [PATCH 6/8] Add installer smoke tests for sh and PowerShell test-get-app.sh and test-get-app.ps1 verify the live get-stackql-deploy.io worker: Cloudflare origin, every installer path / wrong-shell guard, the UA-based root redirects, and a real end-to-end install + run of the platform binary. The sh script stops at its Windows-shell guard when run under Git Bash; run it on macOS/Linux for the full install path. Co-Authored-By: Claude Opus 4.8 (1M context) --- test-get-app.ps1 | 141 +++++++++++++++++++++++++++++++++++++++++++++++ test-get-app.sh | 126 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 test-get-app.ps1 create mode 100644 test-get-app.sh diff --git a/test-get-app.ps1 b/test-get-app.ps1 new file mode 100644 index 0000000..2278de5 --- /dev/null +++ b/test-get-app.ps1 @@ -0,0 +1,141 @@ +# Client-side smoke test for the stackql-deploy installer (Windows). +# Confirms the origin is Cloudflare, exercises every installer path / shell-guard, +# then runs the real installer and checks the binary downloads and runs. +# Works on Windows PowerShell 5.1 and PowerShell 7+. + +$ErrorActionPreference = 'Stop' + +$Bin = 'stackql-deploy.exe' +$Base = 'https://get-stackql-deploy.io' +$InstallUrl = "$Base/install.ps1" + +# User-Agents the worker routes on: PowerShell vs a POSIX download tool. +$UaPs = 'Mozilla/5.0 (Windows NT 10.0) WindowsPowerShell/5.1' +$UaCurl = 'curl/8.4.0' + +foreach ($f in @('stackql-deploy', 'stackql', 'stackql-deploy.exe', 'stackql-deploy.zip')) { + if (Test-Path $f) { Remove-Item $f -Force } +} + +Add-Type -AssemblyName System.Net.Http + +# Fetch a URL with a given User-Agent without following redirects. Returns the +# status, Location header, Server header, and body so each check can assert. +function Get-Resp { + param([string]$Url, [string]$Ua) + $handler = [System.Net.Http.HttpClientHandler]::new() + $handler.AllowAutoRedirect = $false + $client = [System.Net.Http.HttpClient]::new($handler) + try { + $msg = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Get, $Url) + [void]$msg.Headers.TryAddWithoutValidation('User-Agent', $Ua) + $resp = $client.SendAsync($msg).Result + $body = $resp.Content.ReadAsStringAsync().Result + $location = '' + if ($resp.Headers.Location) { $location = $resp.Headers.Location.ToString() } + $server = '' + $vals = $null + if ($resp.Headers.TryGetValues('Server', [ref]$vals)) { $server = ($vals -join '') } + [pscustomobject]@{ + Status = [int]$resp.StatusCode + Location = $location + Server = $server + Body = $body + } + } finally { + $client.Dispose() + $handler.Dispose() + } +} + +function Assert-Body { + param([string]$Name, [string]$Url, [string]$Ua, [string]$Expect) + $resp = Get-Resp -Url $Url -Ua $Ua + if ($resp.Body -like "*$Expect*") { + Write-Host " ok: $Name" + } else { + $first = ($resp.Body -split "`n" | Select-Object -First 1) + Write-Host "FAIL: $Name" + Write-Host " expected body to contain: $Expect" + Write-Host " got first line: $first" + exit 1 + } +} + +function Assert-Location { + param([string]$Name, [string]$Url, [string]$Ua, [string]$Expect) + $resp = Get-Resp -Url $Url -Ua $Ua + if ($resp.Location -like "*$Expect*") { + Write-Host " ok: $Name -> $($resp.Location)" + } else { + Write-Host "FAIL: $Name" + Write-Host " expected Location containing: $Expect" + if ($resp.Location) { Write-Host " got: $($resp.Location)" } else { Write-Host " got: " } + exit 1 + } +} + +function Write-Box { + param([string]$Msg) + $line = '-' * ($Msg.Length + 4) + Write-Host "+$line+" + Write-Host "| $Msg |" + Write-Host "+$line+" +} + +Write-Box "Installing StackQL Deploy for Windows" + +Write-Host "Origin check:" +$origin = Get-Resp -Url $InstallUrl -Ua $UaPs +if ($origin.Server) { Write-Host " server: $($origin.Server)" } else { Write-Host " server: " } +if ($origin.Server -like '*cloudflare*') { + Write-Host " ok: served by Cloudflare" +} else { + Write-Host "FAIL: expected Cloudflare origin, got '$($origin.Server)'" + exit 1 +} +Write-Host "" + +Write-Host "Endpoint routing:" +# /install auto-detects the calling shell. +Assert-Body "/install (powershell) -> ps1 installer" "$Base/install" $UaPs '#Requires -Version 5' +Assert-Body "/install (curl) -> sh installer" "$Base/install" $UaCurl '#!/bin/sh' +# Explicit endpoints serve their real script for the matching shell. +Assert-Body "/install.ps1 (ps) -> ps1 installer" "$Base/install.ps1" $UaPs '#Requires -Version 5' +Assert-Body "/install.sh (curl) -> sh installer" "$Base/install.sh" $UaCurl '#!/bin/sh' +# Wrong-shell guards point at the correct command instead of erroring. +Assert-Body "/install.ps1 (curl) -> 'use install.sh'" "$Base/install.ps1" $UaCurl 'install.sh | sh' +Assert-Body "/install.sh (ps) -> 'use install.ps1'" "$Base/install.sh" $UaPs 'install.ps1 | iex' +Write-Host "" + +Write-Host "Root + fallback redirects:" +Assert-Location "/ (windows UA)" "$Base/" $UaPs 'stackql-deploy-windows-x86_64.zip' +Assert-Location "/ (linux UA)" "$Base/" $UaCurl 'stackql-deploy-linux-x86_64.tar.gz' +Assert-Location "/ (macOS UA)" "$Base/" 'Mozilla/5.0 (Macintosh; Intel Mac OS X)' 'stackql-deploy-macos-universal.tar.gz' +Assert-Location "/some/other/path" "$Base/some/other/path" $UaCurl 'stackql-deploy.io' +Write-Host "" + +Write-Host "Running installer:" +Invoke-RestMethod $InstallUrl | Invoke-Expression + +if (-not (Test-Path $Bin)) { + Write-Host "FAIL: $Bin was not downloaded" + exit 1 +} +Write-Host "" + +Write-Host "Binary:" +$item = Get-Item $Bin +Write-Host (" {0} {1:N0} bytes" -f $item.Name, $item.Length) +Write-Host "" + +Write-Host "Execution check:" +try { + & ".\$Bin" --version + Write-Host "" + Write-Host "PASS: runnable $Bin for Windows/$env:PROCESSOR_ARCHITECTURE" +} catch { + Write-Host "" + Write-Host "FAIL: $Bin did not run on this platform" + exit 1 +} diff --git a/test-get-app.sh b/test-get-app.sh new file mode 100644 index 0000000..600e1d8 --- /dev/null +++ b/test-get-app.sh @@ -0,0 +1,126 @@ +#!/bin/sh +# Client-side smoke test for the stackql-deploy installer (mac/linux). +# Confirms the origin is Cloudflare, exercises every installer path / shell-guard, +# then runs the real installer and checks the binary is the right platform build, +# executable, and runnable. + +set -eu + +BIN=stackql-deploy +BASE=https://get-stackql-deploy.io +INSTALL_URL="$BASE/install.sh" + +# User-Agents the worker routes on: a POSIX download tool vs PowerShell. +UA_CURL="curl/8.4.0" +UA_PS="Mozilla/5.0 (Windows NT 10.0) WindowsPowerShell/5.1" + +rm -f stackql-deploy +rm -f stackql +rm -f stackql-deploy.exe +rm -f stackql-deploy.zip +rm -f stackql-*-shell.sh + +print_box() { + msg="$1" + width=$(( ${#msg} + 4 )) + line=$(printf '%*s' "$width" '' | tr ' ' '-') + printf '+%s+\n' "$line" + printf '| %s |\n' "$msg" + printf '+%s+\n' "$line" +} + +# Fetch a body with a given User-Agent and assert it contains a substring. +check_body() { + name="$1"; url="$2"; ua="$3"; expect="$4" + body=$(curl -fsSL -A "$ua" "$url") + case "$body" in + *"$expect"*) echo " ok: $name" ;; + *) + echo "FAIL: $name" + echo " expected body to contain: $expect" + echo " got first line: $(printf '%s' "$body" | sed -n '1p')" + exit 1 + ;; + esac +} + +# Assert a path redirects (no -L) to a Location containing a substring. +check_redirect() { + name="$1"; url="$2"; ua="$3"; expect="$4" + loc=$(curl -fsS -o /dev/null -D - -A "$ua" "$url" \ + | awk -F': ' 'tolower($1)=="location"{print $2}' | tr -d '\r') + case "$loc" in + *"$expect"*) echo " ok: $name -> $loc" ;; + *) + echo "FAIL: $name" + echo " expected Location containing: $expect" + echo " got: ${loc:-}" + exit 1 + ;; + esac +} + +print_box "Installing StackQL Deploy for MacOS/Linux" + +echo "Origin check:" +server=$(curl -fsSL -D - -o /dev/null "$INSTALL_URL" | awk -F': ' 'tolower($1)=="server"{print $2}' | tr -d '\r') +echo " server: ${server:-}" +case "$(printf '%s' "$server" | tr 'A-Z' 'a-z')" in + *cloudflare*) echo " ok: served by Cloudflare" ;; + *) echo "FAIL: expected Cloudflare origin, got '${server:-}'"; exit 1 ;; +esac +echo + +echo "Endpoint routing:" +# /install auto-detects the calling shell. +check_body "/install (curl) -> sh installer" "$BASE/install" "$UA_CURL" "#!/bin/sh" +check_body "/install (powershell) -> ps1 installer" "$BASE/install" "$UA_PS" "#Requires -Version 5" +# Explicit endpoints serve their real script for the matching shell. +check_body "/install.sh (curl) -> sh installer" "$BASE/install.sh" "$UA_CURL" "#!/bin/sh" +check_body "/install.ps1 (ps) -> ps1 installer" "$BASE/install.ps1" "$UA_PS" "#Requires -Version 5" +# Wrong-shell guards point at the correct command instead of erroring. +check_body "/install.sh (ps) -> 'use install.ps1'" "$BASE/install.sh" "$UA_PS" "install.ps1 | iex" +check_body "/install.ps1 (curl) -> 'use install.sh'" "$BASE/install.ps1" "$UA_CURL" "install.sh | sh" +echo + +echo "Root + fallback redirects:" +check_redirect "/ (linux UA)" "$BASE/" "$UA_CURL" "stackql-deploy-linux-x86_64.tar.gz" +check_redirect "/ (macOS UA)" "$BASE/" "Mozilla/5.0 (Macintosh; Intel Mac OS X)" "stackql-deploy-macos-universal.tar.gz" +check_redirect "/ (windows UA)" "$BASE/" "Mozilla/5.0 (Windows NT 10.0; Win64)" "stackql-deploy-windows-x86_64.zip" +check_redirect "/some/other/path" "$BASE/some/other/path" "$UA_CURL" "stackql-deploy.io" +echo + +echo "Running installer:" +curl -fsSL "$INSTALL_URL" | sh + +if [ ! -e "$BIN" ]; then + echo "FAIL: $BIN was not downloaded" + exit 1 +fi +echo + +echo "Binary:" +if command -v file >/dev/null 2>&1; then + file "$BIN" +else + echo " (file not available, skipping arch detail)" +fi +echo + +echo "Permissions:" +ls -l "$BIN" +if [ ! -x "$BIN" ]; then + echo "FAIL: $BIN is not executable" + exit 1 +fi +echo + +echo "Execution check:" +if ./"$BIN" --version; then + echo + echo "PASS: runnable $BIN for $(uname -s)/$(uname -m)" +else + echo + echo "FAIL: $BIN did not run on this platform (wrong binary or exec format error)" + exit 1 +fi From 20ee9a0b5656616ffab15eac88027acbddd5eeb9 Mon Sep 17 00:00:00 2001 From: Jeffrey Aven Date: Wed, 24 Jun 2026 10:54:25 +1000 Subject: [PATCH 7/8] Add colored PASS/FAIL output and summary to smoke tests Each check now prints a green PASS or red FAIL, all checks run instead of bailing on the first failure, and both scripts end with a colored summary box and a non-zero exit on any failure. The sh script gates ANSI codes on a TTY so piped output stays clean; the PowerShell script uses -ForegroundColor. Co-Authored-By: Claude Opus 4.8 (1M context) --- test-get-app.ps1 | 89 ++++++++++++++++++++++---------------- test-get-app.sh | 110 +++++++++++++++++++++++++++-------------------- 2 files changed, 117 insertions(+), 82 deletions(-) diff --git a/test-get-app.ps1 b/test-get-app.ps1 index 2278de5..2d37cca 100644 --- a/test-get-app.ps1 +++ b/test-get-app.ps1 @@ -1,6 +1,7 @@ # Client-side smoke test for the stackql-deploy installer (Windows). # Confirms the origin is Cloudflare, exercises every installer path / shell-guard, -# then runs the real installer and checks the binary downloads and runs. +# then runs the real installer and checks the binary downloads and runs. Prints a +# green PASS / red FAIL per check and a final colored summary. # Works on Windows PowerShell 5.1 and PowerShell 7+. $ErrorActionPreference = 'Stop' @@ -13,6 +14,11 @@ $InstallUrl = "$Base/install.ps1" $UaPs = 'Mozilla/5.0 (Windows NT 10.0) WindowsPowerShell/5.1' $UaCurl = 'curl/8.4.0' +$script:Failures = 0 + +function Pass { param([string]$Name) Write-Host " PASS " -ForegroundColor Green -NoNewline; Write-Host $Name } +function Fail { param([string]$Name) Write-Host " FAIL " -ForegroundColor Red -NoNewline; Write-Host $Name; $script:Failures++ } + foreach ($f in @('stackql-deploy', 'stackql', 'stackql-deploy.exe', 'stackql-deploy.zip')) { if (Test-Path $f) { Remove-Item $f -Force } } @@ -50,28 +56,23 @@ function Get-Resp { function Assert-Body { param([string]$Name, [string]$Url, [string]$Ua, [string]$Expect) - $resp = Get-Resp -Url $Url -Ua $Ua + try { $resp = Get-Resp -Url $Url -Ua $Ua } catch { Fail "$Name (request failed)"; return } if ($resp.Body -like "*$Expect*") { - Write-Host " ok: $Name" + Pass $Name } else { $first = ($resp.Body -split "`n" | Select-Object -First 1) - Write-Host "FAIL: $Name" - Write-Host " expected body to contain: $Expect" - Write-Host " got first line: $first" - exit 1 + Fail "$Name (expected '$Expect', got '$first')" } } function Assert-Location { param([string]$Name, [string]$Url, [string]$Ua, [string]$Expect) - $resp = Get-Resp -Url $Url -Ua $Ua + try { $resp = Get-Resp -Url $Url -Ua $Ua } catch { Fail "$Name (request failed)"; return } if ($resp.Location -like "*$Expect*") { - Write-Host " ok: $Name -> $($resp.Location)" + Pass "$Name -> $($resp.Location)" } else { - Write-Host "FAIL: $Name" - Write-Host " expected Location containing: $Expect" - if ($resp.Location) { Write-Host " got: $($resp.Location)" } else { Write-Host " got: " } - exit 1 + $got = if ($resp.Location) { $resp.Location } else { '' } + Fail "$Name (expected Location '$Expect', got '$got')" } } @@ -86,13 +87,12 @@ function Write-Box { Write-Box "Installing StackQL Deploy for Windows" Write-Host "Origin check:" -$origin = Get-Resp -Url $InstallUrl -Ua $UaPs -if ($origin.Server) { Write-Host " server: $($origin.Server)" } else { Write-Host " server: " } -if ($origin.Server -like '*cloudflare*') { - Write-Host " ok: served by Cloudflare" +try { $origin = Get-Resp -Url $InstallUrl -Ua $UaPs } catch { $origin = $null } +if ($origin -and $origin.Server -like '*cloudflare*') { + Pass "served by Cloudflare (server: $($origin.Server))" } else { - Write-Host "FAIL: expected Cloudflare origin, got '$($origin.Server)'" - exit 1 + $got = if ($origin) { $origin.Server } else { '' } + Fail "expected Cloudflare origin, got '$got'" } Write-Host "" @@ -116,26 +116,43 @@ Assert-Location "/some/other/path" "$Base/some/other/path" $UaCurl Write-Host "" Write-Host "Running installer:" -Invoke-RestMethod $InstallUrl | Invoke-Expression - -if (-not (Test-Path $Bin)) { - Write-Host "FAIL: $Bin was not downloaded" - exit 1 +try { + Invoke-RestMethod $InstallUrl | Invoke-Expression +} catch { + Fail "installer raised an error: $($_.Exception.Message)" +} +if (Test-Path $Bin) { + Pass "installer downloaded $Bin" +} else { + Fail "installer did not produce $Bin" } Write-Host "" -Write-Host "Binary:" -$item = Get-Item $Bin -Write-Host (" {0} {1:N0} bytes" -f $item.Name, $item.Length) -Write-Host "" - -Write-Host "Execution check:" -try { - & ".\$Bin" --version +if (Test-Path $Bin) { + Write-Host "Binary:" + $item = Get-Item $Bin + Write-Host (" {0} {1:N0} bytes" -f $item.Name, $item.Length) Write-Host "" - Write-Host "PASS: runnable $Bin for Windows/$env:PROCESSOR_ARCHITECTURE" -} catch { + + Write-Host "Execution check:" + try { + & ".\$Bin" --version + Pass "runnable $Bin for Windows/$env:PROCESSOR_ARCHITECTURE" + } catch { + Fail "$Bin did not run on this platform" + } Write-Host "" - Write-Host "FAIL: $Bin did not run on this platform" - exit 1 } + +# Final summary. +if ($script:Failures -eq 0) { + $color = 'Green'; $text = " PASS - all checks passed " +} else { + $color = 'Red'; $text = " FAIL - $($script:Failures) check(s) failed " +} +$line = '+' + ('-' * $text.Length) + '+' +Write-Host $line -ForegroundColor $color +Write-Host "|$text|" -ForegroundColor $color +Write-Host $line -ForegroundColor $color + +if ($script:Failures -ne 0) { exit 1 } diff --git a/test-get-app.sh b/test-get-app.sh index 600e1d8..511b6b2 100644 --- a/test-get-app.sh +++ b/test-get-app.sh @@ -2,9 +2,10 @@ # Client-side smoke test for the stackql-deploy installer (mac/linux). # Confirms the origin is Cloudflare, exercises every installer path / shell-guard, # then runs the real installer and checks the binary is the right platform build, -# executable, and runnable. +# executable, and runnable. Prints a green PASS / red FAIL per check and a final +# colored summary. -set -eu +set -u BIN=stackql-deploy BASE=https://get-stackql-deploy.io @@ -14,6 +15,19 @@ INSTALL_URL="$BASE/install.sh" UA_CURL="curl/8.4.0" UA_PS="Mozilla/5.0 (Windows NT 10.0) WindowsPowerShell/5.1" +# Colors, only when stdout is a terminal (keeps piped/redirected output clean). +if [ -t 1 ]; then + GREEN=$(printf '\033[32m'); RED=$(printf '\033[31m') + BOLD=$(printf '\033[1m'); RESET=$(printf '\033[0m') +else + GREEN=''; RED=''; BOLD=''; RESET='' +fi + +FAILURES=0 + +pass() { printf ' %s%sPASS%s %s\n' "$BOLD" "$GREEN" "$RESET" "$1"; } +fail() { printf ' %s%sFAIL%s %s\n' "$BOLD" "$RED" "$RESET" "$1"; FAILURES=$((FAILURES + 1)); } + rm -f stackql-deploy rm -f stackql rm -f stackql-deploy.exe @@ -32,42 +46,32 @@ print_box() { # Fetch a body with a given User-Agent and assert it contains a substring. check_body() { name="$1"; url="$2"; ua="$3"; expect="$4" - body=$(curl -fsSL -A "$ua" "$url") + body=$(curl -fsSL -A "$ua" "$url" 2>/dev/null) || { fail "$name (request failed)"; return; } case "$body" in - *"$expect"*) echo " ok: $name" ;; - *) - echo "FAIL: $name" - echo " expected body to contain: $expect" - echo " got first line: $(printf '%s' "$body" | sed -n '1p')" - exit 1 - ;; + *"$expect"*) pass "$name" ;; + *) fail "$name (expected '$expect', got '$(printf '%s' "$body" | sed -n '1p')')" ;; esac } # Assert a path redirects (no -L) to a Location containing a substring. check_redirect() { name="$1"; url="$2"; ua="$3"; expect="$4" - loc=$(curl -fsS -o /dev/null -D - -A "$ua" "$url" \ + loc=$(curl -fsS -o /dev/null -D - -A "$ua" "$url" 2>/dev/null \ | awk -F': ' 'tolower($1)=="location"{print $2}' | tr -d '\r') case "$loc" in - *"$expect"*) echo " ok: $name -> $loc" ;; - *) - echo "FAIL: $name" - echo " expected Location containing: $expect" - echo " got: ${loc:-}" - exit 1 - ;; + *"$expect"*) pass "$name -> $loc" ;; + *) fail "$name (expected Location '$expect', got '${loc:-}')" ;; esac } print_box "Installing StackQL Deploy for MacOS/Linux" echo "Origin check:" -server=$(curl -fsSL -D - -o /dev/null "$INSTALL_URL" | awk -F': ' 'tolower($1)=="server"{print $2}' | tr -d '\r') -echo " server: ${server:-}" +server=$(curl -fsSL -D - -o /dev/null "$INSTALL_URL" 2>/dev/null \ + | awk -F': ' 'tolower($1)=="server"{print $2}' | tr -d '\r') case "$(printf '%s' "$server" | tr 'A-Z' 'a-z')" in - *cloudflare*) echo " ok: served by Cloudflare" ;; - *) echo "FAIL: expected Cloudflare origin, got '${server:-}'"; exit 1 ;; + *cloudflare*) pass "served by Cloudflare (server: ${server:-})" ;; + *) fail "expected Cloudflare origin, got '${server:-}'" ;; esac echo @@ -92,35 +96,49 @@ echo echo "Running installer:" curl -fsSL "$INSTALL_URL" | sh - -if [ ! -e "$BIN" ]; then - echo "FAIL: $BIN was not downloaded" - exit 1 -fi -echo - -echo "Binary:" -if command -v file >/dev/null 2>&1; then - file "$BIN" +if [ -e "$BIN" ]; then + pass "installer downloaded $BIN" else - echo " (file not available, skipping arch detail)" + fail "installer did not produce $BIN (expected on Windows/Git Bash; run on mac/linux for the full path)" fi echo -echo "Permissions:" -ls -l "$BIN" -if [ ! -x "$BIN" ]; then - echo "FAIL: $BIN is not executable" - exit 1 -fi -echo +if [ -e "$BIN" ]; then + echo "Binary:" + if command -v file >/dev/null 2>&1; then + file "$BIN" + else + echo " (file not available, skipping arch detail)" + fi + echo -echo "Execution check:" -if ./"$BIN" --version; then + echo "Permissions:" + ls -l "$BIN" + if [ -x "$BIN" ]; then + pass "$BIN is executable" + else + fail "$BIN is not executable" + fi echo - echo "PASS: runnable $BIN for $(uname -s)/$(uname -m)" -else + + echo "Execution check:" + if ./"$BIN" --version; then + pass "runnable $BIN for $(uname -s)/$(uname -m)" + else + fail "$BIN did not run (wrong binary or exec format error)" + fi echo - echo "FAIL: $BIN did not run on this platform (wrong binary or exec format error)" - exit 1 fi + +# Final summary. +if [ "$FAILURES" -eq 0 ]; then + color=$GREEN; text=" PASS - all checks passed " +else + color=$RED; text=" FAIL - $FAILURES check(s) failed " +fi +line=$(printf '%*s' "${#text}" '' | tr ' ' '-') +printf '%s%s+%s+%s\n' "$BOLD" "$color" "$line" "$RESET" +printf '%s%s|%s|%s\n' "$BOLD" "$color" "$text" "$RESET" +printf '%s%s+%s+%s\n' "$BOLD" "$color" "$line" "$RESET" + +[ "$FAILURES" -eq 0 ] From 53423fa53b46000a701dfc008ca5a381d9264d7e Mon Sep 17 00:00:00 2001 From: Jeffrey Aven Date: Wed, 24 Jun 2026 10:58:31 +1000 Subject: [PATCH 8/8] updated get-app --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5a49eb8..a267564 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ stackql_history.txt stackql.log stackql-zip stackql-deploy +stackql-deploy.exe .stackql-deploy-exports .env nohup.out