@bhooai/nexus-cli 0.1.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/package.json +8 -12
  2. package/src/commands/add.ts +274 -0
  3. package/src/commands/build.ts +91 -0
  4. package/src/commands/db.ts +241 -0
  5. package/src/commands/dev.ts +149 -186
  6. package/src/commands/doctor.ts +102 -184
  7. package/src/commands/init.ts +204 -958
  8. package/src/commands/maintenance-commands.ts +40 -0
  9. package/src/commands/plugin.ts +233 -0
  10. package/src/commands/queue.ts +145 -0
  11. package/src/commands/test.ts +34 -0
  12. package/src/dispatcher.ts +98 -0
  13. package/src/generators.ts +102 -0
  14. package/src/index.ts +32 -90
  15. package/src/make/generators.ts +431 -0
  16. package/src/ports.ts +85 -0
  17. package/src/prompts.ts +68 -0
  18. package/src/templating/render.ts +109 -0
  19. package/src/util.ts +74 -112
  20. package/templates/base/Dockerfile.ejs +68 -0
  21. package/templates/base/README.md.ejs +62 -0
  22. package/templates/base/apps/admin/index.html.ejs +12 -0
  23. package/templates/base/apps/admin/nginx.conf.ejs +16 -0
  24. package/templates/base/apps/admin/package.json.ejs +15 -0
  25. package/templates/base/apps/admin/src/main.tsx.ejs +7 -0
  26. package/templates/base/apps/admin/vite.config.ts.ejs +22 -0
  27. package/templates/base/apps/ai-server/main.py.ejs +30 -0
  28. package/templates/base/apps/ai-server/requirements.txt +4 -0
  29. package/templates/base/apps/backend/src/main.ts +14 -0
  30. package/templates/base/apps/backend/src/routes/index.ts.ejs +11 -0
  31. package/templates/base/apps/frontend/index.html.ejs +12 -0
  32. package/templates/base/apps/frontend/nginx.conf.ejs +33 -0
  33. package/templates/base/apps/frontend/package.json.ejs +11 -0
  34. package/templates/base/apps/frontend/src/main.tsx.ejs +16 -0
  35. package/templates/base/apps/frontend/vite.config.ts.ejs +22 -0
  36. package/templates/base/docker-compose.yml.ejs +93 -0
  37. package/templates/base/dockerignore +21 -0
  38. package/templates/base/gitignore +43 -0
  39. package/templates/base/nexus.config.ts.ejs +97 -0
  40. package/templates/base/package.json.ejs +40 -0
  41. package/templates/{tsconfig.json → base/tsconfig.json} +2 -3
  42. package/tsconfig.json +5 -4
  43. package/PLAN.md +0 -141
  44. package/README.md +0 -34
  45. package/src/commands/cluster.ts +0 -133
  46. package/src/commands/node.ts +0 -106
  47. package/src/commands/pysetup.ts +0 -142
  48. package/src/commands/sync.ts +0 -116
  49. package/src/commands/uninstall.ts +0 -287
  50. package/src/config-sync.ts +0 -384
  51. package/src/dotenv.ts +0 -39
  52. package/src/supervisor.ts +0 -394
  53. package/src/wizard.ts +0 -149
  54. package/templates/Dockerfile +0 -60
  55. package/templates/README.md +0 -69
  56. package/templates/apps/admin/index.html +0 -12
  57. package/templates/apps/admin/package.json +0 -22
  58. package/templates/apps/admin/postcss.config.js +0 -3
  59. package/templates/apps/admin/src/main.tsx +0 -10
  60. package/templates/apps/admin/src/vite-env.d.ts +0 -18
  61. package/templates/apps/admin/tailwind.config.js +0 -9
  62. package/templates/apps/admin/tsconfig.json +0 -17
  63. package/templates/apps/admin/vite.config.ts +0 -64
  64. package/templates/apps/ai-server/main.py +0 -43
  65. package/templates/apps/ai-server/providers/__init__.py +0 -3
  66. package/templates/apps/ai-server/providers/base.py +0 -111
  67. package/templates/apps/ai-server/requirements.txt +0 -3
  68. package/templates/apps/ai-server/routers/__init__.py +0 -3
  69. package/templates/apps/ai-server/routers/chat.py +0 -47
  70. package/templates/apps/ai-server/routers/embeddings.py +0 -30
  71. package/templates/apps/ai-server/routers/lint.py +0 -167
  72. package/templates/apps/ai-server/routers/models.py +0 -23
  73. package/templates/apps/ai-server/routers/preflight.py +0 -169
  74. package/templates/apps/ai-server/settings.py +0 -48
  75. package/templates/apps/backend/package.json +0 -34
  76. package/templates/apps/backend/src/main.ts +0 -375
  77. package/templates/apps/backend/src/modules/admin/adminRoutes.ts +0 -732
  78. package/templates/apps/backend/src/modules/admin/clusterRoutes.ts +0 -391
  79. package/templates/apps/backend/src/modules/admin/databaseRoutes.ts +0 -161
  80. package/templates/apps/backend/src/modules/admin/lintProxy.ts +0 -89
  81. package/templates/apps/backend/src/modules/admin/preflightProxy.ts +0 -242
  82. package/templates/apps/backend/src/modules/admin/roleCatalog.ts +0 -78
  83. package/templates/apps/backend/src/modules/admin/schemaRoutes.ts +0 -449
  84. package/templates/apps/backend/src/modules/ai/aiProxy.ts +0 -265
  85. package/templates/apps/backend/src/modules/auth/authRoutes.ts +0 -220
  86. package/templates/apps/backend/src/modules/payments/paymentRoutes.ts +0 -100
  87. package/templates/apps/backend/src/modules/payments/paymentStore.ts +0 -172
  88. package/templates/apps/backend/src/modules/requests/requestLog.ts +0 -175
  89. package/templates/apps/backend/src/modules/users/userGraph.ts +0 -83
  90. package/templates/apps/backend/src/modules/users/userModel.ts +0 -88
  91. package/templates/apps/backend/src/plugins/CronScheduler.ts +0 -69
  92. package/templates/apps/backend/src/plugins/loadPlugins.ts +0 -107
  93. package/templates/apps/backend/tsconfig.json +0 -14
  94. package/templates/apps/frontend/index.html +0 -12
  95. package/templates/apps/frontend/package.json +0 -20
  96. package/templates/apps/frontend/postcss.config.js +0 -3
  97. package/templates/apps/frontend/src/index.css +0 -44
  98. package/templates/apps/frontend/src/main.tsx +0 -65
  99. package/templates/apps/frontend/tailwind.config.js +0 -9
  100. package/templates/apps/frontend/vite.config.ts +0 -72
  101. package/templates/bin/nexus.js +0 -35
  102. package/templates/bin/serve-all.mjs +0 -45
  103. package/templates/dockerignore +0 -15
  104. package/templates/gitignore +0 -12
  105. package/templates/nexus.config.ts +0 -69
  106. package/templates/package.json +0 -47
  107. package/tests/cli.test.ts +0 -52
  108. package/tests/config-sync.test.ts +0 -201
  109. package/tests/dotenv.test.ts +0 -51
  110. package/vitest.config.ts +0 -9
  111. package/vitest.config.ts.timestamp-1786095205351-444061f6ff5c58.mjs +0 -13
  112. /package/templates/{uploads → base/apps/backend/storage}/.gitkeep +0 -0
@@ -1,69 +0,0 @@
1
- # My Nexus App
2
-
3
- Scaffolded by `nexus init` — a [BhooAI Nexus](https://github.com/bhooai/nexus) full-stack app
4
- with four runnable terminals: backend, frontend, Python AI server, and admin.
5
-
6
- ## Quick start
7
-
8
- ```bash
9
- npm install
10
- npm run doctor # verify node, python, mongo, redis
11
- npm run dev # start all four terminals under the supervisor
12
- ```
13
-
14
- `nexus init` installs these dependencies automatically. Run `npm install` manually only when this
15
- directory was copied without running `nexus init`, or after changing dependency versions.
16
-
17
- Then visit:
18
-
19
- - Backend: http://localhost:4000/health
20
- - Frontend: http://localhost:3000
21
- - Admin: http://localhost:3300
22
- - AI server: http://localhost:8000
23
-
24
- ## What you got
25
-
26
- - `apps/backend/` — Node backend on the Nexus framework (custom HTTP server, CSRF/CORS,
27
- GraphQL, websockets, ODM, OAuth, payments, and multipart uploads under `/uploads`.
28
- Uploaded files are stored in the project-local `uploads/` directory. See `src/main.ts`.
29
- - `apps/frontend/` — React + Vite + Tailwind (placeholder; add components in `src/`).
30
- - `apps/ai-server/` — Python FastAPI (OpenAI + Ollama). `pip install -r requirements.txt`.
31
- - `apps/admin/` — React + Vite admin console (config, processes, plugins, users, monitoring).
32
-
33
- ## Config
34
-
35
- Edit `nexus.config.ts` for app settings. Secrets go directly in `.env` (no `.env.example`).
36
- Admin overrides are written to `nexus.runtime.json` (gitignored).
37
-
38
- ## Uploads
39
-
40
- The generated backend accepts a `multipart/form-data` `POST /uploads` request and returns generated
41
- file URLs. The default limit is 10 MiB per file and 20 files per request. The request must include the
42
- CSRF token returned by `GET /csrf-token`:
43
-
44
- ```js
45
- const csrf = await fetch('/csrf-token', { credentials: 'include' }).then((r) => r.json());
46
- const form = new FormData();
47
- form.append('file', input.files[0]);
48
- const response = await fetch('/uploads', {
49
- method: 'POST',
50
- body: form,
51
- credentials: 'include',
52
- headers: { 'x-csrf-token': csrf.token },
53
- });
54
- ```
55
-
56
- Configure `uploads.dir`, `uploads.path`, `uploads.maxFileSize`, `uploads.maxFiles`, and
57
- `uploads.allowedTypes` in `nexus.config.ts`. Uploaded files are ignored by Git.
58
-
59
- > Generated projects depend on the published `bhooai-nexus` package. Publish the framework package
60
- > from the `bhooai-nexus/` workspace root with `npm publish` before installing it from the public registry.
61
-
62
- ## Scripts
63
-
64
- | command | what it does |
65
- | --- | --- |
66
- | `npm run dev` | start backend + frontend + AI + admin under the supervisor |
67
- | `npm run doctor` | check node/python versions and mongo/redis reachability |
68
- | `npm run build` | build all TypeScript workspaces |
69
- | `npm test` | run the test suites |
@@ -1,12 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>BhooAI Nexus Admin</title>
7
- </head>
8
- <body class="bg-mesh text-slate-100">
9
- <div id="root"></div>
10
- <script type="module" src="/src/main.tsx"></script>
11
- </body>
12
- </html>
@@ -1,22 +0,0 @@
1
- {
2
- "name": "my-nexus-admin",
3
- "private": true,
4
- "type": "module",
5
- "scripts": {
6
- "dev": "vite --port 3300",
7
- "build": "tsc -b && vite build",
8
- "preview": "vite preview"
9
- },
10
- "dependencies": {
11
- "react": "^18.3.0",
12
- "react-dom": "^18.3.0"
13
- },
14
- "devDependencies": {
15
- "@bhooai/nexus-postcss": "*",
16
- "@types/react": "^18.3.0",
17
- "@types/react-dom": "^18.3.0",
18
- "@vitejs/plugin-react": "^4.3.0",
19
- "typescript": "^5.6.0",
20
- "vite": "^5.4.0"
21
- }
22
- }
@@ -1,3 +0,0 @@
1
- import { createPreset } from '@bhooai/nexus-postcss';
2
-
3
- export default createPreset();
@@ -1,10 +0,0 @@
1
- import React from 'react';
2
- import { createRoot } from 'react-dom/client';
3
- import { App } from '@bhooai/admin';
4
- import '@bhooai/admin/style.css';
5
-
6
- createRoot(document.getElementById('root')!).render(
7
- <React.StrictMode>
8
- <App />
9
- </React.StrictMode>,
10
- );
@@ -1,18 +0,0 @@
1
- // Ambient declarations for the admin host app. Self-contained: does not depend
2
- // on `vite/client` types being resolvable from the host workspace, so `@bhooai/admin`
3
- // imports (import.meta.env, *.svg) type-check consistently in every project.
4
- interface ImportMetaEnv {
5
- readonly VITE_SUPERVISOR_URL?: string;
6
- readonly [key: string]: string | undefined;
7
- }
8
- interface ImportMeta {
9
- readonly env: ImportMetaEnv;
10
- }
11
- declare module '*.svg' {
12
- const src: string;
13
- export default src;
14
- }
15
- declare module '*.png' {
16
- const src: string;
17
- export default src;
18
- }
@@ -1,9 +0,0 @@
1
- export default {
2
- content: [
3
- './index.html',
4
- './src/**/*.{ts,tsx}',
5
- '../../bhooai-nexus/packages/nexus-admin/src/**/*.{ts,tsx}',
6
- ],
7
- theme: { extend: {} },
8
- plugins: [],
9
- };
@@ -1,17 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
- "module": "ESNext",
6
- "moduleResolution": "Bundler",
7
- "jsx": "react-jsx",
8
- "strict": true,
9
- "noEmit": true,
10
- "skipLibCheck": true,
11
- "esModuleInterop": true,
12
- "resolveJsonModule": true,
13
- "isolatedModules": true,
14
- "allowImportingTsExtensions": true
15
- },
16
- "include": ["src"]
17
- }
@@ -1,64 +0,0 @@
1
- import { defineConfig } from 'vite';
2
- import react from '@vitejs/plugin-react';
3
- import { existsSync } from 'node:fs';
4
- import { dirname, join, resolve } from 'node:path';
5
- import { fileURLToPath, pathToFileURL } from 'node:url';
6
-
7
- const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
8
-
9
- // Read the merged host/port values from the project's single nexus config file
10
- // (ts/js/mjs/cjs, discovery order matches the backend loader) plus NEXUS_*
11
- // env overrides, so this Vite dev server and its proxy target stay in one
12
- // source of truth: nexus.config.js.
13
- async function loadConfig() {
14
- const exts = ['ts', 'js', 'mjs', 'cjs'];
15
- let user: Record<string, any> = {};
16
- for (const ext of exts) {
17
- const file = join(PROJECT_ROOT, `nexus.config.${ext}`);
18
- if (existsSync(file)) {
19
- const mod = (await import(pathToFileURL(file).href + `?v=${Date.now()}`)) as Record<string, any>;
20
- user = mod.default ?? mod.config ?? {};
21
- break;
22
- }
23
- }
24
- const num = (v: string | undefined, fallback: number) => (v !== undefined && Number.isFinite(Number(v)) ? Number(v) : fallback);
25
- const str = (v: string | undefined, fallback: string) => (v !== undefined && v !== '' ? v : fallback);
26
-
27
- return {
28
- server: {
29
- host: str(process.env.NEXUS_SERVER_HOST, user.server?.host ?? '0.0.0.0'),
30
- port: num(process.env.NEXUS_SERVER_PORT, user.server?.port ?? 4000),
31
- },
32
- admin: {
33
- host: str(process.env.NEXUS_ADMIN_HOST, user.admin?.host ?? 'localhost'),
34
- port: num(process.env.NEXUS_ADMIN_PORT, user.admin?.port ?? 3300),
35
- enabled: user.admin?.enabled ?? true,
36
- },
37
- frontend: {
38
- host: str(process.env.NEXUS_FRONTEND_HOST, user.frontend?.host ?? 'localhost'),
39
- port: num(process.env.NEXUS_FRONTEND_PORT, user.frontend?.port ?? 3000),
40
- enabled: user.frontend?.enabled ?? true,
41
- },
42
- };
43
- }
44
-
45
- export default defineConfig(async () => {
46
- const cfg = await loadConfig();
47
- const backendHost = cfg.server.host === '0.0.0.0' || cfg.server.host === '::' ? '127.0.0.1' : cfg.server.host;
48
- const target = `http://${backendHost}:${cfg.server.port}`;
49
-
50
- return {
51
- plugins: [react()],
52
- server: {
53
- port: cfg.admin.port,
54
- host: cfg.admin.host,
55
- proxy: {
56
- '/admin': { target, changeOrigin: true },
57
- '/auth': { target, changeOrigin: true },
58
- '/payments': { target, changeOrigin: true },
59
- '/csrf-token': { target, changeOrigin: true },
60
- '/ai': { target, changeOrigin: true },
61
- },
62
- },
63
- };
64
- });
@@ -1,43 +0,0 @@
1
- """BhooAI Nexus AI server (FastAPI).
2
-
3
- OpenAI-compatible gateway fronting OpenAI and Ollama. The Node backend proxies
4
- requests here (and re-emits SSE); the browser never calls this server directly.
5
-
6
- Run: uvicorn main:app --reload --port 8000 (from apps/ai-server)
7
- """
8
- from __future__ import annotations
9
-
10
- from fastapi import FastAPI
11
- from fastapi.middleware.cors import CORSMiddleware
12
-
13
- from settings import settings
14
- from providers import available_providers
15
- from routers import chat, embeddings, models, preflight, lint
16
-
17
- app = FastAPI(title="BhooAI Nexus AI", version="1.0.0")
18
-
19
- app.add_middleware(
20
- CORSMiddleware,
21
- allow_origins=settings.cors_origins,
22
- allow_credentials=True,
23
- allow_methods=["*"],
24
- allow_headers=["*"],
25
- )
26
-
27
-
28
- @app.get("/health")
29
- async def health():
30
- return {"status": "ok", "providers": available_providers()}
31
-
32
-
33
- app.include_router(chat.router)
34
- app.include_router(embeddings.router)
35
- app.include_router(models.router)
36
- app.include_router(preflight.router)
37
- app.include_router(lint.router)
38
-
39
-
40
- if __name__ == "__main__":
41
- import uvicorn
42
-
43
- uvicorn.run("main:app", host="0.0.0.0", port=settings.server_port, reload=False)
@@ -1,3 +0,0 @@
1
- from .base import Provider, OpenAIProvider, OllamaProvider, get_provider, available_providers
2
-
3
- __all__ = ["Provider", "OpenAIProvider", "OllamaProvider", "get_provider", "available_providers"]
@@ -1,111 +0,0 @@
1
- """Provider abstraction. Both OpenAI and Ollama expose OpenAI-compatible
2
- endpoints, so a single OpenAICompatibleProvider parameterised by base URL +
3
- auth handles both. Streaming uses SSE (upstream → parsed → re-emitted with the
4
- provider field injected)."""
5
- from __future__ import annotations
6
-
7
- import json
8
- from typing import Any, AsyncIterator
9
-
10
- import httpx
11
-
12
- from settings import Settings, settings, resolve_provider
13
-
14
-
15
- class Provider:
16
- name = "base"
17
-
18
- def __init__(self, base_url: str, api_key: str, timeout: float, transport: Any | None = None):
19
- self.base_url = base_url.rstrip("/")
20
- self.api_key = api_key
21
- self.timeout = timeout
22
- self.transport = transport
23
-
24
- def _headers(self) -> dict[str, str]:
25
- h = {"Content-Type": "application/json", "Accept": "application/json"}
26
- if self.api_key:
27
- h["Authorization"] = f"Bearer {self.api_key}"
28
- return h
29
-
30
- def _clean(self, body: dict[str, Any]) -> dict[str, Any]:
31
- # Strip our routing field before forwarding upstream.
32
- return {k: v for k, v in body.items() if k != "provider"}
33
-
34
- async def chat(self, body: dict[str, Any]) -> dict[str, Any]:
35
- async with httpx.AsyncClient(timeout=self.timeout, transport=self.transport) as client:
36
- resp = await client.post(f"{self.base_url}/chat/completions", json=self._clean(body), headers=self._headers())
37
- resp.raise_for_status()
38
- data = resp.json()
39
- data["provider"] = self.name
40
- return data
41
-
42
- async def chat_stream(self, body: dict[str, Any]) -> AsyncIterator[str]:
43
- """Yield SSE-formatted `data: ...` lines (already ending in \\n\\n)."""
44
- payload = self._clean(body)
45
- payload["stream"] = True
46
- async with httpx.AsyncClient(timeout=self.timeout, transport=self.transport) as client:
47
- async with client.stream("POST", f"{self.base_url}/chat/completions", json=payload, headers=self._headers()) as resp:
48
- resp.raise_for_status()
49
- async for line in resp.aiter_lines():
50
- if not line or not line.startswith("data:"):
51
- continue
52
- raw = line[5:].strip()
53
- if raw == "[DONE]":
54
- yield "data: [DONE]\n\n"
55
- return
56
- try:
57
- chunk = json.loads(raw)
58
- except json.JSONDecodeError:
59
- continue
60
- chunk["provider"] = self.name
61
- yield f"data: {json.dumps(chunk)}\n\n"
62
- # If the upstream didn't send [DONE], close the stream cleanly.
63
- yield "data: [DONE]\n\n"
64
-
65
- async def embed(self, body: dict[str, Any]) -> dict[str, Any]:
66
- async with httpx.AsyncClient(timeout=self.timeout, transport=self.transport) as client:
67
- resp = await client.post(f"{self.base_url}/embeddings", json=self._clean(body), headers=self._headers())
68
- resp.raise_for_status()
69
- data = resp.json()
70
- data["provider"] = self.name
71
- return data
72
-
73
- async def list_models(self) -> dict[str, Any]:
74
- async with httpx.AsyncClient(timeout=self.timeout, transport=self.transport) as client:
75
- resp = await client.get(f"{self.base_url}/models", headers=self._headers())
76
- resp.raise_for_status()
77
- data = resp.json()
78
- data["provider"] = self.name
79
- return data
80
-
81
-
82
- class OpenAIProvider(Provider):
83
- name = "openai"
84
-
85
- @classmethod
86
- def from_settings(cls, s: Settings) -> "OpenAIProvider":
87
- return cls(s.openai_base_url, s.openai_api_key, s.request_timeout_s)
88
-
89
-
90
- class OllamaProvider(Provider):
91
- name = "ollama"
92
-
93
- @classmethod
94
- def from_settings(cls, s: Settings) -> "OllamaProvider":
95
- return cls(s.ollama_base_url, s.ollama_api_key, s.request_timeout_s)
96
-
97
-
98
- def get_provider(requested: str | None) -> Provider:
99
- """Resolve and instantiate the provider for a request."""
100
- name = resolve_provider(requested)
101
- if name == "openai":
102
- return OpenAIProvider.from_settings(settings)
103
- return OllamaProvider.from_settings(settings)
104
-
105
-
106
- def available_providers() -> list[str]:
107
- avail = []
108
- if settings.openai_api_key:
109
- avail.append("openai")
110
- avail.append("ollama")
111
- return avail
@@ -1,3 +0,0 @@
1
- fastapi>=0.115.0
2
- uvicorn[standard]>=0.30.0
3
- httpx>=0.27.0
@@ -1,3 +0,0 @@
1
- from . import chat, embeddings, models
2
-
3
- __all__ = ["chat", "embeddings", "models"]
@@ -1,47 +0,0 @@
1
- """Chat completions router — OpenAI-compatible, supports SSE streaming."""
2
- from __future__ import annotations
3
-
4
- from fastapi import APIRouter, HTTPException
5
- from fastapi.responses import StreamingResponse
6
- from pydantic import BaseModel, Field
7
-
8
- from providers import get_provider
9
-
10
- router = APIRouter()
11
-
12
-
13
- class Message(BaseModel):
14
- role: str
15
- content: str
16
- name: str | None = None
17
-
18
-
19
- class ChatCompletionRequest(BaseModel):
20
- model: str
21
- messages: list[Message]
22
- stream: bool = False
23
- temperature: float | None = None
24
- max_tokens: int | None = None
25
- provider: str | None = None
26
-
27
-
28
- @router.post("/chat/completions")
29
- async def chat_completions(req: ChatCompletionRequest):
30
- body = req.model_dump(exclude_none=True)
31
- try:
32
- provider = get_provider(body.get("provider"))
33
- except ValueError as e:
34
- raise HTTPException(status_code=400, detail=str(e))
35
-
36
- if body.get("stream"):
37
- async def gen():
38
- async for chunk in provider.chat_stream(body):
39
- yield chunk
40
- return StreamingResponse(gen(), media_type="text/event-stream")
41
-
42
- try:
43
- return await provider.chat(body)
44
- except HTTPException:
45
- raise
46
- except Exception as e: # upstream error
47
- raise HTTPException(status_code=502, detail=f"upstream error: {e}")
@@ -1,30 +0,0 @@
1
- """Embeddings router — OpenAI-compatible."""
2
- from __future__ import annotations
3
-
4
- from fastapi import APIRouter, HTTPException
5
- from pydantic import BaseModel
6
-
7
- from providers import get_provider
8
-
9
- router = APIRouter()
10
-
11
-
12
- class EmbeddingRequest(BaseModel):
13
- model: str
14
- input: str | list[str]
15
- provider: str | None = None
16
-
17
-
18
- @router.post("/embeddings")
19
- async def create_embeddings(req: EmbeddingRequest):
20
- body = req.model_dump(exclude_none=True)
21
- try:
22
- provider = get_provider(body.get("provider"))
23
- except ValueError as e:
24
- raise HTTPException(status_code=400, detail=str(e))
25
- try:
26
- return await provider.embed(body)
27
- except HTTPException:
28
- raise
29
- except Exception as e:
30
- raise HTTPException(status_code=502, detail=f"upstream error: {e}")
@@ -1,167 +0,0 @@
1
- """Env + config linter router.
2
-
3
- Node sends the raw `.env` text (and the raw `nexus.runtime.json` text) and
4
- Python computes a structured report. Python never echoes secret *values* — only
5
- keys + issues. This keeps the lint logic in Python while Node owns auth + file
6
- access. There is no `.env.example` anymore: secrets live directly in `.env`.
7
- """
8
- from __future__ import annotations
9
-
10
- import json
11
- import re
12
- import time
13
- from typing import Any
14
-
15
- from fastapi import APIRouter, HTTPException
16
- from pydantic import BaseModel
17
-
18
- router = APIRouter()
19
-
20
- ALLOWED_LOG_LEVELS = {"silent", "fatal", "error", "warn", "info", "debug", "trace"}
21
-
22
-
23
- class LintEnvRequest(BaseModel):
24
- envText: str = ""
25
-
26
-
27
- class LintConfigRequest(BaseModel):
28
- content: str = ""
29
-
30
-
31
- # ---- env parsing helpers ----
32
-
33
- def _parse_env(text: str) -> tuple[list[dict[str, Any]], list[str]]:
34
- """Parse dotenv-ish text. Returns (exports, malformed_lines)."""
35
- exports: list[dict[str, Any]] = []
36
- malformed: list[str] = []
37
- for lineno, raw in enumerate(text.splitlines(), 1):
38
- line = raw.strip()
39
- if not line or line.startswith("#"):
40
- continue
41
- if "=" not in line:
42
- malformed.append(f"line {lineno}: no '=' found")
43
- continue
44
- key, _, value = line.partition("=")
45
- key = key.strip()
46
- value = value.strip()
47
- if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
48
- value = value[1:-1]
49
- exports.append({"key": key, "value": value})
50
- return exports, malformed
51
-
52
-
53
- _PLACEHOLDER_PATTERNS = [
54
- r"^(change|changeme|change-me|your|example|exampl|replace|dummy|sample)",
55
- r"^xxxx+$",
56
- r"^xxxx+",
57
- ]
58
-
59
-
60
- def _is_placeholder(value: str) -> bool:
61
- if len(value.strip()) < 4:
62
- return True
63
- lowered = value.strip().lower()
64
- return any(re.search(p, lowered) for p in _PLACEHOLDER_PATTERNS)
65
-
66
-
67
- # ---- env checks ----
68
-
69
- def _check_env(exports: list[dict[str, Any]], malformed: list[str]) -> list[dict[str, Any]]:
70
- checks: list[dict[str, Any]] = []
71
- seen: dict[str, int] = {}
72
- for e in exports:
73
- k = e["key"]
74
- count = seen.get(k, 0) + 1
75
- seen[k] = count
76
- if count > 1:
77
- checks.append({"key": k, "severity": "error", "kind": "duplicate", "message": "duplicate key"})
78
- if e["value"] == "":
79
- checks.append({"key": k, "severity": "warning", "kind": "empty", "message": "has an empty value"})
80
- elif _is_placeholder(e["value"]):
81
- checks.append({"key": k, "severity": "warning", "kind": "placeholder", "message": "value still looks like a placeholder"})
82
- for line in malformed:
83
- checks.append({"key": "(syntax)", "severity": "error", "kind": "malformed", "message": line})
84
- env = {f["key"] for f in exports}
85
- if "NEXUS_AUTH_JWT_SECRET" not in env:
86
- checks.append({"key": "NEXUS_AUTH_JWT_SECRET", "severity": "warning", "kind": "missing", "message": "set this in .env — there is no default JWT secret"})
87
- return checks
88
-
89
-
90
- # ---- config checks ----
91
-
92
- def _config_checks(data: dict[str, Any]) -> list[dict[str, Any]]:
93
- checks: list[dict[str, Any]] = []
94
- port = _get(data, "server.port")
95
- if port is not None and not (isinstance(port, (int, float)) and 1 <= int(port) <= 65535):
96
- checks.append({"key": "server.port", "severity": "error", "kind": "type", "message": "must be an integer port 1–65535"})
97
- body = _get(data, "server.bodyLimit")
98
- if body is not None and not isinstance(body, (int, float)):
99
- checks.append({"key": "server.bodyLimit", "severity": "warning", "kind": "type", "message": "must be a number (bytes)"})
100
- lvl = _get(data, "logging.level")
101
- if lvl is not None and lvl not in ALLOWED_LOG_LEVELS:
102
- checks.append({"key": "logging.level", "severity": "warning", "kind": "value", "message": f"unexpected level '{lvl}' (expected one of {sorted(ALLOWED_LOG_LEVELS)})"})
103
- uri = _get(data, "db.uri")
104
- if uri is not None and not (isinstance(uri, str) and uri.startswith(("mongodb://", "mongodb+srv://"))):
105
- checks.append({"key": "db.uri", "severity": "error", "kind": "url", "message": "must be a mongodb:// or mongodb+srv:// URL"})
106
- auto = _get(data, "db.autoIndex")
107
- if auto is not None and not isinstance(auto, bool):
108
- checks.append({"key": "db.autoIndex", "severity": "warning", "kind": "type", "message": "must be a boolean"})
109
- intro = _get(data, "graphql.introspection")
110
- if intro is not None and not isinstance(intro, bool):
111
- checks.append({"key": "graphql.introspection", "severity": "warning", "kind": "type", "message": "must be a boolean"})
112
- return checks
113
-
114
-
115
- def _get(doc: dict[str, Any], path: str) -> Any:
116
- node: Any = doc
117
- for part in path.split("."):
118
- if isinstance(node, dict) and part in node:
119
- node = node[part]
120
- else:
121
- return None
122
- return node
123
-
124
-
125
- # ---- report ----
126
-
127
- def _report(checks: list[dict[str, Any]]) -> dict[str, Any]:
128
- summary = {"error": 0, "warning": 0, "info": 0, "ok": 0}
129
- for c in checks:
130
- sev = c["severity"] if c["severity"] in summary else "info"
131
- summary[sev] += 1
132
- if not checks:
133
- summary["ok"] = 1
134
- order = {"error": 0, "warning": 1, "info": 2, "ok": 9}
135
- checks.sort(key=lambda c: (order.get(c["severity"], 9), c["key"]))
136
- return {"ranAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "summary": summary, "checks": checks}
137
-
138
-
139
- # ---- routes ----
140
-
141
- @router.post("/lint/env")
142
- async def lint_env(req: LintEnvRequest):
143
- exports, malformed = _parse_env(req.envText)
144
- checks = _check_env(exports, malformed)
145
- return _report(checks)
146
-
147
-
148
- @router.post("/lint/config")
149
- async def lint_config(req: LintConfigRequest):
150
- try:
151
- doc = json.loads(req.content)
152
- except json.JSONDecodeError as e:
153
- return {
154
- "ranAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
155
- "summary": {"error": 1, "warning": 0, "info": 0, "ok": 0},
156
- "checks": [
157
- {"key": "(syntax)", "severity": "error", "kind": "malformed",
158
- "message": f"not valid JSON: {e} at line {e.lineno} col {e.colno}"}
159
- ],
160
- }
161
- if not isinstance(doc, dict):
162
- return {
163
- "ranAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
164
- "summary": {"error": 1, "warning": 0, "info": 0, "ok": 0},
165
- "checks": [{"key": "(root)", "severity": "error", "kind": "type", "message": "config root must be a JSON object"}],
166
- }
167
- return _report(_config_checks(doc))
@@ -1,23 +0,0 @@
1
- """Models router — lists available upstream models."""
2
- from __future__ import annotations
3
-
4
- from fastapi import APIRouter, HTTPException
5
- from fastapi.params import Query
6
-
7
- from providers import get_provider
8
-
9
- router = APIRouter()
10
-
11
-
12
- @router.get("/models")
13
- async def list_models(provider: str | None = Query(default=None)):
14
- try:
15
- p = get_provider(provider)
16
- except ValueError as e:
17
- raise HTTPException(status_code=400, detail=str(e))
18
- try:
19
- return await p.list_models()
20
- except HTTPException:
21
- raise
22
- except Exception as e:
23
- raise HTTPException(status_code=502, detail=f"upstream error: {e}")