Restructure: move Vite project into app/, deploy build output to repo root

The webserver serves the repo checkout directly as static files, so
index.html at the repo root must be the production build (pointing at
hashed /assets/*.js) rather than the dev template (/src/main.tsx),
which was being served with an unrecognized application/octet-stream
MIME type and blocked by the browser.

- Move src/, public/, scripts/, server/, package.json, vite.config.ts,
  tsconfig.json, .env(.example), and the dev index.html into app/.
- Add app/scripts/deploy.mjs: copies app/dist/* into the repo root
  after every `vite build`, cleaning up stale hashed filenames.
- Wire it into `npm run build` (app/package.json).
- Update run.sh to operate from app/, and .gitignore for the new layout.
- Commit the current build output (index.html, assets/, manifest,
  sw.js, workbox-*.js, favicon.svg, icons/, LICENSE) to the repo root.
- Update README with the new project layout and deployment model.
This commit is contained in:
oliver committed 2026-11-04 14:05:46 -03:00
1 parent 5245c0a856
commit c7b653ff6a
54 files changed
+224 -34

No files matched your search

+94
View File
@@ -0,0 +1,94 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { defineConfig, loadEnv, type Plugin } from "vite";
import react from "@vitejs/plugin-react";
import { VitePWA } from "vite-plugin-pwa";
/** Emits the repo's LICENSE file into the build output. */
function licensePlugin(): Plugin {
return {
name: "hermes-pwa-license",
apply: "build",
generateBundle() {
this.emitFile({
type: "asset",
fileName: "LICENSE",
source: readFileSync(fileURLToPath(new URL("../LICENSE", import.meta.url)), "utf-8"),
});
},
};
}
// During `npm run dev` you can point the dev server at a Hermes host to avoid
// CORS entirely: set HERMES_DEV_TARGET=https://hermes.example.com in a .env file
// (or the environment) and the dev server proxies /api/* to it.
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, ".", "");
const target = env.HERMES_DEV_TARGET;
return {
server: {
port: 5173,
proxy: target
? Object.fromEntries(
["/api", "/v1", "/health"].map((prefix) => [
prefix,
{
target,
changeOrigin: true,
secure: false,
// SSE needs buffering disabled.
configure: (proxy) => {
proxy.on("proxyRes", (proxyRes) => {
proxyRes.headers["x-accel-buffering"] = "no";
});
},
},
]),
)
: undefined,
},
plugins: [
react(),
licensePlugin(),
VitePWA({
registerType: "autoUpdate",
includeAssets: ["icons/apple-touch-icon.png", "favicon.svg"],
manifest: {
name: "Hermes",
short_name: "Hermes",
description: "Chat with your Hermes Agent profiles, sessions, voice and images.",
theme_color: "#000810",
background_color: "#000810",
display: "standalone",
orientation: "portrait-primary",
start_url: "/",
scope: "/",
icons: [
{ src: "icons/icon-192.png", sizes: "192x192", type: "image/png" },
{ src: "icons/icon-512.png", sizes: "512x512", type: "image/png" },
{
src: "icons/maskable-512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
},
workbox: {
// Never cache API traffic — it is authenticated and dynamic.
navigateFallbackDenylist: [/^\/api\//],
runtimeCaching: [
{
urlPattern: ({ url }) => url.pathname.startsWith("/api/"),
handler: "NetworkOnly",
},
],
},
devOptions: {
enabled: false,
},
}),
],
};
});