Blame · Line-by-line history
install.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 46d6165 | 1 | /** |
| 2 | * Block L2 — one-command install. | |
| 3 | * | |
| 63fe719 | 4 | * GET /install -> the bash installer (scripts/install.sh). |
| 5 | * GET /install/vscode -> a tiny HTML landing for the VS Code extension | |
| 6 | * with install instructions and a .vsix link if | |
| 7 | * one has been uploaded to `public/`. | |
| 46d6165 | 8 | * |
| 9 | * Curl-able: `curl -sSL https://gluecron.com/install | bash`. | |
| 10 | * | |
| 11 | * The script is read from disk once at module load and cached in memory; we | |
| 12 | * also serve it with `Cache-Control: public, max-age=300` so any CDN in front | |
| 13 | * of us can absorb the load. Mirrors the static-string pattern used by | |
| 14 | * `src/routes/pwa.ts`. | |
| 15 | */ | |
| 16 | ||
| 17 | import { Hono } from "hono"; | |
| 63fe719 | 18 | import { readFileSync, existsSync, statSync, readdirSync } from "fs"; |
| 46d6165 | 19 | import { join } from "path"; |
| 20 | ||
| 21 | const install = new Hono(); | |
| 22 | ||
| 23 | // Resolve at module-load time. We try the on-disk script first; if anything | |
| 24 | // goes sideways (missing file in a stripped container image, weird CWD, etc.) | |
| 25 | // we fall back to a stub that points the user at the canonical URL so the | |
| 26 | // endpoint always returns *something* shell-safe. | |
| 27 | const FALLBACK_SCRIPT = `#!/usr/bin/env bash | |
| 28 | echo "Gluecron install script unavailable on this host." >&2 | |
| 29 | echo "Fetch it directly from https://gluecron.com/install" >&2 | |
| 30 | exit 1 | |
| 31 | `; | |
| 32 | ||
| 33 | function loadScript(): string { | |
| 34 | // Walk a few candidate locations so this works in dev, tests, and the | |
| 35 | // fly.io container where CWD may be /app. | |
| 36 | const candidates = [ | |
| 37 | join(process.cwd(), "scripts", "install.sh"), | |
| 38 | join(import.meta.dir, "..", "..", "scripts", "install.sh"), | |
| 39 | ]; | |
| 40 | for (const p of candidates) { | |
| 41 | try { | |
| 42 | const buf = readFileSync(p, "utf8"); | |
| 43 | if (buf && buf.length > 0) return buf; | |
| 44 | } catch { | |
| 45 | // try the next candidate | |
| 46 | } | |
| 47 | } | |
| 48 | return FALLBACK_SCRIPT; | |
| 49 | } | |
| 50 | ||
| 51 | export const INSTALL_SCRIPT_SRC = loadScript(); | |
| 52 | ||
| 53 | install.get("/install", (c) => { | |
| 54 | c.header("content-type", "text/x-shellscript; charset=utf-8"); | |
| 55 | c.header("cache-control", "public, max-age=300"); | |
| 56 | // Make `curl https://gluecron.com/install -o install.sh` give a sensible | |
| 57 | // default filename without forcing it for piped installs. | |
| 58 | c.header("content-disposition", 'inline; filename="install.sh"'); | |
| 59 | return c.body(INSTALL_SCRIPT_SRC); | |
| 60 | }); | |
| 61 | ||
| 63fe719 | 62 | // ─── VS Code extension landing ────────────────────────────────────────────── |
| 63 | // | |
| 64 | // We don't (yet) have a marketplace listing, so this page shows install | |
| 65 | // instructions and — if a .vsix has been dropped into `public/` — a | |
| 66 | // download link. The page is intentionally minimalist: it does NOT pull | |
| 67 | // in the main app layout so a misbehaving CSS change can't break the | |
| 68 | // install funnel. | |
| 69 | ||
| 70 | const MARKETPLACE_URL = | |
| 71 | "https://marketplace.visualstudio.com/items?itemName=gluecron.gluecron-vscode"; | |
| 72 | ||
| 73 | function findVsix(): { name: string; size: number } | null { | |
| 74 | // Look under `public/` so the file is served by the static handler | |
| 75 | // (Bun's `Bun.file` route at /:filename). Keep it deterministic — | |
| 76 | // largest-version sorted last after lexicographic sort works for our | |
| 77 | // semver naming. | |
| 78 | const candidates = [ | |
| 79 | join(process.cwd(), "public"), | |
| 80 | join(import.meta.dir, "..", "..", "public"), | |
| 81 | ]; | |
| 82 | for (const dir of candidates) { | |
| 83 | try { | |
| 84 | if (!existsSync(dir)) continue; | |
| 85 | const files = readdirSync(dir).filter((f) => f.endsWith(".vsix")); | |
| 86 | if (files.length === 0) continue; | |
| 87 | files.sort(); | |
| 88 | const pick = files[files.length - 1]; | |
| 89 | const st = statSync(join(dir, pick)); | |
| 90 | return { name: pick, size: st.size }; | |
| 91 | } catch { | |
| 92 | // try the next candidate | |
| 93 | } | |
| 94 | } | |
| 95 | return null; | |
| 96 | } | |
| 97 | ||
| 98 | function formatBytes(n: number): string { | |
| 99 | if (n < 1024) return `${n} B`; | |
| 100 | if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`; | |
| 101 | return `${(n / (1024 * 1024)).toFixed(1)} MiB`; | |
| 102 | } | |
| 103 | ||
| 104 | function vscodeLandingHtml(vsix: { name: string; size: number } | null): string { | |
| 105 | const vsixBlock = vsix | |
| 106 | ? ` | |
| 107 | <p> | |
| 108 | <a class="cta" href="/${encodeURIComponent(vsix.name)}" download> | |
| 109 | Download ${vsix.name} (${formatBytes(vsix.size)}) | |
| 110 | </a> | |
| 111 | </p> | |
| 112 | <pre><code>code --install-extension ${vsix.name}</code></pre>` | |
| 113 | : ` | |
| 114 | <p class="muted"> | |
| 115 | No <code>.vsix</code> uploaded yet — build one yourself: | |
| 116 | </p> | |
| 117 | <pre><code>git clone https://gluecron.com/ccantynz/Gluecron.com | |
| 118 | cd Gluecron.com/editor-extensions/vscode | |
| 119 | npm install | |
| 120 | npm run compile | |
| 121 | npx vsce package | |
| 122 | code --install-extension gluecron-vscode-0.1.0.vsix</code></pre>`; | |
| 123 | ||
| 124 | return `<!DOCTYPE html> | |
| 125 | <html lang="en"> | |
| 126 | <head> | |
| 127 | <meta charset="utf-8" /> | |
| 128 | <title>Gluecron for VS Code</title> | |
| 129 | <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| 130 | <style> | |
| 131 | :root { color-scheme: dark light; } | |
| 132 | body { font: 15px/1.55 system-ui, sans-serif; max-width: 640px; margin: 5rem auto; padding: 0 1rem; } | |
| 133 | h1 { margin-top: 0; font-size: 1.6rem; } | |
| 134 | .badge { display: inline-block; padding: 4px 10px; border-radius: 4px; background: #2c2c2c; color: #fff; font-size: 13px; text-decoration: none; } | |
| 135 | .cta { display: inline-block; padding: 8px 14px; border-radius: 6px; background: #1f6feb; color: #fff; text-decoration: none; font-weight: 600; } | |
| 136 | pre { background: #0e1116; color: #e6edf3; padding: 12px; border-radius: 6px; overflow-x: auto; } | |
| 137 | code { font: 13px ui-monospace, monospace; } | |
| 138 | .muted { opacity: 0.75; } | |
| 139 | ul { padding-left: 1.2rem; } | |
| 140 | </style> | |
| 141 | </head> | |
| 142 | <body> | |
| 143 | <h1>Gluecron for VS Code</h1> | |
| 144 | <p> | |
| 145 | Chat with this repo, ship PRs, run specs, voice-to-PR, and let Claude write | |
| 146 | your commit messages — without leaving the editor. | |
| 147 | </p> | |
| 148 | <p> | |
| 149 | <a class="badge" href="${MARKETPLACE_URL}" rel="nofollow"> | |
| 150 | Marketplace listing (coming soon) | |
| 151 | </a> | |
| 152 | </p> | |
| 153 | <h2>Install</h2> | |
| 154 | ${vsixBlock} | |
| 155 | <h2>What you get</h2> | |
| 156 | <ul> | |
| 157 | <li>Sidebar <strong>repo chat</strong> grounded in the current repository</li> | |
| 158 | <li>One-click <strong>AI commit messages</strong> in the SCM title bar</li> | |
| 159 | <li><strong>Open in Gluecron</strong> deep-links for the active file / line</li> | |
| 160 | <li>Embedded <strong>pull requests</strong>, <strong>issues</strong>, and <strong>AI standups</strong></li> | |
| 161 | <li><strong>Ship spec</strong> + <strong>voice-to-PR</strong> shortcuts</li> | |
| 162 | </ul> | |
| 163 | <p class="muted">Source: <a href="/ccantynz/Gluecron.com/tree/main/editor-extensions/vscode">editor-extensions/vscode</a></p> | |
| 164 | </body> | |
| 165 | </html>`; | |
| 166 | } | |
| 167 | ||
| 168 | install.get("/install/vscode", (c) => { | |
| 169 | const vsix = findVsix(); | |
| 170 | c.header("content-type", "text/html; charset=utf-8"); | |
| 171 | c.header("cache-control", "public, max-age=300"); | |
| 172 | return c.body(vscodeLandingHtml(vsix)); | |
| 173 | }); | |
| 174 | ||
| 46d6165 | 175 | export default install; |