diff --git a/.env.example b/.env.example index 13aa6051..f5fed3a1 100644 --- a/.env.example +++ b/.env.example @@ -26,6 +26,11 @@ SLACK_SIGNING_SECRET= # Context folder (to change for your setup) NAO_DEFAULT_PROJECT_PATH=/Users/blef/Work/naolabs/chat/example +# Build metadata (optional) +APP_VERSION=dev +APP_COMMIT=unknown +APP_BUILD_DATE= + # SMTP server Configuration SMTP_HOST= # smtp.yourservice.com SMTP_SSL=false diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 84aea570..b386cae0 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -59,6 +59,9 @@ jobs: cache-to: type=gha,mode=max build-args: | GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} + APP_VERSION=${{ steps.sha.outputs.short_sha }} + APP_COMMIT=${{ github.sha }} + APP_BUILD_DATE=${{ github.event.head_commit.timestamp }} - name: Update Docker Hub description if: github.event_name != 'pull_request' diff --git a/Dockerfile b/Dockerfile index 1b27423d..81c1ca68 100644 --- a/Dockerfile +++ b/Dockerfile @@ -73,6 +73,10 @@ RUN uv pip install --system . # ============================================================================= FROM python:3.12-slim AS runtime +ARG APP_VERSION=dev +ARG APP_COMMIT=unknown +ARG APP_BUILD_DATE= + # Install Node.js, Bun, git, and supervisor RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ @@ -128,6 +132,9 @@ ENV MODE=prod ENV NODE_ENV=production ENV BETTER_AUTH_URL=http://localhost:5005 ENV FASTAPI_PORT=8005 +ENV APP_VERSION=$APP_VERSION +ENV APP_COMMIT=$APP_COMMIT +ENV APP_BUILD_DATE=$APP_BUILD_DATE ENV NAO_DEFAULT_PROJECT_PATH=/app/example ENV NAO_CONTEXT_SOURCE=local diff --git a/README.md b/README.md index 6ce5cae4..a272e9ab 100644 --- a/README.md +++ b/README.md @@ -170,8 +170,6 @@ docker run -d \ -e BETTER_AUTH_URL=http://localhost:5005 \ -v /path/to/your/nao-project:/app/project \ -e NAO_DEFAULT_PROJECT_PATH=/app/project \ - getnao/nao:latest -``` Access the UI at http://localhost:5005 @@ -216,3 +214,4 @@ nao Labs is a proud Y Combinator company! ## 📄 License This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details. +``` diff --git a/apps/backend/package.json b/apps/backend/package.json index 447410c5..6c911968 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -16,7 +16,7 @@ "build": "tsup src/index.ts --format esm --target node20 --minify", "build:standalone": "bun build src/cli.ts --compile --outfile nao-chat-server", "start": "node dist/index.js", - "test": "vitest run", + "test": "vitest --config ./vitest.config.ts run", "lint": "tsc --noEmit && eslint", "lint:fix": "eslint --fix", "db:generate": "bash scripts/db.generate.sh", @@ -28,7 +28,7 @@ "db:reset": "rm db.sqlite", "db:check-migrations": "bun scripts/db.check-migrations.ts", "format": "prettier --write .", - "test:tool-outputs": "PRINT_OUTPUT=true vitest run tests/tool-outputs/" + "test:tool-outputs": "PRINT_OUTPUT=true vitest --config ../frontend/vite.config.ts run tests/tool-outputs/" }, "dependencies": { "@ai-sdk/anthropic": "^3.0.15", diff --git a/apps/backend/src/agents/tools/execute-sql.ts b/apps/backend/src/agents/tools/execute-sql.ts index be918ce8..3099abe6 100644 --- a/apps/backend/src/agents/tools/execute-sql.ts +++ b/apps/backend/src/agents/tools/execute-sql.ts @@ -19,8 +19,9 @@ export default tool({ export async function executeQuery({ sql_query, database_id }: executeSql.Input): Promise { const naoProjectFolder = getProjectFolder(); + const executeSqlUrl = new URL('/execute_sql', `http://127.0.0.1:${env.FASTAPI_PORT}`).toString(); - const response = await fetch(`http://localhost:${env.FASTAPI_PORT}/execute_sql`, { + const response = await fetch(executeSqlUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/apps/backend/src/env.ts b/apps/backend/src/env.ts index 9b010ef5..eaefbf0a 100644 --- a/apps/backend/src/env.ts +++ b/apps/backend/src/env.ts @@ -33,6 +33,10 @@ const envSchema = z.object({ FASTAPI_PORT: z.coerce.number().default(8005), + APP_VERSION: z.string().default('dev'), + APP_COMMIT: z.string().default('unknown'), + APP_BUILD_DATE: z.string().default(''), + NAO_DEFAULT_PROJECT_PATH: z.string().optional(), MCP_JSON_FILE_PATH: z.string().optional(), diff --git a/apps/backend/src/trpc/google.routes.ts b/apps/backend/src/trpc/google.routes.ts index cd18c9d7..b4bc90ef 100644 --- a/apps/backend/src/trpc/google.routes.ts +++ b/apps/backend/src/trpc/google.routes.ts @@ -22,12 +22,12 @@ export const googleRoutes = { authDomains: z.string(), }), ) - .mutation(({ input }) => { + .mutation(({ input: _input }) => { //TO DO : Save google settings in a secure store or database - // process.env.GOOGLE_CLIENT_ID = input.clientId; - // process.env.GOOGLE_CLIENT_SECRET = input.clientSecret; - // process.env.GOOGLE_AUTH_DOMAINS = input.authDomains; + // process.env.GOOGLE_CLIENT_ID = _input.clientId; + // process.env.GOOGLE_CLIENT_SECRET = _input.clientSecret; + // process.env.GOOGLE_AUTH_DOMAINS = _input.authDomains; return { success: true }; }), diff --git a/apps/backend/src/trpc/router.ts b/apps/backend/src/trpc/router.ts index 63268675..50f526c8 100644 --- a/apps/backend/src/trpc/router.ts +++ b/apps/backend/src/trpc/router.ts @@ -5,6 +5,7 @@ import { googleRoutes } from './google.routes'; import { mcpRoutes } from './mcp.routes'; import { posthogRoutes } from './posthog.routes'; import { projectRoutes } from './project.routes'; +import { systemRoutes } from './system.routes'; import { router } from './trpc'; import { usageRoutes } from './usage.routes'; import { userRoutes } from './user.routes'; @@ -19,6 +20,7 @@ export const trpcRouter = router({ google: googleRoutes, account: accountRoutes, mcp: mcpRoutes, + system: systemRoutes, }); export type TrpcRouter = typeof trpcRouter; diff --git a/apps/backend/src/trpc/system.routes.ts b/apps/backend/src/trpc/system.routes.ts new file mode 100644 index 00000000..dc9c529d --- /dev/null +++ b/apps/backend/src/trpc/system.routes.ts @@ -0,0 +1,10 @@ +import { env } from '../env'; +import { adminProtectedProcedure } from './trpc'; + +export const systemRoutes = { + version: adminProtectedProcedure.query(() => ({ + version: env.APP_VERSION, + commit: env.APP_COMMIT, + buildDate: env.APP_BUILD_DATE, + })), +}; diff --git a/apps/backend/tests/pg.test.ts b/apps/backend/tests/pg.test.ts index 49c12fbf..fa86b9d4 100644 --- a/apps/backend/tests/pg.test.ts +++ b/apps/backend/tests/pg.test.ts @@ -8,9 +8,9 @@ import { NewUser } from '../src/db/abstractSchema'; import { user } from '../src/db/pgSchema'; import * as pgSchema from '../src/db/pgSchema'; -const db = drizzle(process.env.DB_URI!, { schema: pgSchema }); +const db = drizzle(process.env.DB_URI || '', { schema: pgSchema }); -describe('userTable', () => { +(process.env.DB_URI ? describe : describe.skip)('userTable', () => { const testUser: NewUser = { id: 'test-user-id', name: 'John', diff --git a/apps/backend/vitest.config.ts b/apps/backend/vitest.config.ts new file mode 100644 index 00000000..ddadd2ec --- /dev/null +++ b/apps/backend/vitest.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vitest/config'; +import base from '../frontend/vite.config'; + +const baseConfig = base as unknown as { test?: Record }; + +export default defineConfig({ + ...base, + test: { + ...(baseConfig.test ?? {}), + // Run migrations once in the main process before workers start + globalSetup: './vitest.setup.ts', + }, +}); diff --git a/apps/backend/vitest.setup.ts b/apps/backend/vitest.setup.ts new file mode 100644 index 00000000..8f47af01 --- /dev/null +++ b/apps/backend/vitest.setup.ts @@ -0,0 +1,54 @@ +import fs from 'fs'; +import path from 'path'; +import Database from 'better-sqlite3'; + +async function runMigrations() { + // Ensure a fresh sqlite DB for tests + const DB_PATH = path.resolve(process.cwd(), 'db.sqlite'); + if (fs.existsSync(DB_PATH)) { + try { + fs.unlinkSync(DB_PATH); + } catch { + // ignore + } + } + + // Create DB file and run sqlite migrations + const db = new Database(DB_PATH); + const migrationsDir = path.resolve(process.cwd(), 'migrations-sqlite'); + if (fs.existsSync(migrationsDir)) { + const files = fs + .readdirSync(migrationsDir) + .filter((f) => f.endsWith('.sql')) + .sort(); + for (const file of files) { + const content = fs.readFileSync(path.join(migrationsDir, file), 'utf8'); + const stmts = content + .split('--> statement-breakpoint') + .map((s) => s.trim()) + .filter(Boolean); + for (const stmt of stmts) { + if (stmt) { + try { + db.exec(stmt); + } catch (err: unknown) { + let msg = ''; + if (err instanceof Error) msg = err.message; + else msg = String(err); + // Ignore "already exists" / duplicate column errors so setup is idempotent + if (/already exists|duplicate column/i.test(msg)) { + continue; + } + throw err; + } + } + } + } + } + + db.close(); +} + +export default async function () { + await runMigrations(); +} diff --git a/apps/frontend/src/routes/_sidebar-layout.settings.project.tsx b/apps/frontend/src/routes/_sidebar-layout.settings.project.tsx index 20026563..e5fc206b 100644 --- a/apps/frontend/src/routes/_sidebar-layout.settings.project.tsx +++ b/apps/frontend/src/routes/_sidebar-layout.settings.project.tsx @@ -40,8 +40,11 @@ function ProjectPage() { const { tab } = Route.useSearch(); const activeTab = tab ?? 'project'; const project = useQuery(trpc.project.getCurrent.queryOptions()); - const isAdmin = project.data?.userRole === 'admin'; + const appVersion = useQuery({ + ...trpc.system.version.queryOptions(), + enabled: isAdmin, + }); return (
@@ -90,6 +93,55 @@ function ProjectPage() { + + {isAdmin && ( + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ )}
)}