@bhooai/nexus-cli 0.1.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 (75) hide show
  1. package/PLAN.md +141 -0
  2. package/README.md +34 -0
  3. package/package.json +25 -0
  4. package/src/commands/cluster.ts +133 -0
  5. package/src/commands/dev.ts +133 -0
  6. package/src/commands/doctor.ts +199 -0
  7. package/src/commands/init.ts +960 -0
  8. package/src/commands/node.ts +101 -0
  9. package/src/commands/pysetup.ts +136 -0
  10. package/src/commands/sync.ts +116 -0
  11. package/src/commands/uninstall.ts +287 -0
  12. package/src/config-sync.ts +384 -0
  13. package/src/dotenv.ts +39 -0
  14. package/src/index.ts +94 -0
  15. package/src/supervisor.ts +384 -0
  16. package/src/util.ts +123 -0
  17. package/src/wizard.ts +149 -0
  18. package/templates/Dockerfile +60 -0
  19. package/templates/README.md +69 -0
  20. package/templates/apps/admin/index.html +12 -0
  21. package/templates/apps/admin/package.json +24 -0
  22. package/templates/apps/admin/postcss.config.js +6 -0
  23. package/templates/apps/admin/src/main.tsx +10 -0
  24. package/templates/apps/admin/src/vite-env.d.ts +18 -0
  25. package/templates/apps/admin/tailwind.config.js +9 -0
  26. package/templates/apps/admin/tsconfig.json +17 -0
  27. package/templates/apps/admin/vite.config.ts +64 -0
  28. package/templates/apps/ai-server/main.py +43 -0
  29. package/templates/apps/ai-server/providers/__init__.py +3 -0
  30. package/templates/apps/ai-server/providers/base.py +111 -0
  31. package/templates/apps/ai-server/requirements.txt +3 -0
  32. package/templates/apps/ai-server/routers/__init__.py +3 -0
  33. package/templates/apps/ai-server/routers/chat.py +47 -0
  34. package/templates/apps/ai-server/routers/embeddings.py +30 -0
  35. package/templates/apps/ai-server/routers/lint.py +167 -0
  36. package/templates/apps/ai-server/routers/models.py +23 -0
  37. package/templates/apps/ai-server/routers/preflight.py +169 -0
  38. package/templates/apps/ai-server/settings.py +48 -0
  39. package/templates/apps/backend/package.json +33 -0
  40. package/templates/apps/backend/src/main.ts +375 -0
  41. package/templates/apps/backend/src/modules/admin/adminRoutes.ts +732 -0
  42. package/templates/apps/backend/src/modules/admin/clusterRoutes.ts +391 -0
  43. package/templates/apps/backend/src/modules/admin/databaseRoutes.ts +161 -0
  44. package/templates/apps/backend/src/modules/admin/lintProxy.ts +89 -0
  45. package/templates/apps/backend/src/modules/admin/preflightProxy.ts +242 -0
  46. package/templates/apps/backend/src/modules/admin/roleCatalog.ts +78 -0
  47. package/templates/apps/backend/src/modules/admin/schemaRoutes.ts +449 -0
  48. package/templates/apps/backend/src/modules/ai/aiProxy.ts +265 -0
  49. package/templates/apps/backend/src/modules/auth/authRoutes.ts +220 -0
  50. package/templates/apps/backend/src/modules/payments/paymentRoutes.ts +100 -0
  51. package/templates/apps/backend/src/modules/payments/paymentStore.ts +172 -0
  52. package/templates/apps/backend/src/modules/requests/requestLog.ts +175 -0
  53. package/templates/apps/backend/src/modules/users/userGraph.ts +83 -0
  54. package/templates/apps/backend/src/modules/users/userModel.ts +88 -0
  55. package/templates/apps/backend/src/plugins/CronScheduler.ts +69 -0
  56. package/templates/apps/backend/src/plugins/loadPlugins.ts +107 -0
  57. package/templates/apps/backend/tsconfig.json +14 -0
  58. package/templates/apps/frontend/index.html +12 -0
  59. package/templates/apps/frontend/package.json +19 -0
  60. package/templates/apps/frontend/src/main.tsx +64 -0
  61. package/templates/apps/frontend/vite.config.ts +63 -0
  62. package/templates/bin/nexus.js +35 -0
  63. package/templates/bin/serve-all.mjs +45 -0
  64. package/templates/dockerignore +15 -0
  65. package/templates/gitignore +12 -0
  66. package/templates/nexus.config.ts +69 -0
  67. package/templates/package.json +47 -0
  68. package/templates/tsconfig.json +17 -0
  69. package/templates/uploads/.gitkeep +0 -0
  70. package/tests/cli.test.ts +45 -0
  71. package/tests/config-sync.test.ts +201 -0
  72. package/tests/dotenv.test.ts +51 -0
  73. package/tsconfig.json +9 -0
  74. package/vitest.config.ts +9 -0
  75. package/vitest.config.ts.timestamp-1786095205351-444061f6ff5c58.mjs +13 -0
@@ -0,0 +1,201 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdtemp, writeFile, readFile, rm, mkdir } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { syncConfig, deriveValues, fingerprintOf, configChangedSinceSync, syncFingerprintPath } from '../src/config-sync.js';
7
+ import { mergeConfig } from '../../nexus-core/src/index.js';
8
+
9
+ const DOCKERFILE_FIXTURE = `# Build
10
+ # docker run --rm -p 4000:4000 -p 3000:3000 -p 3001:3001 --env-file .env node-1
11
+ # Access:
12
+ # frontend http://localhost:3000 (SPA)
13
+ # admin http://localhost:3001 (SPA)
14
+ # backend http://localhost:4000 (API + /health)
15
+ # node http://localhost:7575 (cluster node agent)
16
+ EXPOSE 3000 3001 4000 7575
17
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \\\\
18
+ CMD wget -qO- http://127.0.0.1:\\\${NEXUS_SERVER_PORT:-4000}/health >/dev/null || exit 1
19
+ `;
20
+
21
+ const SERVE_ALL_FIXTURE = `import { spawn } from 'node:child_process';
22
+ const nodePort = process.env.NEXUS_NODE_PORT ?? '7575';
23
+ run('frontend', join(root, 'apps', 'frontend'), npm, ['run', 'preview', '--', '--host', '0.0.0.0', '--port', '3000']);
24
+ run('admin', join(root, 'apps', 'admin'), npm, ['run', 'preview', '--', '--host', '0.0.0.0', '--port', '3001']);
25
+ `;
26
+
27
+ const ADMIN_PKG_FIXTURE = `{
28
+ "scripts": { "dev": "vite --port 3001", "preview": "vite preview" }
29
+ }
30
+ `;
31
+
32
+ const PS1_FIXTURE = [
33
+ "$dbUri = 'mongodb://host.docker.internal:27017/nexus-docker'",
34
+ "$redisUrl = 'redis://host.docker.internal:6379'",
35
+ "$aiUrl = 'http://host.docker.internal:8000'",
36
+ "$nodePort = '7575'",
37
+ "$serverPort = '4000'",
38
+ "$frontendPort = '3000'",
39
+ "$adminPort = '3001'",
40
+ " '-p', \"$serverPort`:$serverPort\",",
41
+ " '-p', \"$frontendPort`:$frontendPort\",",
42
+ " '-p', \"$adminPort`:$adminPort\",",
43
+ " '-p', \"$nodePort`:$nodePort\",",
44
+ " '-e', \"NEXUS_NODE_PORT=$nodePort\",",
45
+ " '-e', \"NEXUS_SERVER_PORT=$serverPort\"",
46
+ ].join('\n');
47
+
48
+ const SH_FIXTURE = [
49
+ 'DB_URI="mongodb://host.docker.internal:27017/nexus-docker"',
50
+ 'REDIS_URL="redis://host.docker.internal:6379"',
51
+ 'AI_URL="http://host.docker.internal:8000"',
52
+ 'NODE_PORT="7575"',
53
+ ' -p 4000:4000',
54
+ ' -p 3000:3000',
55
+ ' -p 3001:3001',
56
+ ' -p 7575:7575',
57
+ ' -e "NEXUS_NODE_PORT=$NODE_PORT"',
58
+ ].join('\n');
59
+
60
+ const BAT_FIXTURE = [
61
+ '-p 4000:4000',
62
+ '-p 3000:3000',
63
+ '-p 3001:3001',
64
+ '-p 7575:7575',
65
+ '-e NEXUS_DB_URI=mongodb://host.docker.internal:27017/nexus-docker',
66
+ '-e NEXUS_REDIS_URL=redis://host.docker.internal:6379',
67
+ '-e NEXUS_AI_SERVER_URL=http://host.docker.internal:8000',
68
+ '-e NEXUS_NODE_PORT=7575',
69
+ 'echo frontend http://localhost:3000 admin http://localhost:3001 backend http://localhost:4000/health',
70
+ ].join('\n');
71
+
72
+ describe('nexus config-sync', () => {
73
+ let dir: string;
74
+
75
+ beforeEach(async () => {
76
+ dir = await mkdtemp(join(tmpdir(), 'nexus-sync-'));
77
+ await mkdir(join(dir, 'bin'), { recursive: true });
78
+ await mkdir(join(dir, 'apps', 'admin'), { recursive: true });
79
+ await writeFile(join(dir, 'package.json'), JSON.stringify({ name: 'sync-test' }), 'utf8');
80
+ await writeFile(join(dir, 'Dockerfile'), DOCKERFILE_FIXTURE, 'utf8');
81
+ await writeFile(join(dir, 'bin', 'serve-all.mjs'), SERVE_ALL_FIXTURE, 'utf8');
82
+ await writeFile(join(dir, 'apps', 'admin', 'package.json'), ADMIN_PKG_FIXTURE, 'utf8');
83
+ await writeFile(join(dir, 'docker.ps1'), PS1_FIXTURE, 'utf8');
84
+ await writeFile(join(dir, 'docker.sh'), SH_FIXTURE, 'utf8');
85
+ await writeFile(join(dir, 'docker.bat'), BAT_FIXTURE, 'utf8');
86
+ });
87
+
88
+ afterEach(async () => {
89
+ await rm(dir, { recursive: true, force: true });
90
+ });
91
+
92
+ it('writes the configured ports + URLs into every derived file', async () => {
93
+ const cfg = mergeConfig({
94
+ server: { port: 4100, host: '0.0.0.0' },
95
+ frontend: { port: 3100 },
96
+ admin: { port: 3111 },
97
+ cluster: { nodeAgentPort: 7676, lbPort: 9090 },
98
+ ai: { serverUrl: 'http://localhost:8111' },
99
+ db: { uri: 'mongodb://localhost:27017/sync-test' },
100
+ redis: { url: 'redis://localhost:6379' },
101
+ });
102
+
103
+ const report = await syncConfig(dir, cfg, { db: false });
104
+ expect(existsSync(join(dir, '.nexus-sync.json'))).toBe(true);
105
+
106
+ expect(report.files.every((f) => f.status === 'updated' || f.status === 'in-sync')).toBe(true);
107
+
108
+ const df = await readFile(join(dir, 'Dockerfile'), 'utf8');
109
+ expect(df).toContain('EXPOSE 3100 3111 4100 7676');
110
+ expect(df).toContain('-p 4100:4100 -p 3100:3100 -p 3111:3111');
111
+ expect(df).toContain('http://localhost:3100');
112
+ expect(df).toContain('http://localhost:3111');
113
+ expect(df).toContain('http://localhost:4100');
114
+ expect(df).toContain('http://localhost:7676');
115
+ expect(df).toContain('NEXUS_SERVER_PORT:-4100');
116
+
117
+ const sa = await readFile(join(dir, 'bin', 'serve-all.mjs'), 'utf8');
118
+ expect(sa).toContain("NEXUS_NODE_PORT ?? '7676'");
119
+ expect(sa).toContain("'--port', '3100'");
120
+ expect(sa).toContain("'--port', '3111'");
121
+
122
+ const pkg = await readFile(join(dir, 'apps', 'admin', 'package.json'), 'utf8');
123
+ expect(pkg).toContain('"dev": "vite --port 3111"');
124
+
125
+ const ps1 = await readFile(join(dir, 'docker.ps1'), 'utf8');
126
+ expect(ps1).toContain("$nodePort = '7676'");
127
+ expect(ps1).toContain("$serverPort = '4100'");
128
+ expect(ps1).toContain("$frontendPort = '3100'");
129
+ expect(ps1).toContain("$adminPort = '3111'");
130
+ expect(ps1).toContain('NEXUS_NODE_PORT=$nodePort');
131
+ expect(ps1).toContain('NEXUS_SERVER_PORT=$serverPort');
132
+
133
+ const sh = await readFile(join(dir, 'docker.sh'), 'utf8');
134
+ expect(sh).toContain('-p 4100:4100');
135
+ expect(sh).toContain('-p 3100:3100');
136
+ expect(sh).toContain('-p 3111:3111');
137
+ expect(sh).toContain('-p 7676:7676');
138
+
139
+ const bat = await readFile(join(dir, 'docker.bat'), 'utf8');
140
+ expect(bat).toContain('-p 4100:4100');
141
+ expect(bat).toContain('-p 3100:3100');
142
+ expect(bat).toContain('NEXUS_NODE_PORT=7676');
143
+ });
144
+
145
+ it('second run is idempotent (in-sync everywhere)', async () => {
146
+ const cfg = mergeConfig({});
147
+ await syncConfig(dir, cfg, { db: false });
148
+ const second = await syncConfig(dir, cfg, { db: false });
149
+ expect(second.files.map((f) => f.status)).toEqual(['in-sync', 'in-sync', 'in-sync', 'in-sync', 'in-sync', 'in-sync']);
150
+ });
151
+
152
+ it('ported to a new value and back — round-trips (no stale digits left)', async () => {
153
+ const cfgA = mergeConfig({ frontend: { port: 3120 }, admin: { port: 3121 } });
154
+ await syncConfig(dir, cfgA, { db: false });
155
+ const cfgB = mergeConfig({});
156
+ await syncConfig(dir, cfgB, { db: false });
157
+
158
+ const df = await readFile(join(dir, 'Dockerfile'), 'utf8');
159
+ expect(df).toContain('EXPOSE 3000 3001 4000 7575');
160
+ expect(df).not.toContain('3120');
161
+
162
+ const sa = await readFile(join(dir, 'bin', 'serve-all.mjs'), 'utf8');
163
+ expect(sa).toContain("'--port', '3000'");
164
+ expect(sa).not.toContain('312');
165
+
166
+ const sh = await readFile(join(dir, 'docker.sh'), 'utf8');
167
+ expect(sh).not.toContain('312');
168
+
169
+ const bat = await readFile(join(dir, 'docker.bat'), 'utf8');
170
+ expect(bat).not.toContain('312');
171
+
172
+ const pkg = await readFile(join(dir, 'apps', 'admin', 'package.json'), 'utf8');
173
+ expect(pkg).toContain('"dev": "vite --port 3001"');
174
+ });
175
+
176
+ it('fingerprint flips when the config changes', async () => {
177
+ const base = mergeConfig({});
178
+ expect(configChangedSinceSync(dir, base)).toBe(true);
179
+ const report = await syncConfig(dir, base, { db: false });
180
+ expect(report.fingerprint).toBeTruthy();
181
+ expect(configChangedSinceSync(dir, base)).toBe(false);
182
+ const changed = mergeConfig({ server: { port: 5555 } });
183
+ expect(configChangedSinceSync(dir, changed)).toBe(true);
184
+ expect(syncFingerprintPath(dir).length).toBeGreaterThan(0);
185
+ });
186
+
187
+ it('dry-run (write:false) reports updates but leaves files untouched', async () => {
188
+ const cfg = mergeConfig({ server: { port: 9999 } });
189
+ const before = await readFile(join(dir, 'Dockerfile'), 'utf8');
190
+ const report = await syncConfig(dir, cfg, { write: false, db: false });
191
+ expect(report.files.some((f) => f.status === 'updated')).toBe(true);
192
+ const after = await readFile(join(dir, 'Dockerfile'), 'utf8');
193
+ expect(after).toBe(before);
194
+ });
195
+
196
+ it('reports missing files without crashing and without hidden dotfile', async () => {
197
+ await rm(join(dir, 'Dockerfile'));
198
+ const report = await syncConfig(dir, mergeConfig({}), { db: false });
199
+ expect(report.files.find((f) => f.file === 'Dockerfile')?.status).toBe('missing');
200
+ });
201
+ });
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mkdtemp, writeFile, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { loadDotEnv } from '../src/dotenv.js';
6
+
7
+ describe('loadDotEnv', () => {
8
+ it('parses KEY=VALUE lines, comments, quotes, and export prefixes into env', async () => {
9
+ const dir = await mkdtemp(join(tmpdir(), 'nexus-dotenv-'));
10
+ try {
11
+ await writeFile(
12
+ join(dir, '.env'),
13
+ [
14
+ '# comment',
15
+ 'NEXUS_PAYMENTS_RAZORPAY_ENABLED=true',
16
+ 'NEXUS_PAYMENTS_RAZORPAY_KEY_ID=rzp_test_abc',
17
+ "export NEXUS_AI_SERVER_URL='http://localhost:8000'",
18
+ 'QUOTED="value with spaces"',
19
+ '',
20
+ ].join('\n'),
21
+ 'utf8',
22
+ );
23
+ const env: Record<string, string> = {};
24
+ loadDotEnv(dir, env as NodeJS.ProcessEnv);
25
+ expect(env.NEXUS_PAYMENTS_RAZORPAY_ENABLED).toBe('true');
26
+ expect(env.NEXUS_PAYMENTS_RAZORPAY_KEY_ID).toBe('rzp_test_abc');
27
+ expect(env.NEXUS_AI_SERVER_URL).toBe('http://localhost:8000');
28
+ expect(env.QUOTED).toBe('value with spaces');
29
+ } finally {
30
+ await rm(dir, { recursive: true, force: true });
31
+ }
32
+ });
33
+
34
+ it('never overrides an existing environment variable', async () => {
35
+ const dir = await mkdtemp(join(tmpdir(), 'nexus-dotenv-'));
36
+ try {
37
+ await writeFile(join(dir, '.env'), 'NEXUS_SERVER_PORT=9999\n', 'utf8');
38
+ const env: Record<string, string> = { NEXUS_SERVER_PORT: '4000' };
39
+ loadDotEnv(dir, env as NodeJS.ProcessEnv);
40
+ expect(env.NEXUS_SERVER_PORT).toBe('4000');
41
+ } finally {
42
+ await rm(dir, { recursive: true, force: true });
43
+ }
44
+ });
45
+
46
+ it('is a no-op when there is no .env file', () => {
47
+ const env: Record<string, string> = {};
48
+ loadDotEnv(join(tmpdir(), 'definitely-missing-' + Date.now()), env as NodeJS.ProcessEnv);
49
+ expect(Object.keys(env)).toHaveLength(0);
50
+ });
51
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "./dist"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "references": [{ "path": "../nexus-core" }]
9
+ }
@@ -0,0 +1,9 @@
1
+ import { defineProject } from 'vitest/config';
2
+
3
+ export default defineProject({
4
+ test: {
5
+ environment: 'node',
6
+ include: ['tests/**/*.test.ts'],
7
+ globals: false,
8
+ },
9
+ });
@@ -0,0 +1,13 @@
1
+ // packages/nexus-cli/vitest.config.ts
2
+ import { defineProject } from "file:///C:/server/BhooAI/BhooAI-Nexus/BhooAI-Nexus/bhooai-nexus/node_modules/vitest/dist/config.js";
3
+ var vitest_config_default = defineProject({
4
+ test: {
5
+ environment: "node",
6
+ include: ["tests/**/*.test.ts"],
7
+ globals: false
8
+ }
9
+ });
10
+ export {
11
+ vitest_config_default as default
12
+ };
13
+ //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsicGFja2FnZXMvbmV4dXMtY2xpL3ZpdGVzdC5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJDOlxcXFxzZXJ2ZXJcXFxcQmhvb0FJXFxcXEJob29BSS1OZXh1c1xcXFxCaG9vQUktTmV4dXNcXFxcYmhvb2FpLW5leHVzXFxcXHBhY2thZ2VzXFxcXG5leHVzLWNsaVwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiQzpcXFxcc2VydmVyXFxcXEJob29BSVxcXFxCaG9vQUktTmV4dXNcXFxcQmhvb0FJLU5leHVzXFxcXGJob29haS1uZXh1c1xcXFxwYWNrYWdlc1xcXFxuZXh1cy1jbGlcXFxcdml0ZXN0LmNvbmZpZy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vQzovc2VydmVyL0Job29BSS9CaG9vQUktTmV4dXMvQmhvb0FJLU5leHVzL2Job29haS1uZXh1cy9wYWNrYWdlcy9uZXh1cy1jbGkvdml0ZXN0LmNvbmZpZy50c1wiO2ltcG9ydCB7IGRlZmluZVByb2plY3QgfSBmcm9tICd2aXRlc3QvY29uZmlnJztcblxuZXhwb3J0IGRlZmF1bHQgZGVmaW5lUHJvamVjdCh7XG4gIHRlc3Q6IHtcbiAgICBlbnZpcm9ubWVudDogJ25vZGUnLFxuICAgIGluY2x1ZGU6IFsndGVzdHMvKiovKi50ZXN0LnRzJ10sXG4gICAgZ2xvYmFsczogZmFsc2UsXG4gIH0sXG59KTsiXSwKICAibWFwcGluZ3MiOiAiO0FBQW9hLFNBQVMscUJBQXFCO0FBRWxjLElBQU8sd0JBQVEsY0FBYztBQUFBLEVBQzNCLE1BQU07QUFBQSxJQUNKLGFBQWE7QUFBQSxJQUNiLFNBQVMsQ0FBQyxvQkFBb0I7QUFBQSxJQUM5QixTQUFTO0FBQUEsRUFDWDtBQUNGLENBQUM7IiwKICAibmFtZXMiOiBbXQp9Cg==