CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
build-dxt-assets.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.
| cd4f63b | 1 | #!/usr/bin/env bun |
| 2 | /** | |
| 3 | * BLOCK Q1 — placeholder asset generator for the Claude Desktop .dxt bundle. | |
| 4 | * | |
| 5 | * Generates two PNGs into extension/gluecron.dxt/: | |
| 6 | * - icon.png 256x256 — dark-bg "g" mark with accent gradient | |
| 7 | * - screenshot-1.png 1280x800 — dark-bg wordmark placeholder | |
| 8 | * | |
| 9 | * These are intentionally simple: hand-rolled PNGs (no Canvas / sharp / etc.) | |
| 10 | * so the build works on any system with `bun` + system `zip`. The intent is | |
| 11 | * for a designer to replace them later; both files are committed as | |
| 12 | * placeholders so the .dxt is shippable today. | |
| 13 | * | |
| 14 | * Follow-up: replace icon.png with the actual brand "g" SVG-rasterised at | |
| 15 | * 256x256, and screenshot-1.png with a real product screenshot of Claude | |
| 16 | * opening a PR on Gluecron. | |
| 17 | * | |
| 18 | * Usage: | |
| 19 | * bun run scripts/build-dxt-assets.ts | |
| 20 | * | |
| 21 | * Idempotent — re-running overwrites the files in place. | |
| 22 | */ | |
| 23 | ||
| 24 | import { writeFileSync, mkdirSync } from "node:fs"; | |
| 25 | import { join, dirname } from "node:path"; | |
| 26 | import { deflateSync } from "node:zlib"; | |
| 27 | ||
| 28 | const OUT_DIR = join(import.meta.dir, "..", "extension", "gluecron.dxt"); | |
| 29 | ||
| 30 | // --------------------------------------------------------------------------- | |
| 31 | // Minimal PNG encoder (RGB, no alpha, no filtering beyond filter=0). | |
| 32 | // Hand-rolled to avoid pulling in a dependency just for placeholder assets. | |
| 33 | // --------------------------------------------------------------------------- | |
| 34 | ||
| 35 | const CRC_TABLE = (() => { | |
| 36 | const t = new Uint32Array(256); | |
| 37 | for (let n = 0; n < 256; n++) { | |
| 38 | let c = n; | |
| 39 | for (let k = 0; k < 8; k++) { | |
| 40 | c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; | |
| 41 | } | |
| 42 | t[n] = c >>> 0; | |
| 43 | } | |
| 44 | return t; | |
| 45 | })(); | |
| 46 | ||
| 47 | function crc32(buf: Uint8Array): number { | |
| 48 | let c = 0xffffffff; | |
| 49 | for (let i = 0; i < buf.length; i++) { | |
| 50 | c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8); | |
| 51 | } | |
| 52 | return (c ^ 0xffffffff) >>> 0; | |
| 53 | } | |
| 54 | ||
| 55 | function u32be(n: number): Uint8Array { | |
| 56 | const b = new Uint8Array(4); | |
| 57 | b[0] = (n >>> 24) & 0xff; | |
| 58 | b[1] = (n >>> 16) & 0xff; | |
| 59 | b[2] = (n >>> 8) & 0xff; | |
| 60 | b[3] = n & 0xff; | |
| 61 | return b; | |
| 62 | } | |
| 63 | ||
| 64 | function chunk(type: string, data: Uint8Array): Uint8Array { | |
| 65 | const typeBytes = new TextEncoder().encode(type); | |
| 66 | const len = u32be(data.length); | |
| 67 | const crcInput = new Uint8Array(typeBytes.length + data.length); | |
| 68 | crcInput.set(typeBytes, 0); | |
| 69 | crcInput.set(data, typeBytes.length); | |
| 70 | const crc = u32be(crc32(crcInput)); | |
| 71 | const out = new Uint8Array(4 + 4 + data.length + 4); | |
| 72 | out.set(len, 0); | |
| 73 | out.set(typeBytes, 4); | |
| 74 | out.set(data, 8); | |
| 75 | out.set(crc, 8 + data.length); | |
| 76 | return out; | |
| 77 | } | |
| 78 | ||
| 79 | function encodePng( | |
| 80 | width: number, | |
| 81 | height: number, | |
| 82 | pixels: Uint8Array // RGB, length = width*height*3 | |
| 83 | ): Uint8Array { | |
| 84 | if (pixels.length !== width * height * 3) { | |
| 85 | throw new Error("pixel buffer size mismatch"); | |
| 86 | } | |
| 87 | // IHDR | |
| 88 | const ihdr = new Uint8Array(13); | |
| 89 | ihdr.set(u32be(width), 0); | |
| 90 | ihdr.set(u32be(height), 4); | |
| 91 | ihdr[8] = 8; // bit depth | |
| 92 | ihdr[9] = 2; // color type: RGB | |
| 93 | ihdr[10] = 0; // compression | |
| 94 | ihdr[11] = 0; // filter | |
| 95 | ihdr[12] = 0; // interlace | |
| 96 | ||
| 97 | // IDAT — prepend filter byte (0 = None) to every scanline. | |
| 98 | const raw = new Uint8Array(height * (1 + width * 3)); | |
| 99 | for (let y = 0; y < height; y++) { | |
| 100 | const rowStart = y * (1 + width * 3); | |
| 101 | raw[rowStart] = 0; // filter type | |
| 102 | raw.set(pixels.subarray(y * width * 3, (y + 1) * width * 3), rowStart + 1); | |
| 103 | } | |
| 104 | const idat = deflateSync(Buffer.from(raw)); | |
| 105 | ||
| 106 | const SIG = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); | |
| 107 | const ihdrChunk = chunk("IHDR", ihdr); | |
| 108 | const idatChunk = chunk("IDAT", new Uint8Array(idat)); | |
| 109 | const iendChunk = chunk("IEND", new Uint8Array(0)); | |
| 110 | ||
| 111 | const out = new Uint8Array( | |
| 112 | SIG.length + ihdrChunk.length + idatChunk.length + iendChunk.length | |
| 113 | ); | |
| 114 | let off = 0; | |
| 115 | out.set(SIG, off); | |
| 116 | off += SIG.length; | |
| 117 | out.set(ihdrChunk, off); | |
| 118 | off += ihdrChunk.length; | |
| 119 | out.set(idatChunk, off); | |
| 120 | off += idatChunk.length; | |
| 121 | out.set(iendChunk, off); | |
| 122 | return out; | |
| 123 | } | |
| 124 | ||
| 125 | // --------------------------------------------------------------------------- | |
| 126 | // Drawing helpers | |
| 127 | // --------------------------------------------------------------------------- | |
| 128 | ||
| 129 | type Rgb = [number, number, number]; | |
| 130 | ||
| 131 | const BG: Rgb = [0x0d, 0x11, 0x17]; // #0d1117 | |
| 132 | const ACCENT_START: Rgb = [0x8c, 0x6d, 0xff]; // #8c6dff (purple) | |
| 133 | const ACCENT_END: Rgb = [0x36, 0xc5, 0xd6]; // #36c5d6 (teal) | |
| 134 | const FG: Rgb = [0xe6, 0xed, 0xf3]; // soft white | |
| 135 | ||
| 136 | function lerp(a: number, b: number, t: number): number { | |
| 137 | return Math.round(a + (b - a) * t); | |
| 138 | } | |
| 139 | ||
| 140 | function lerpRgb(a: Rgb, b: Rgb, t: number): Rgb { | |
| 141 | return [lerp(a[0], b[0], t), lerp(a[1], b[1], t), lerp(a[2], b[2], t)]; | |
| 142 | } | |
| 143 | ||
| 144 | function makeBuf(w: number, h: number, fill: Rgb): Uint8Array { | |
| 145 | const buf = new Uint8Array(w * h * 3); | |
| 146 | for (let i = 0; i < w * h; i++) { | |
| 147 | buf[i * 3] = fill[0]; | |
| 148 | buf[i * 3 + 1] = fill[1]; | |
| 149 | buf[i * 3 + 2] = fill[2]; | |
| 150 | } | |
| 151 | return buf; | |
| 152 | } | |
| 153 | ||
| 154 | function setPixel(buf: Uint8Array, w: number, x: number, y: number, c: Rgb) { | |
| 155 | const i = (y * w + x) * 3; | |
| 156 | buf[i] = c[0]; | |
| 157 | buf[i + 1] = c[1]; | |
| 158 | buf[i + 2] = c[2]; | |
| 159 | } | |
| 160 | ||
| 161 | function fillRect( | |
| 162 | buf: Uint8Array, | |
| 163 | w: number, | |
| 164 | x0: number, | |
| 165 | y0: number, | |
| 166 | x1: number, | |
| 167 | y1: number, | |
| 168 | c: Rgb | |
| 169 | ) { | |
| 170 | for (let y = y0; y < y1; y++) { | |
| 171 | for (let x = x0; x < x1; x++) { | |
| 172 | setPixel(buf, w, x, y, c); | |
| 173 | } | |
| 174 | } | |
| 175 | } | |
| 176 | ||
| 177 | function fillCircle( | |
| 178 | buf: Uint8Array, | |
| 179 | w: number, | |
| 180 | cx: number, | |
| 181 | cy: number, | |
| 182 | r: number, | |
| 183 | c: Rgb | |
| 184 | ) { | |
| 185 | const r2 = r * r; | |
| 186 | for (let y = cy - r; y <= cy + r; y++) { | |
| 187 | for (let x = cx - r; x <= cx + r; x++) { | |
| 188 | const dx = x - cx; | |
| 189 | const dy = y - cy; | |
| 190 | if (dx * dx + dy * dy <= r2) { | |
| 191 | setPixel(buf, w, x, y, c); | |
| 192 | } | |
| 193 | } | |
| 194 | } | |
| 195 | } | |
| 196 | ||
| 197 | function ringMask(cx: number, cy: number, rOuter: number, rInner: number) { | |
| 198 | const rO2 = rOuter * rOuter; | |
| 199 | const rI2 = rInner * rInner; | |
| 200 | return (x: number, y: number) => { | |
| 201 | const dx = x - cx; | |
| 202 | const dy = y - cy; | |
| 203 | const d2 = dx * dx + dy * dy; | |
| 204 | return d2 <= rO2 && d2 >= rI2; | |
| 205 | }; | |
| 206 | } | |
| 207 | ||
| 208 | // --------------------------------------------------------------------------- | |
| 209 | // 1. icon.png — 256x256 "g" mark on dark bg, gradient ring | |
| 210 | // --------------------------------------------------------------------------- | |
| 211 | ||
| 212 | function drawIcon(): Uint8Array { | |
| 213 | const W = 256; | |
| 214 | const H = 256; | |
| 215 | const buf = makeBuf(W, H, BG); | |
| 216 | ||
| 217 | // Gradient ring: outer radius 110, inner radius 86. | |
| 218 | const cx = W / 2; | |
| 219 | const cy = H / 2; | |
| 220 | const inRing = ringMask(cx, cy, 110, 86); | |
| 221 | for (let y = 0; y < H; y++) { | |
| 222 | for (let x = 0; x < W; x++) { | |
| 223 | if (inRing(x, y)) { | |
| 224 | // gradient along x (left = start, right = end). | |
| 225 | const t = x / W; | |
| 226 | setPixel(buf, W, x, y, lerpRgb(ACCENT_START, ACCENT_END, t)); | |
| 227 | } | |
| 228 | } | |
| 229 | } | |
| 230 | ||
| 231 | // Stylised "g" — a filled disc with a notch cut on the right side and a | |
| 232 | // descender bar. Placeholder, not the real wordmark. | |
| 233 | fillCircle(buf, W, cx, cy, 60, FG); | |
| 234 | // Cut the right notch (rectangle in BG colour). | |
| 235 | fillRect(buf, W, cx + 20, cy - 18, cx + 65, cy + 8, BG); | |
| 236 | // Descender bar. | |
| 237 | fillRect(buf, W, cx + 18, cy + 8, cx + 60, cy + 24, FG); | |
| 238 | ||
| 239 | return encodePng(W, H, buf); | |
| 240 | } | |
| 241 | ||
| 242 | // --------------------------------------------------------------------------- | |
| 243 | // 2. screenshot-1.png — 1280x800 dark mockup with wordmark band | |
| 244 | // --------------------------------------------------------------------------- | |
| 245 | ||
| 246 | function drawScreenshot(): Uint8Array { | |
| 247 | const W = 1280; | |
| 248 | const H = 800; | |
| 249 | const buf = makeBuf(W, H, BG); | |
| 250 | ||
| 251 | // Top accent gradient bar (the wordmark band). | |
| 252 | for (let y = 60; y < 80; y++) { | |
| 253 | for (let x = 0; x < W; x++) { | |
| 254 | const t = x / W; | |
| 255 | setPixel(buf, W, x, y, lerpRgb(ACCENT_START, ACCENT_END, t)); | |
| 256 | } | |
| 257 | } | |
| 258 | ||
| 259 | // Mock "browser chrome" — three traffic-light circles. | |
| 260 | fillCircle(buf, W, 30, 30, 8, [0xff, 0x5f, 0x57]); | |
| 261 | fillCircle(buf, W, 52, 30, 8, [0xfe, 0xbc, 0x2e]); | |
| 262 | fillCircle(buf, W, 74, 30, 8, [0x27, 0xc9, 0x3f]); | |
| 263 | ||
| 264 | // Mock "card" — a centered panel that hints at a PR view. | |
| 265 | const px = 200; | |
| 266 | const py = 200; | |
| 267 | const pw = W - 400; | |
| 268 | const ph = H - 400; | |
| 269 | fillRect(buf, W, px, py, px + pw, py + ph, [0x16, 0x1b, 0x22]); // panel bg | |
| 270 | ||
| 271 | // Header strip on the panel | |
| 272 | fillRect(buf, W, px, py, px + pw, py + 60, [0x21, 0x26, 0x2d]); | |
| 273 | ||
| 274 | // "PR opened" accent dot (green). | |
| 275 | fillCircle(buf, W, px + 30, py + 30, 10, [0x3f, 0xb9, 0x50]); | |
| 276 | ||
| 277 | // Two faux content rows. | |
| 278 | fillRect(buf, W, px + 30, py + 100, px + pw - 30, py + 110, [0x30, 0x36, 0x3d]); | |
| 279 | fillRect(buf, W, px + 30, py + 140, px + pw - 100, py + 150, [0x30, 0x36, 0x3d]); | |
| 280 | fillRect(buf, W, px + 30, py + 180, px + pw - 200, py + 190, [0x30, 0x36, 0x3d]); | |
| 281 | ||
| 282 | return encodePng(W, H, buf); | |
| 283 | } | |
| 284 | ||
| 285 | // --------------------------------------------------------------------------- | |
| 286 | // Main | |
| 287 | // --------------------------------------------------------------------------- | |
| 288 | ||
| 289 | function main() { | |
| 290 | mkdirSync(OUT_DIR, { recursive: true }); | |
| 291 | ||
| 292 | const iconPath = join(OUT_DIR, "icon.png"); | |
| 293 | const screenshotPath = join(OUT_DIR, "screenshot-1.png"); | |
| 294 | ||
| 295 | writeFileSync(iconPath, drawIcon()); | |
| 296 | console.log(`wrote ${iconPath}`); | |
| 297 | ||
| 298 | writeFileSync(screenshotPath, drawScreenshot()); | |
| 299 | console.log(`wrote ${screenshotPath}`); | |
| 300 | } | |
| 301 | ||
| 302 | main(); |