From 720bc1826b1133c6ae0f2d0d22bc2c9aa9217a25 Mon Sep 17 00:00:00 2001 From: Devarshi Shimpi Date: Tue, 2 Jun 2026 22:14:26 +0530 Subject: [PATCH 1/7] add: automated cf setup configuration script --- package-lock.json | 328 ++++++++++++++++++++-- package.json | 4 + scripts/setup-cloudflare.js | 543 ++++++++++++++++++++++++++++++++++++ 3 files changed, 854 insertions(+), 21 deletions(-) create mode 100644 scripts/setup-cloudflare.js diff --git a/package-lock.json b/package-lock.json index a9e72f9..9909c30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,9 +47,12 @@ "@vitejs/plugin-react": "^6.0.1", "@vitest/browser": "^4.1.4", "@vitest/browser-playwright": "^4.1.4", + "chalk": "^5.6.2", "concurrently": "^9.2.1", "jsdom": "^29.0.2", + "ora": "^9.4.0", "playwright": "^1.59.1", + "prompts": "^2.4.2", "tailwindcss": "^4.2.2", "typescript": "^6.0.2", "vite": "^8.0.8", @@ -3711,35 +3714,18 @@ } }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/character-entities": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", @@ -3799,6 +3785,35 @@ "url": "https://polar.sh/cva" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -3878,6 +3893,36 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -4358,6 +4403,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -4688,6 +4746,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -4707,6 +4778,19 @@ "dev": true, "license": "MIT" }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -5057,6 +5141,23 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -5956,6 +6057,19 @@ ], "license": "MIT" }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -6065,6 +6179,91 @@ ], "license": "MIT" }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.0.tgz", + "integrity": "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.2", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", @@ -6255,6 +6454,30 @@ "dev": true, "license": "MIT" }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -6640,6 +6863,23 @@ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", "license": "MIT" }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/rolldown": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", @@ -6847,6 +7087,19 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -6862,6 +7115,13 @@ "node": ">=18" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -6906,6 +7166,19 @@ "dev": true, "license": "MIT" }, + "node_modules/stdin-discarder": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -7989,6 +8262,19 @@ "node": ">=12" } }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/youch": { "version": "4.1.0-beta.10", "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", diff --git a/package.json b/package.json index 0f41312..8cb7355 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "dev:client": "vite build --watch --mode development", "dev:worker": "wrangler dev --local", "start": "npm run dev", + "setup:cloudflare": "node scripts/setup-cloudflare.js", "migrate": "node scripts/migrate.mjs", "test": "node scripts/test.mjs", "test:watch": "vitest", @@ -40,9 +41,12 @@ "@vitejs/plugin-react": "^6.0.1", "@vitest/browser": "^4.1.4", "@vitest/browser-playwright": "^4.1.4", + "chalk": "^5.6.2", "concurrently": "^9.2.1", "jsdom": "^29.0.2", + "ora": "^9.4.0", "playwright": "^1.59.1", + "prompts": "^2.4.2", "tailwindcss": "^4.2.2", "typescript": "^6.0.2", "vite": "^8.0.8", diff --git a/scripts/setup-cloudflare.js b/scripts/setup-cloudflare.js new file mode 100644 index 0000000..11dfe2a --- /dev/null +++ b/scripts/setup-cloudflare.js @@ -0,0 +1,543 @@ +import { exec, spawn } from 'node:child_process'; +import util from 'node:util'; +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; +import chalk from 'chalk'; +import ora from 'ora'; +import prompts from 'prompts'; + +const execAsync = util.promisify(exec); + +const WRANGLER_JSONC_PATH = path.join(process.cwd(), 'wrangler.jsonc'); +const DEV_VARS_PATH = path.join(process.cwd(), '.dev.vars'); + +async function runWranglerCmd(cmd, spinnerMessage) { + const spinner = ora(spinnerMessage).start(); + try { + const { stdout } = await execAsync(cmd); + spinner.succeed(); + return stdout; + } catch (error) { + spinner.fail(); + console.error(chalk.red(`\nโŒ Error executing: ${cmd}`)); + const errorMsg = error.stderr || error.message; + console.error(chalk.red(errorMsg)); + + if (errorMsg.includes('[code: 10000]') || errorMsg.includes('Authentication error')) { + console.log(chalk.yellow('\n๐Ÿ’ก Hint: Alternatively, run `npx wrangler login` to use your global Cloudflare session instead.')); + } + process.exit(1); + } +} + +function extractId(output) { + const match = output.match(/[a-f0-9]{32}/); + return match ? match[0] : null; +} + +async function handleKVNamespace(baseBinding, isPreview) { + const previewFlag = isPreview ? ' --preview' : ''; + let currentBinding = baseBinding; + + while (true) { + const spinner = ora(`Creating ${isPreview ? 'preview' : 'production'} KV namespace (${currentBinding})...`).start(); + try { + const { stdout } = await execAsync(`npx wrangler kv namespace create ${currentBinding}${previewFlag}`); + spinner.succeed(); + return extractId(stdout); + } catch (error) { + const errorMsg = error.stderr || error.message; + if (errorMsg.includes('already exists')) { + spinner.warn(`${isPreview ? 'Preview' : 'Production'} KV namespace for "${currentBinding}" already exists.`); + + const { action } = await prompts({ + type: 'select', + name: 'action', + message: `How would you like to handle this existing namespace?`, + choices: [ + { title: 'Auto-fetch existing ID', value: 'fetch' }, + { title: 'Manually enter ID', value: 'manual' }, + { title: 'Create new with different name', value: 'new' }, + { title: 'Skip', value: 'skip' } + ] + }, { onCancel: () => process.exit(1) }); + + if (action === 'fetch') { + const fetchSpinner = ora('Fetching existing KV namespaces...').start(); + try { + const { stdout: listOut } = await execAsync('npx wrangler kv namespace list'); + fetchSpinner.succeed(); + + let searchTitle = isPreview ? `${baseBinding}_preview` : baseBinding; + let parsed = null; + try { + const jsonStr = listOut.substring(listOut.indexOf('['), listOut.lastIndexOf(']') + 1); + parsed = JSON.parse(jsonStr); + } catch(e) {} + + if (parsed && Array.isArray(parsed)) { + const found = parsed.find(ns => ns.title.includes(searchTitle)); + if (found) { + console.log(chalk.green(` โœ… Found existing ID: ${found.id}`)); + return found.id; + } + } + + console.log(chalk.yellow(` โš ๏ธ Could not automatically find an ID matching ${searchTitle}.`)); + const { manualId } = await prompts({ type: 'text', name: 'manualId', message: 'Enter the KV Namespace ID manually:'}, { onCancel: () => process.exit(1) }); + if (manualId) return manualId; + return null; + } catch(e) { + fetchSpinner.fail('Failed to fetch KV namespaces.'); + const { manualId } = await prompts({ type: 'text', name: 'manualId', message: 'Enter the KV Namespace ID manually:'}, { onCancel: () => process.exit(1) }); + if (manualId) return manualId; + return null; + } + } else if (action === 'manual') { + const { manualId } = await prompts({ type: 'text', name: 'manualId', message: 'Enter the KV Namespace ID:'}, { onCancel: () => process.exit(1) }); + if (manualId) return manualId; + return null; + } else if (action === 'new') { + const { newName } = await prompts({ type: 'text', name: 'newName', message: 'Enter a new binding name (e.g. APP_KV_2):', initial: `${currentBinding}_2`}, { onCancel: () => process.exit(1) }); + if (newName) { + currentBinding = newName; + continue; + } + return null; + } else { + return null; + } + } else { + spinner.fail(); + console.error(chalk.red(`\nโŒ Error executing KV creation.`)); + console.error(chalk.red(errorMsg)); + if (errorMsg.includes('[code: 10000]') || errorMsg.includes('Authentication error')) { + console.log(chalk.yellow('\n๐Ÿ’ก Hint: Alternatively, run `npx wrangler login` to use your global Cloudflare session instead.')); + } + process.exit(1); + } + } + } +} + +async function handleHyperdrive(dbUrl) { + let currentBinding = 'codra-db'; + + while (true) { + const spinner = ora(`Creating Hyperdrive (${currentBinding})...`).start(); + try { + const { stdout } = await execAsync(`npx wrangler hyperdrive create ${currentBinding} --connection-string="${dbUrl}"`); + spinner.succeed(); + return extractId(stdout); + } catch (error) { + const errorMsg = error.stderr || error.message; + if (errorMsg.includes('already exists') || errorMsg.includes('code: 2017')) { + spinner.warn(`Hyperdrive config "${currentBinding}" already exists.`); + + const { action } = await prompts({ + type: 'select', + name: 'action', + message: `How would you like to handle this existing Hyperdrive?`, + choices: [ + { title: 'Auto-fetch existing ID', value: 'fetch' }, + { title: 'Manually enter ID', value: 'manual' }, + { title: 'Create new with different name', value: 'new' }, + { title: 'Skip', value: 'skip' } + ] + }, { onCancel: () => process.exit(1) }); + + if (action === 'fetch') { + const fetchSpinner = ora('Fetching existing Hyperdrive configs...').start(); + try { + const { stdout: listOut } = await execAsync('npx wrangler hyperdrive list'); + fetchSpinner.succeed(); + + let parsed = null; + try { + const jsonStr = listOut.substring(listOut.indexOf('['), listOut.lastIndexOf(']') + 1); + parsed = JSON.parse(jsonStr); + } catch(e) {} + + if (parsed && Array.isArray(parsed)) { + const found = parsed.find(hd => hd.name === currentBinding); + if (found) { + console.log(chalk.green(` โœ… Found existing ID: ${found.id}`)); + return found.id; + } + } else { + const lines = listOut.split('\n'); + for (const line of lines) { + if (line.includes(currentBinding)) { + const match = line.match(/[a-f0-9]{32}/); + if (match) { + console.log(chalk.green(` โœ… Found existing ID: ${match[0]}`)); + return match[0]; + } + } + } + } + + console.log(chalk.yellow(` โš ๏ธ Could not automatically find an ID matching ${currentBinding}.`)); + const { manualId } = await prompts({ type: 'text', name: 'manualId', message: 'Enter the Hyperdrive ID manually:'}, { onCancel: () => process.exit(1) }); + if (manualId) return manualId; + return null; + } catch(e) { + fetchSpinner.fail('Failed to fetch Hyperdrive configs.'); + const { manualId } = await prompts({ type: 'text', name: 'manualId', message: 'Enter the Hyperdrive ID manually:'}, { onCancel: () => process.exit(1) }); + if (manualId) return manualId; + return null; + } + } else if (action === 'manual') { + const { manualId } = await prompts({ type: 'text', name: 'manualId', message: 'Enter the Hyperdrive ID:'}, { onCancel: () => process.exit(1) }); + if (manualId) return manualId; + return null; + } else if (action === 'new') { + const { newName } = await prompts({ type: 'text', name: 'newName', message: 'Enter a new Hyperdrive name (e.g. codra-db-2):', initial: `${currentBinding}-2`}, { onCancel: () => process.exit(1) }); + if (newName) { + currentBinding = newName; + continue; + } + return null; + } else { + return null; + } + } else { + spinner.fail(); + console.error(chalk.red(`\nโŒ Error executing Hyperdrive creation.`)); + console.error(chalk.red(errorMsg)); + process.exit(1); + } + } + } +} + +function getEnvVars() { + const env = {}; + if (fs.existsSync(DEV_VARS_PATH)) { + const content = fs.readFileSync(DEV_VARS_PATH, 'utf-8'); + const lines = content.split(/\r?\n/); + for (const line of lines) { + if (line.trim() && !line.startsWith('#')) { + const [key, ...values] = line.split('='); + if (key && values.length > 0) { + env[key.trim()] = values.join('=').trim().replace(/^"|"$/g, ''); + } + } + } + } + return env; +} + +function setSecret(secretName, secretValue) { + return new Promise((resolve, reject) => { + const child = exec(`npx wrangler secret put ${secretName}`, (error, stdout, stderr) => { + if (error) reject(new Error(stderr || error.message)); + else resolve(); + }); + + child.stdin.write(secretValue); + child.stdin.end(); + }); +} + +async function main() { + console.clear(); + console.log(chalk.blue.bold('\nโ˜๏ธ Codra Cloudflare Setup\n')); + console.log(chalk.gray('This script will automatically configure your Cloudflare resources.\n')); + + const env = getEnvVars(); + + // 1. Prerequisites Check + const authSpinner = ora('Checking Cloudflare authentication...').start(); + let globallyAuthenticated = true; + try { + const { stdout, stderr } = await execAsync('npx wrangler whoami'); + const output = (stdout + (stderr || '')).toLowerCase(); + + // Wrangler sometimes exits with 0 even when not logged in + if (output.includes('not logged in') || output.includes('non-interactive environment') || output.includes('you are not authenticated')) { + throw new Error('Not logged in'); + } + authSpinner.succeed('Authenticated with Cloudflare.'); + } catch (error) { + globallyAuthenticated = false; + authSpinner.warn('Cloudflare is not authenticated in wrangler.'); + } + + if (!globallyAuthenticated) { + console.error(chalk.red('\nโŒ You are not logged into Cloudflare.')); + console.log(chalk.yellow('Please run `npx wrangler login` in your terminal and try again.')); + process.exit(1); + } + + // 2. KV Namespace + console.log(chalk.cyan.bold('๐Ÿ“ฆ KV Namespaces')); + const kvId = await handleKVNamespace('codra-review', false); + if (!kvId) console.log(chalk.yellow(' โš ๏ธ Could not extract KV ID.')); + + const kvPreviewId = await handleKVNamespace('codra-review', true); + if (!kvPreviewId) console.log(chalk.yellow(' โš ๏ธ Could not extract preview KV ID.')); + console.log(''); + + // 3. Queues + console.log(chalk.cyan.bold('๐Ÿ“จ Queues')); + const dlqSpinner = ora('Creating DLQ queue (codra-review-dlq)...').start(); + try { + await execAsync('npx wrangler queues create codra-review-dlq'); + dlqSpinner.succeed(); + } catch (e) { + if (e.stderr && (e.stderr.includes('already taken') || e.stderr.includes('already exists'))) { + dlqSpinner.succeed('DLQ queue (codra-review-dlq) already exists.'); + } else { + dlqSpinner.fail(); + console.error(chalk.yellow(' โš ๏ธ ' + (e.stderr || e.message))); + } + } + + const jobsSpinner = ora('Creating jobs queue (codra-review-jobs)...').start(); + try { + await execAsync('npx wrangler queues create codra-review-jobs'); + jobsSpinner.succeed(); + } catch (e) { + if (e.stderr && (e.stderr.includes('already taken') || e.stderr.includes('already exists'))) { + jobsSpinner.succeed('Jobs queue (codra-review-jobs) already exists.'); + } else { + jobsSpinner.fail(); + console.error(chalk.yellow(' โš ๏ธ ' + (e.stderr || e.message))); + } + } + + let dlqQueueId = null; + const queuesOutputSpinner = ora('Fetching queue information...').start(); + try { + const { stdout } = await execAsync('npx wrangler queues list'); + queuesOutputSpinner.succeed(); + const lines = stdout.split('\n'); + for (const line of lines) { + if (line.includes('codra-review-dlq')) { + dlqQueueId = extractId(line); + } + } + } catch (e) { + queuesOutputSpinner.fail('Failed to fetch queues list.'); + console.error(chalk.yellow(' โš ๏ธ Could not automatically fetch DLQ queue ID. You may need to manually update CF_DLQ_ID.')); + } + console.log(''); + + // 4. Hyperdrive + console.log(chalk.cyan.bold('๐Ÿ—„๏ธ Hyperdrive')); + console.log(chalk.gray(` (Using default from .dev.vars if available)`)); + const { dbUrl } = await prompts({ + type: 'text', + name: 'dbUrl', + message: 'Enter your Database Connection String for Hyperdrive:', + initial: env.DATABASE_URL || 'postgres://user:password@hostname:5432/codra' + }, { + onCancel: () => { + console.log(chalk.red('\n๐Ÿ›‘ Setup aborted.')); + process.exit(1); + } + }); + + if (!dbUrl) { + console.log(chalk.red('โŒ Database URL is required for Hyperdrive. Exiting.')); + process.exit(1); + } + + const hyperdriveId = await handleHyperdrive(dbUrl); + console.log(''); + + // 5. Domain Configuration + console.log(chalk.cyan.bold('๐ŸŒ Domain Configuration')); + const { domainChoice } = await prompts({ + type: 'select', + name: 'domainChoice', + message: 'Where would you like to deploy this application?', + choices: [ + { title: 'Use a workers.dev subdomain (Free & Easy)', value: 'workers_dev' }, + { title: 'Use a Custom Domain', value: 'custom_domain' } + ] + }, { onCancel: () => process.exit(1) }); + + let appUrl = ''; + let routesConfigStr = ''; + + if (domainChoice === 'workers_dev') { + routesConfigStr = `"workers_dev": true`; + const { workersDev } = await prompts({ + type: 'text', + name: 'workersDev', + message: 'What will be your workers.dev app URL? (e.g. https://codra.username.workers.dev):', + initial: 'https://codra..workers.dev' + }, { onCancel: () => process.exit(1) }); + appUrl = workersDev.replace(/\/$/, ''); + } else { + const { customDomain } = await prompts({ + type: 'text', + name: 'customDomain', + message: 'Enter your custom domain:', + initial: 'app.codra.devarshi.dev' + }, { onCancel: () => process.exit(1) }); + + appUrl = `https://${customDomain}`; + routesConfigStr = `"routes": [ + { + "pattern": "${customDomain}", + "custom_domain": true + } + ]`; + } + console.log(''); + + // 6. Application Variables + console.log(chalk.cyan.bold('๐Ÿ“ Application Variables')); + const { botUsername } = await prompts({ + type: 'text', + name: 'botUsername', + message: 'Enter your GitHub Bot Username:', + initial: 'codra-app' + }, { onCancel: () => process.exit(1) }); + + const { githubAppSlug } = await prompts({ + type: 'text', + name: 'githubAppSlug', + message: 'Enter your GitHub App Slug:', + initial: 'codra-app-personal' + }, { onCancel: () => process.exit(1) }); + + const { allowedUsers } = await prompts({ + type: 'text', + name: 'allowedUsers', + message: 'Enter comma-separated GitHub usernames allowed to access the dashboard:', + initial: 'devarshishimpi' + }, { onCancel: () => process.exit(1) }); + console.log(''); + + // 7. Config Update + console.log(chalk.cyan.bold('โš™๏ธ Configuration')); + const configSpinner = ora('Updating wrangler.jsonc...').start(); + let wranglerConfig = fs.readFileSync(WRANGLER_JSONC_PATH, 'utf-8'); + let configChanged = false; + + const routeRegex = /"routes"\s*:\s*\[[\s\S]*?\]|"workers_dev"\s*:\s*(true|false)/; + wranglerConfig = wranglerConfig.replace(routeRegex, routesConfigStr); + + const appUrlRegex = /"APP_URL":\s*"[^"]+"/; + wranglerConfig = wranglerConfig.replace(appUrlRegex, `"APP_URL": "${appUrl}"`); + + const callbackUrlRegex = /"AUTH_CALLBACK_URL":\s*"[^"]+"/; + wranglerConfig = wranglerConfig.replace(callbackUrlRegex, `"AUTH_CALLBACK_URL": "${appUrl}/auth/github/callback"`); + + const botUsernameRegex = /"BOT_USERNAME":\s*"[^"]+"/; + wranglerConfig = wranglerConfig.replace(botUsernameRegex, `"BOT_USERNAME": "${botUsername}"`); + + const githubAppSlugRegex = /"GITHUB_APP_SLUG":\s*"[^"]+"/; + wranglerConfig = wranglerConfig.replace(githubAppSlugRegex, `"GITHUB_APP_SLUG": "${githubAppSlug}"`); + + const allowedUsersRegex = /"DASHBOARD_ALLOWED_USERS":\s*"[^"]+"/; + wranglerConfig = wranglerConfig.replace(allowedUsersRegex, `"DASHBOARD_ALLOWED_USERS": "${allowedUsers}"`); + + configChanged = true; + + if (kvId && kvPreviewId) { + wranglerConfig = wranglerConfig.replace( + /"binding":\s*"APP_KV",\s*"id":\s*"[^"]+",\s*"preview_id":\s*"[^"]+"/, + `"binding": "APP_KV",${os.EOL} "id": "${kvId}",${os.EOL} "preview_id": "${kvPreviewId}"` + ); + configChanged = true; + } + + if (hyperdriveId) { + wranglerConfig = wranglerConfig.replace( + /"binding":\s*"HYPERDRIVE",\s*"id":\s*"[^"]+"/, + `"binding": "HYPERDRIVE",${os.EOL} "id": "${hyperdriveId}"` + ); + configChanged = true; + } + + if (dlqQueueId) { + wranglerConfig = wranglerConfig.replace( + /"CF_DLQ_ID":\s*"[^"]+"/, + `"CF_DLQ_ID": "${dlqQueueId}"` + ); + configChanged = true; + } + + if (configChanged) { + fs.writeFileSync(WRANGLER_JSONC_PATH, wranglerConfig, 'utf-8'); + configSpinner.succeed('Updated wrangler.jsonc with new resource IDs.'); + } else { + configSpinner.warn('No IDs were successfully extracted. wrangler.jsonc was not modified.'); + } + console.log(''); + + // 8. Secrets + console.log(chalk.cyan.bold('๐Ÿ” Secrets')); + const requiredSecrets = [ + "APP_PRIVATE_KEY", + "GITHUB_APP_ID", + "GITHUB_APP_WEBHOOK_SECRET", + "GITHUB_CLIENT_ID", + "GITHUB_CLIENT_SECRET", + "LLM_CONFIG_ENCRYPTION_KEY", + "CF_API_TOKEN", + "CF_ACCOUNT_ID" + ]; + + const { confirmSecrets } = await prompts({ + type: 'confirm', + name: 'confirmSecrets', + message: 'Would you like to interactively configure the required Cloudflare secrets now?', + initial: true + }, { + onCancel: () => { + console.log(chalk.red('\n๐Ÿ›‘ Setup aborted.')); + process.exit(1); + } + }); + + if (confirmSecrets) { + console.log(''); + for (const secretName of requiredSecrets) { + let initialVal = env[secretName] || ''; + + const { secretValue } = await prompts({ + type: 'text', + name: 'secretValue', + message: `Value for ${secretName}:`, + initial: initialVal || undefined, + style: secretName === 'APP_PRIVATE_KEY' ? 'default' : 'password' + }, { + onCancel: () => { + console.log(chalk.red('\n๐Ÿ›‘ Setup aborted.')); + process.exit(1); + } + }); + + if (secretValue) { + const spinner = ora(`Setting secret ${secretName}...`).start(); + try { + await setSecret(secretName, secretValue); + spinner.succeed(); + } catch (e) { + spinner.fail(); + console.error(chalk.red(` โŒ Failed to set secret ${secretName}: ${e.message}`)); + } + } else { + console.log(chalk.yellow(` โญ๏ธ Skipped ${secretName}`)); + } + } + } + + console.log(chalk.green.bold('\n=============================================')); + console.log(chalk.green.bold('๐ŸŽ‰ Cloudflare Setup Successfully Completed!')); + console.log(chalk.green.bold('=============================================\n')); + console.log(chalk.white('You are all set. Run ') + chalk.cyan('npm run deploy') + chalk.white(' to deploy Codra to Cloudflare.\n')); +} + +main().catch(error => { + console.error(chalk.red('\nโŒ An unexpected error occurred:')); + console.error(error); + process.exit(1); +}); From 6d361b58224fd8fec02258e3e29cf80aa6823d7f Mon Sep 17 00:00:00 2001 From: Devarshi Shimpi Date: Sun, 7 Jun 2026 13:14:53 +0530 Subject: [PATCH 2/7] add: redesign UI with updated design system and version display --- README.md | 2 + src/client/app.css | 51 +++++++++++--------- src/client/components/layout/page-header.tsx | 8 ++- src/client/components/shared/jobs-table.tsx | 16 +++--- src/client/components/ui/button.tsx | 2 +- src/client/components/ui/dropdown-menu.tsx | 4 +- src/client/components/ui/input.tsx | 2 +- src/client/components/ui/select.tsx | 15 ++++-- src/client/pages/settings.tsx | 50 ++++++++++++++++++- 9 files changed, 109 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 411c3bb..19bb877 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ Codra listens to GitHub pull request events, runs AI-powered review jobs, posts inline findings back to the PR, and gives you a dashboard to inspect jobs, repositories, model routing, review history, and failed queue runs. +> **Beta** -- Codra is under active development. Expect rough edges, missing features, and breaking changes between releases. Feedback and bug reports are welcome via [GitHub Issues](https://github.com/devarshishimpi/codra/issues). + ## Why Codra - **Own the whole review loop**: Run the GitHub App, Cloudflare Worker, queue, database, model credentials, and dashboard under your own control. diff --git a/src/client/app.css b/src/client/app.css index a09fe98..b1677d2 100644 --- a/src/client/app.css +++ b/src/client/app.css @@ -16,7 +16,7 @@ :root { /* Surfaces - Pure & Crisp */ /* Surfaces - High-Contrast */ - --background: #ffffff; + --background: #f4f4f5; --foreground: oklch(12% 0.02 115); --card: #ffffff; --card-foreground: oklch(12% 0.02 115); @@ -27,27 +27,27 @@ --primary: oklch(64% 0.24 115); --primary-foreground: oklch(100% 0 0); /* White text on the deeper green */ - /* Secondary / muted - Subtly cool */ - --secondary: oklch(96% 0.006 115); - --secondary-foreground:oklch(25% 0.020 115); - --muted: oklch(96% 0.006 115); - --muted-foreground: oklch(44% 0.015 115); + /* Secondary / muted - Zinc */ + --secondary: #f4f4f5; + --secondary-foreground:#27272a; + --muted: #f4f4f5; + --muted-foreground: #71717a; - /* Accent */ - --accent: oklch(94% 0.03 115); - --accent-foreground: oklch(18% 0.016 115); + /* Accent - slightly darker zinc for visible hover on white popovers */ + --accent: #e4e4e7; + --accent-foreground: #18181b; /* Destructive */ --destructive: oklch(55% 0.22 25); --destructive-foreground: oklch(100% 0 0); /* Border / input / ring */ - --border: oklch(88% 0.008 115); - --input: oklch(88% 0.008 115); + --border: #e4e4e7; + --input: #e4e4e7; --ring: oklch(72% 0.22 115); /* Radius */ - --radius: 0.5rem; + --radius: 0.75rem; --sidebar-width: 240px; --sidebar-collapsed-width: 72px; @@ -66,9 +66,9 @@ --info-border: oklch(88% 0.12 250); /* Premium Shadows */ - --shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.05); - --shadow-md: 0 4px 12px oklch(0% 0 0 / 0.06), 0 1px 4px oklch(0% 0 0 / 0.03); - --shadow-lg: 0 12px 24px -4px oklch(0% 0 0 / 0.08), 0 4px 12px -2px oklch(0% 0 0 / 0.04); + --shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.02); + --shadow-md: 0 1px 4px oklch(0% 0 0 / 0.03), 0 1px 2px oklch(0% 0 0 / 0.02); + --shadow-lg: 0 4px 16px -4px oklch(0% 0 0 / 0.04), 0 1px 6px -2px oklch(0% 0 0 / 0.03); /* Code Blocks (Zinc) */ --code-bg: #f4f4f5; @@ -173,10 +173,10 @@ --color-info-bg: var(--info-bg); --color-info-border: var(--info-border); - --radius-sm: 2px; - --radius-md: 4px; - --radius-lg: 8px; - --radius-xl: 16px; + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 18px; --radius-2xl: 32px; /* Fluid Typography Scale (Ratio: 1.25) */ @@ -495,9 +495,8 @@ Surface & Utilities โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ @utility surface { - @apply bg-card border border-border rounded-md; + @apply bg-card border border-border rounded-xl; box-shadow: var(--shadow-md); - &:hover { box-shadow: var(--shadow-md); } } .surface-static { @@ -657,11 +656,13 @@ } .app-shell-content { - --background: #ffffff; + --background: #f4f4f5; --card: #ffffff; - --muted: #ffffff; + --muted: #f4f4f5; --popover: #ffffff; - --secondary: #ffffff; + --secondary: #f4f4f5; + --border: #e4e4e7; + --input: #e4e4e7; } .dark .app-shell-content { @@ -670,6 +671,8 @@ --muted: #09090b; --popover: #09090b; --secondary: #09090b; + --border: oklch(22% 0.02 115); + --input: oklch(22% 0.02 115); } .dashboard-sidebar-divider { diff --git a/src/client/components/layout/page-header.tsx b/src/client/components/layout/page-header.tsx index 026c765..3590e19 100644 --- a/src/client/components/layout/page-header.tsx +++ b/src/client/components/layout/page-header.tsx @@ -7,6 +7,7 @@ interface PageHeaderProps extends React.HTMLAttributes { title: string; description?: React.ReactNode; actions?: React.ReactNode; + versionBadge?: string; } export function PageHeader({ @@ -28,10 +29,15 @@ export function PageHeader({ {category}

{title} + {props.versionBadge && ( + + v{props.versionBadge} + + )}

{description && (
diff --git a/src/client/components/shared/jobs-table.tsx b/src/client/components/shared/jobs-table.tsx index 2bd99be..f135ccc 100644 --- a/src/client/components/shared/jobs-table.tsx +++ b/src/client/components/shared/jobs-table.tsx @@ -40,8 +40,8 @@ const thCls = 'px-4 py-3 text-left text-[10px] font-bold uppercase tracking-[0.16em] text-muted-foreground select-none'; const COLUMN_CLASSES: Record = { - repo: 'w-[190px]', - pr: 'min-w-[280px]', + repo: 'w-[190px] max-w-[190px]', + pr: 'max-w-[480px]', status: 'w-[150px]', verdict: 'w-[120px]', files: 'hidden md:table-cell w-[76px]', @@ -262,7 +262,7 @@ export function JobsTable({ jobs, loading, columns }: JobsTableProps) { return ( {cols.includes('repo') && ( -
+
-
-
+
+
#{job.prNumber} {job.prTitle ?? 'Untitled PR'} diff --git a/src/client/components/ui/button.tsx b/src/client/components/ui/button.tsx index 9d79858..2ae9a8d 100644 --- a/src/client/components/ui/button.tsx +++ b/src/client/components/ui/button.tsx @@ -13,7 +13,7 @@ const buttonVariants = cva( destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', outline: - 'border border-input bg-card/60 shadow-sm hover:bg-secondary hover:text-secondary-foreground', + 'border border-zinc-200 bg-white shadow-sm hover:bg-zinc-50 hover:text-zinc-900 dark:border-input dark:bg-card/60 dark:hover:bg-secondary dark:hover:text-secondary-foreground', secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', ghost: 'hover:bg-secondary hover:text-secondary-foreground', diff --git a/src/client/components/ui/dropdown-menu.tsx b/src/client/components/ui/dropdown-menu.tsx index df58e9a..32c61fc 100644 --- a/src/client/components/ui/dropdown-menu.tsx +++ b/src/client/components/ui/dropdown-menu.tsx @@ -16,10 +16,10 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub; const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; const menuContentClass = - 'z-50 min-w-[8rem] overflow-hidden rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-xl shadow-black/10 dark:shadow-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 duration-200'; + 'z-50 min-w-[8rem] overflow-hidden rounded-lg border border-zinc-200 bg-white p-1 text-zinc-900 shadow-lg shadow-black/[0.06] dark:border-border dark:bg-popover dark:text-popover-foreground dark:shadow-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 duration-200'; const menuItemClass = - 'relative flex cursor-default select-none items-center rounded-md text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground dark:hover:bg-primary/[0.12] dark:focus:bg-primary/[0.12] dark:data-[highlighted]:bg-primary/[0.12] data-[disabled]:pointer-events-none data-[disabled]:opacity-50'; + 'relative flex cursor-default select-none items-center rounded-md text-sm outline-none transition-colors hover:bg-zinc-200 hover:text-zinc-900 focus:bg-zinc-200 focus:text-zinc-900 data-[highlighted]:bg-zinc-200 data-[highlighted]:text-zinc-900 dark:hover:bg-primary/[0.12] dark:hover:text-foreground dark:focus:bg-primary/[0.12] dark:focus:text-foreground dark:data-[highlighted]:bg-primary/[0.12] dark:data-[highlighted]:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50'; const DropdownMenuSubTrigger = React.forwardRef< React.ElementRef, diff --git a/src/client/components/ui/input.tsx b/src/client/components/ui/input.tsx index 345dcbc..1970d23 100644 --- a/src/client/components/ui/input.tsx +++ b/src/client/components/ui/input.tsx @@ -8,7 +8,7 @@ const Input = React.forwardRef(({ className, type, @@ -77,7 +85,8 @@ export function Select({ onClick={() => onValueChange(option.value)} className={cn( 'cursor-pointer whitespace-normal break-words py-2', - value === option.value && 'bg-primary/10 font-medium text-primary dark:bg-primary/[0.12]' + value === option.value && + 'bg-primary/10 font-medium text-primary dark:bg-primary/[0.12] dark:text-primary', )} > {option.label} diff --git a/src/client/pages/settings.tsx b/src/client/pages/settings.tsx index cde3db8..371b1ca 100644 --- a/src/client/pages/settings.tsx +++ b/src/client/pages/settings.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; +import pkg from '../../../package.json'; import { toast } from 'sonner'; import { api, type ProviderPayload } from '@client/lib/api'; import { PageHeader } from '@client/components/layout/page-header'; @@ -21,6 +22,9 @@ import { ChevronDown, ChevronRight, X, + Tag, + ExternalLink, + GitCommit, } from 'lucide-react'; import type { LlmApiFormat, LlmProvider, ModelConfig, RepoConfig } from '@shared/schema'; import type { ModelConfigsResponse } from '@shared/api'; @@ -1142,7 +1146,7 @@ export function SettingsPage() { ))}
) : ( -
+
{filteredConfigs.map((cfg) => { const saved = savedConfigs.find(item => item.modelId === cfg.modelId); const dirty = !configEqual(cfg, saved); @@ -1280,6 +1284,50 @@ export function SettingsPage() {
)} + + {/* โ”€โ”€ System Information โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} + +
+ + {/* Version row */} +
+ + + +
+

Version

+

Installed Codra release

+
+ + v{pkg.version} + +
+ + {/* Changelog / links row */} +
+ + + +
+

Releases

+

Browse all versions and release notes on GitHub

+
+ + View releases + + +
+ +
+
); } From b5361fd07f5e3deb971e1350962f69d2f5c96d1d Mon Sep 17 00:00:00 2001 From: Devarshi Shimpi Date: Sun, 7 Jun 2026 21:21:08 +0530 Subject: [PATCH 3/7] refactor: lazy load pages + redesign auth pages - Version 0.9.2 - Implement code splitting with React.lazy for all pages - Add Suspense fallbacks for async route components - Redesign landing page (hero, sign-in, feature layout) - Redesign login page (cleaner UI, security note) - Minor app-shell spacing adjustment --- package.json | 2 +- src/client/components/layout/app-shell.tsx | 1 + src/client/main.tsx | 50 +++++---- src/client/pages/landing.tsx | 120 ++++++++++++--------- src/client/pages/login.tsx | 75 ++++++------- 5 files changed, 137 insertions(+), 111 deletions(-) diff --git a/package.json b/package.json index 8cb7355..0210a1f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codra", - "version": "0.9.0", + "version": "0.9.2", "description": "Open-source code review engine", "author": "Devarshi Shimpi", "license": "AGPL-3.0-only", diff --git a/src/client/components/layout/app-shell.tsx b/src/client/components/layout/app-shell.tsx index 18f99a8..f467b26 100644 --- a/src/client/components/layout/app-shell.tsx +++ b/src/client/components/layout/app-shell.tsx @@ -146,6 +146,7 @@ export function AppShell() { className={cn( 'flex min-w-0 items-center gap-2.5 rounded-lg p-1 -m-1', 'transition-opacity duration-150 hover:opacity-75', + !sidebarCollapsed && 'lg:ml-1.5', sidebarCollapsed && 'lg:justify-center', )} aria-label="Codra dashboard" diff --git a/src/client/main.tsx b/src/client/main.tsx index 95b7dbf..7e40f75 100644 --- a/src/client/main.tsx +++ b/src/client/main.tsx @@ -1,18 +1,20 @@ -import React from 'react'; +import React, { Suspense } from 'react'; import ReactDOM from 'react-dom/client'; import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import { Toaster } from 'sonner'; import { AppShell } from './components/layout/app-shell'; -import { LandingPage } from './pages/landing'; -import { DashboardPage } from './pages/dashboard'; -import { LoginPage } from './pages/login'; -import { JobsPage } from './pages/jobs'; -import { JobDetailPage } from './pages/job-detail'; -import { JobLogsPage } from './pages/job-logs'; -import { ReposPage } from './pages/repos'; -import { StatsPage } from './pages/stats'; -import { SettingsPage } from './pages/settings'; -import { NotFoundPage } from './pages/not-found'; + +const LandingPage = React.lazy(() => import('./pages/landing').then(m => ({ default: m.LandingPage }))); +const DashboardPage = React.lazy(() => import('./pages/dashboard').then(m => ({ default: m.DashboardPage }))); +const LoginPage = React.lazy(() => import('./pages/login').then(m => ({ default: m.LoginPage }))); +const JobsPage = React.lazy(() => import('./pages/jobs').then(m => ({ default: m.JobsPage }))); +const JobDetailPage = React.lazy(() => import('./pages/job-detail').then(m => ({ default: m.JobDetailPage }))); +const JobLogsPage = React.lazy(() => import('./pages/job-logs').then(m => ({ default: m.JobLogsPage }))); +const ReposPage = React.lazy(() => import('./pages/repos').then(m => ({ default: m.ReposPage }))); +const StatsPage = React.lazy(() => import('./pages/stats').then(m => ({ default: m.StatsPage }))); +const SettingsPage = React.lazy(() => import('./pages/settings').then(m => ({ default: m.SettingsPage }))); +const NotFoundPage = React.lazy(() => import('./pages/not-found').then(m => ({ default: m.NotFoundPage }))); + import './app.css'; import { ThemeProvider } from './lib/theme'; @@ -49,30 +51,36 @@ function ToasterWrapper() { ); } +const withSuspense = (Component: React.ComponentType, isFullPage = false) => ( + }> + + +); + const router = createBrowserRouter([ { path: '/', - element: , + element: withSuspense(LandingPage, true), }, { path: '/login', - element: , + element: withSuspense(LoginPage, true), }, { element: , children: [ - { path: 'dashboard', element: }, - { path: 'jobs', element: }, - { path: 'jobs/:id', element: }, - { path: 'jobs/:id/logs', element: }, - { path: 'repos', element: }, - { path: 'stats', element: }, - { path: 'settings', element: }, + { path: 'dashboard', element: withSuspense(DashboardPage) }, + { path: 'jobs', element: withSuspense(JobsPage) }, + { path: 'jobs/:id', element: withSuspense(JobDetailPage) }, + { path: 'jobs/:id/logs', element: withSuspense(JobLogsPage) }, + { path: 'repos', element: withSuspense(ReposPage) }, + { path: 'stats', element: withSuspense(StatsPage) }, + { path: 'settings', element: withSuspense(SettingsPage) }, ], }, { path: '*', - element: , + element: withSuspense(NotFoundPage, true), }, ]); diff --git a/src/client/pages/landing.tsx b/src/client/pages/landing.tsx index 86d4233..cb5fb86 100644 --- a/src/client/pages/landing.tsx +++ b/src/client/pages/landing.tsx @@ -3,63 +3,91 @@ import { useTheme } from '@client/lib/theme'; import codraDark from '@/assets/codra-fullicon-dark.svg'; import codraLight from '@/assets/codra-fullicon-light.svg'; +const FEATURES = [ + { + title: 'Understands your codebase', + desc: 'Reviews diffs with full context from the surrounding code, not just the changed lines.', + }, + { + title: 'Flags real issues', + desc: 'Security vulnerabilities, logic errors, and pattern violations โ€” surfaced before merge.', + }, + { + title: 'Configurable per repo', + desc: 'Set review depth, model chain, and strictness from the dashboard. No config files.', + }, +]; + export function LandingPage() { const { theme, toggleTheme } = useTheme(); return (
- {/* Header */} -
+ {/* โ”€โ”€ Header โ”€โ”€ */} +
Codra - +
+ + Sign in + + + +
- {/* Body */} -
+ {/* โ”€โ”€ Body โ”€โ”€ */} +
- {/* Left โ€” Identity & CTA */} -
+ {/* Left โ€” Hero */} +
-
-
-

- AI code review
- on every PR. +
+ {/* Badge */} + + AI-powered ยท GitHub App + + +
+

+ AI code review
on every PR.

-

- Codra reviews pull requests automatically โ€” checking for bugs, security issues, - and code patterns specific to your repository. +

+ Codra reviews pull requests automatically โ€” checking for bugs, + security issues, and code patterns specific to your repository.

-
{/* Footer links */} - - {/* Right โ€” What it does */} -
-
- {[ - { - title: 'Understands your codebase', - desc: 'Reviews diffs with context from the surrounding code, not just the changed lines.', - }, - { - title: 'Flags real issues', - desc: 'Security vulnerabilities, logic errors, and pattern violations โ€” surfaced before merge.', - }, - { - title: 'Configurable per repo', - desc: 'Set review depth, model chain, and strictness from the dashboard. No config files.', - }, - ].map((item) => ( -
-

{item.title}

-

{item.desc}

+ {/* Right โ€” Features */} +
+

+ What it does +

+ +
+ {FEATURES.map((item, i) => ( +
+ + {i + 1} + +
+

{item.title}

+

{item.desc}

+
))}
diff --git a/src/client/pages/login.tsx b/src/client/pages/login.tsx index 0300777..e94937c 100644 --- a/src/client/pages/login.tsx +++ b/src/client/pages/login.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { Button } from '@client/components/ui/button'; -import { Sun, Moon } from 'lucide-react'; +import { Sun, Moon, ShieldCheck } from 'lucide-react'; import { useTheme } from '@client/lib/theme'; import codraDark from '@/assets/codra-fullicon-dark.svg'; import codraLight from '@/assets/codra-fullicon-light.svg'; @@ -29,7 +29,7 @@ export function LoginPage() { const error = useMemo(() => getErrorMessage(searchParams.get('error')), [searchParams]); return ( -
+
-