From adbfab5c2596259fb5d641e5d582aba461fc2286 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Thu, 22 Jan 2026 16:16:36 +0000 Subject: [PATCH] feat(cloudflare): worker-hosted version.json for UI updates --- packages/cloudflare/.gitignore | 1 + packages/cloudflare/package.json | 14 ++++ packages/cloudflare/release-config.json | 4 + .../cloudflare/scripts/build-manifest.mjs | 75 +++++++++++++++++++ packages/cloudflare/scripts/release-ui.mjs | 73 ++++++++++++++++++ packages/cloudflare/src/index.ts | 29 +++++++ packages/cloudflare/wrangler.toml | 9 +++ 7 files changed, 205 insertions(+) create mode 100644 packages/cloudflare/.gitignore create mode 100644 packages/cloudflare/package.json create mode 100644 packages/cloudflare/release-config.json create mode 100644 packages/cloudflare/scripts/build-manifest.mjs create mode 100644 packages/cloudflare/scripts/release-ui.mjs create mode 100644 packages/cloudflare/src/index.ts create mode 100644 packages/cloudflare/wrangler.toml diff --git a/packages/cloudflare/.gitignore b/packages/cloudflare/.gitignore new file mode 100644 index 00000000..849ddff3 --- /dev/null +++ b/packages/cloudflare/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json new file mode 100644 index 00000000..e678bc94 --- /dev/null +++ b/packages/cloudflare/package.json @@ -0,0 +1,14 @@ +{ + "name": "@codenomad/ui-host-worker", + "private": true, + "type": "module", + "scripts": { + "build:manifest": "node ./scripts/build-manifest.mjs", + "release:ui": "node ./scripts/release-ui.mjs", + "dev": "wrangler dev", + "deploy": "wrangler deploy" + }, + "devDependencies": { + "wrangler": "^4.0.0" + } +} diff --git a/packages/cloudflare/release-config.json b/packages/cloudflare/release-config.json new file mode 100644 index 00000000..4e3b324a --- /dev/null +++ b/packages/cloudflare/release-config.json @@ -0,0 +1,4 @@ +{ + "minServerVersion": "0.7.5", + "latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest" +} diff --git a/packages/cloudflare/scripts/build-manifest.mjs b/packages/cloudflare/scripts/build-manifest.mjs new file mode 100644 index 00000000..582bc34c --- /dev/null +++ b/packages/cloudflare/scripts/build-manifest.mjs @@ -0,0 +1,75 @@ +import { createHash } from "crypto" +import fs from "fs" +import path from "path" +import { fileURLToPath } from "url" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const root = path.resolve(__dirname, "..") +const repoRoot = path.resolve(root, "..", "..") + +const releaseConfigPath = path.join(root, "release-config.json") +const uiPackageJsonPath = path.join(repoRoot, "packages/ui/package.json") +const serverPackageJsonPath = path.join(repoRoot, "packages/server/package.json") + +const distDir = path.join(root, "dist") +const manifestPath = path.join(distDir, "version.json") + +const args = new Set(process.argv.slice(2)) + +function getArgValue(flag) { + const idx = process.argv.indexOf(flag) + if (idx === -1) return null + return process.argv[idx + 1] ?? null +} + +const zipPath = getArgValue("--zip") + +if (!zipPath) { + console.error("Usage: node scripts/build-manifest.mjs --zip ") + process.exit(1) +} + +const resolvedZipPath = path.resolve(process.cwd(), zipPath) +if (!fs.existsSync(resolvedZipPath)) { + console.error(`Zip not found: ${resolvedZipPath}`) + process.exit(1) +} + +const releaseConfig = JSON.parse(fs.readFileSync(releaseConfigPath, "utf-8")) +const uiPackageJson = JSON.parse(fs.readFileSync(uiPackageJsonPath, "utf-8")) +const serverPackageJson = JSON.parse(fs.readFileSync(serverPackageJsonPath, "utf-8")) + +const bucket = process.env.CODENOMAD_R2_BUCKET + +if (!bucket) { + console.error("Missing env var: CODENOMAD_R2_BUCKET") + process.exit(1) +} + +const uiVersion = uiPackageJson.version +const serverVersion = serverPackageJson.version + +if (!uiVersion || !serverVersion) { + console.error("Missing version fields in package.json") + process.exit(1) +} + +const sha256 = createHash("sha256").update(fs.readFileSync(resolvedZipPath)).digest("hex") + +const uiPackageURL = `https://download.codenomad.neuralnomads.ai/ui/ui-${uiVersion}.zip` + +const manifest = { + minServerVersion: releaseConfig.minServerVersion, + latestUIVersion: uiVersion, + uiPackageURL, + sha256, + latestServerVersion: serverVersion, + latestServerUrl: releaseConfig.latestServerUrl, +} + +fs.mkdirSync(distDir, { recursive: true }) +fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8") + +console.log(`Wrote ${manifestPath}`) diff --git a/packages/cloudflare/scripts/release-ui.mjs b/packages/cloudflare/scripts/release-ui.mjs new file mode 100644 index 00000000..24de80e5 --- /dev/null +++ b/packages/cloudflare/scripts/release-ui.mjs @@ -0,0 +1,73 @@ +import { execFileSync } from "child_process" +import fs from "fs" +import os from "os" +import path from "path" +import { fileURLToPath } from "url" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const root = path.resolve(__dirname, "..") +const repoRoot = path.resolve(root, "..", "..") + +const r2Bucket = process.env.CODENOMAD_R2_BUCKET + +if (!r2Bucket) { + console.error("Missing env var: CODENOMAD_R2_BUCKET") + process.exit(1) +} + +const uiPackageJsonPath = path.join(repoRoot, "packages/ui/package.json") +const uiPackageJson = JSON.parse(fs.readFileSync(uiPackageJsonPath, "utf-8")) +const uiVersion = uiPackageJson.version + +if (!uiVersion) { + console.error("Missing packages/ui/package.json version") + process.exit(1) +} + +const uiBuildDir = path.join(repoRoot, "packages/ui/src/renderer/dist") +if (!fs.existsSync(uiBuildDir)) { + console.error(`Missing UI build dir: ${uiBuildDir}. Run UI build first.`) + process.exit(1) +} + +const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "codenomad-ui-release-")) +const zipPath = path.join(tmpDir, `ui-${uiVersion}.zip`) + +try { + // Zip the CONTENTS of the dist dir (so index.html is at zip root). + execFileSync("/usr/bin/zip", ["-q", "-r", zipPath, "."], { cwd: uiBuildDir, stdio: "inherit" }) + + // Upload to R2. + const objectKey = `ui/ui-${uiVersion}.zip` + console.log(`[release-ui] Uploading ${zipPath} -> r2://${r2Bucket}/${objectKey}`) + + execFileSync( + "npx", + ["wrangler", "r2", "object", "put", r2Bucket, objectKey, "--file", zipPath], + { cwd: root, stdio: "inherit" }, + ) + + // Generate version.json into packages/cloudflare/dist + console.log("[release-ui] Generating version.json") + execFileSync( + process.execPath, + [path.join(root, "scripts/build-manifest.mjs"), "--zip", zipPath], + { + cwd: root, + stdio: "inherit", + env: { + ...process.env, + CODENOMAD_R2_BUCKET: r2Bucket, + }, + }, + ) + + console.log("[release-ui] Deploying worker") + execFileSync("npx", ["wrangler", "deploy"], { cwd: root, stdio: "inherit" }) + + console.log("[release-ui] Done") +} finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) +} diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts new file mode 100644 index 00000000..46831ae9 --- /dev/null +++ b/packages/cloudflare/src/index.ts @@ -0,0 +1,29 @@ +export interface Env { + ASSETS: { fetch: (request: Request) => Promise } +} + +function withHeader(response: Response, key: string, value: string): Response { + const headers = new Headers(response.headers) + headers.set(key, value) + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }) +} + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url) + + if (url.pathname === "/version.json") { + const assetResponse = await env.ASSETS.fetch(request) + + // Ensure this stays fresh; the server uses it on startup. + const withCache = withHeader(assetResponse, "Cache-Control", "no-cache") + return withHeader(withCache, "Content-Type", "application/json; charset=utf-8") + } + + return new Response("Not found", { status: 404 }) + }, +} diff --git a/packages/cloudflare/wrangler.toml b/packages/cloudflare/wrangler.toml new file mode 100644 index 00000000..fe2d50ac --- /dev/null +++ b/packages/cloudflare/wrangler.toml @@ -0,0 +1,9 @@ +name = "codenomad-ui-host" +main = "src/index.ts" +compatibility_date = "2026-01-22" + +[assets] +directory = "./dist" +binding = "ASSETS" +not_found_handling = "404-page" +run_worker_first = ["/version.json"]