4bnode 3.5.3 → 4.0.2

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 (62) hide show
  1. package/README.md +144 -109
  2. package/add-api.js +288 -156
  3. package/add-crud.js +226 -0
  4. package/add-login.js +152 -319
  5. package/add-mongo-delete.js +67 -138
  6. package/add-mongo-insert.js +87 -204
  7. package/add-mongo-read.js +100 -216
  8. package/add-mongo-schema.js +431 -199
  9. package/add-mongo-update.js +89 -202
  10. package/add-mongo.js +125 -218
  11. package/add-serialport.js +53 -98
  12. package/add-socket.js +43 -66
  13. package/add-websocket.js +68 -140
  14. package/index.js +341 -156
  15. package/lib/env.js +18 -0
  16. package/lib/indexFile.js +134 -0
  17. package/lib/names.js +21 -0
  18. package/lib/project.js +122 -0
  19. package/lib/routeFile.js +56 -0
  20. package/lib/ui.js +161 -0
  21. package/package.json +9 -16
  22. package/skeleton/.4bnode/assets/fonts/NotoSans-Bold.woff2 +0 -0
  23. package/skeleton/.4bnode/assets/fonts/NotoSans-Light.woff2 +0 -0
  24. package/skeleton/.4bnode/assets/fonts/NotoSans-Medium.woff2 +0 -0
  25. package/skeleton/.4bnode/assets/fonts/NotoSans-Regular.woff2 +0 -0
  26. package/skeleton/.4bnode/assets/fonts/NotoSans-SemiBold.woff2 +0 -0
  27. package/skeleton/.4bnode/assets/images/mongo.png +0 -0
  28. package/skeleton/.4bnode/assets/images/mysql.png +0 -0
  29. package/skeleton/.4bnode/assets/images/postgresql.png +0 -0
  30. package/skeleton/.4bnode/assets/images/sqlite.png +0 -0
  31. package/skeleton/.4bnode/assets/js/App.js +115 -0
  32. package/skeleton/.4bnode/assets/js/Root.js +44 -0
  33. package/skeleton/.4bnode/assets/js/components/Modal.js +52 -0
  34. package/skeleton/.4bnode/assets/js/components/Sidebar.js +68 -0
  35. package/skeleton/.4bnode/assets/js/components/ToastContainer.js +15 -0
  36. package/skeleton/.4bnode/assets/js/context.js +7 -0
  37. package/skeleton/.4bnode/assets/js/lib.js +83 -0
  38. package/skeleton/.4bnode/assets/js/main.js +5 -0
  39. package/skeleton/.4bnode/assets/js/pages/ApiTesterPage.js +1594 -0
  40. package/skeleton/.4bnode/assets/js/pages/DashboardPage.js +435 -0
  41. package/skeleton/.4bnode/assets/js/pages/DatabasePage.js +461 -0
  42. package/skeleton/.4bnode/assets/js/pages/EnvironmentPage.js +141 -0
  43. package/skeleton/.4bnode/assets/js/pages/RealtimePage.js +186 -0
  44. package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +1656 -0
  45. package/skeleton/.4bnode/assets/js/pages/SchemasPage.js +901 -0
  46. package/skeleton/.4bnode/assets/js/pages/SerialPortPage.js +170 -0
  47. package/skeleton/.4bnode/assets/js/screens/PasskeyScreen.js +105 -0
  48. package/skeleton/.4bnode/assets/js/screens/SplashScreen.js +66 -0
  49. package/skeleton/.4bnode/dev-api.js +2930 -0
  50. package/skeleton/.4bnode/ui.html +1556 -0
  51. package/skeleton/index.js +70 -17
  52. package/skeleton/package.json +9 -5
  53. package/skeleton/public/index.html +288 -86
  54. package/README_4bnode.md +0 -197
  55. package/src/auth/passport.js +0 -21
  56. package/src/db.js +0 -13
  57. package/test-server/.env.development +0 -1
  58. package/test-server/.env.production +0 -1
  59. package/test-server/index.js +0 -82
  60. package/test-server/package-lock.json +0 -1267
  61. package/test-server/package.json +0 -20
  62. package/test-server/public/index.html +0 -90
@@ -0,0 +1,134 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { getProjectRoot } from "./project.js";
4
+
5
+ function getIndexPath() {
6
+ return path.join(getProjectRoot(), "index.js");
7
+ }
8
+
9
+ function readIndex() {
10
+ return fs.readFileSync(getIndexPath(), "utf8");
11
+ }
12
+
13
+ function writeIndex(content) {
14
+ fs.writeFileSync(getIndexPath(), content, "utf8");
15
+ }
16
+
17
+ export function addImport(importStatement) {
18
+ let content = readIndex();
19
+ if (!content.includes(importStatement.trim())) {
20
+ content = importStatement.trim() + "\n" + content;
21
+ writeIndex(content);
22
+ }
23
+ }
24
+
25
+ export function addRouteRegistration(importLine, useLine) {
26
+ let content = readIndex();
27
+
28
+ if (!content.includes(importLine.trim())) {
29
+ content = importLine.trim() + "\n" + content;
30
+ }
31
+
32
+ if (!content.includes(useLine.trim())) {
33
+ if (content.includes("app.listen(port")) {
34
+ content = content.replace(
35
+ "app.listen(port",
36
+ useLine.trim() + "\n" + "app.listen(port"
37
+ );
38
+ } else if (content.includes("server.listen(port")) {
39
+ content = content.replace(
40
+ "server.listen(port",
41
+ useLine.trim() + "\n" + "server.listen(port"
42
+ );
43
+ } else if (content.match(/^startServer\(/m)) {
44
+ content = content.replace(
45
+ /^startServer\(/m,
46
+ useLine.trim() + "\n" + "startServer("
47
+ );
48
+ }
49
+ }
50
+
51
+ writeIndex(content);
52
+ }
53
+
54
+ export function insertBeforeListen(codeBlock) {
55
+ let content = readIndex();
56
+ const listenMatch = content.match(/(app|server)\.listen\(port/) || content.match(/^startServer\(/m);
57
+ if (listenMatch) {
58
+ content = content.replace(
59
+ listenMatch[0],
60
+ codeBlock.trim() + "\n\n" + listenMatch[0]
61
+ );
62
+ } else {
63
+ content += "\n" + codeBlock.trim() + "\n";
64
+ }
65
+ writeIndex(content);
66
+ }
67
+
68
+ export function replaceAppListen(newListenBlock) {
69
+ let content = readIndex();
70
+
71
+ // If using startServer() pattern, just swap app.listen → server.listen inside it
72
+ if (content.includes("startServer(")) {
73
+ content = content.replace(
74
+ /const listener = app\.listen\(tryPort\)/,
75
+ "const listener = server.listen(tryPort)"
76
+ );
77
+ // Also swap standalone app.listen(tryPort) if no 'const listener' pattern
78
+ if (!content.includes("server.listen(tryPort)")) {
79
+ content = content.replace(
80
+ /app\.listen\(tryPort\)/,
81
+ "server.listen(tryPort)"
82
+ );
83
+ }
84
+ } else {
85
+ content = content.replace(/app\.listen\(port[\s\S]*?\}\);?\n?/m, "");
86
+ if (!content.includes("server.listen(port")) {
87
+ content += "\n" + newListenBlock.trim() + "\n";
88
+ }
89
+ }
90
+ writeIndex(content);
91
+ }
92
+
93
+ export function insertBeforeRoutes(codeBlock) {
94
+ let content = readIndex();
95
+
96
+ // Insert before startServer() or the start server section
97
+ const markers = [
98
+ "// ── Start Server",
99
+ "// ── Helpers",
100
+ "startServer(",
101
+ ];
102
+ let inserted = false;
103
+ for (const marker of markers) {
104
+ const idx = content.indexOf(marker);
105
+ if (idx !== -1) {
106
+ content =
107
+ content.slice(0, idx) +
108
+ codeBlock.trim() +
109
+ "\n\n" +
110
+ content.slice(idx);
111
+ inserted = true;
112
+ break;
113
+ }
114
+ }
115
+
116
+ if (!inserted) {
117
+ // Fallback: before first app.use that isn't middleware
118
+ const routeUseMatch = content.match(/app\.use\(['"]\/(?!_dev)/);
119
+ if (routeUseMatch) {
120
+ const idx = content.indexOf(routeUseMatch[0]);
121
+ content =
122
+ content.slice(0, idx) +
123
+ codeBlock.trim() +
124
+ "\n\n" +
125
+ content.slice(idx);
126
+ } else {
127
+ content += "\n" + codeBlock.trim() + "\n";
128
+ }
129
+ }
130
+
131
+ writeIndex(content);
132
+ }
133
+
134
+ export { readIndex, writeIndex, getIndexPath };
package/lib/names.js ADDED
@@ -0,0 +1,21 @@
1
+ export function toCamelCase(str) {
2
+ return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
3
+ }
4
+
5
+ export function toPascalCase(str) {
6
+ const camel = toCamelCase(str);
7
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
8
+ }
9
+
10
+ export function sanitizeName(input) {
11
+ if (!input || typeof input !== "string") {
12
+ throw new Error("Name is required.");
13
+ }
14
+ const trimmed = input.trim();
15
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(trimmed)) {
16
+ throw new Error(
17
+ `Invalid name "${trimmed}". Use only letters, numbers, hyphens, and underscores. Must start with a letter.`
18
+ );
19
+ }
20
+ return trimmed;
21
+ }
package/lib/project.js ADDED
@@ -0,0 +1,122 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export function getProjectRoot() {
5
+ return process.cwd();
6
+ }
7
+
8
+ export function listRoutes() {
9
+ const routesDir = path.join(getProjectRoot(), "src", "routes");
10
+ if (!fs.existsSync(routesDir)) {
11
+ return [];
12
+ }
13
+ return fs.readdirSync(routesDir).filter((f) => f.endsWith(".js"));
14
+ }
15
+
16
+ export function listModels() {
17
+ const modelsDir = path.join(getProjectRoot(), "src", "models");
18
+ if (!fs.existsSync(modelsDir)) {
19
+ return [];
20
+ }
21
+ return fs.readdirSync(modelsDir).filter((f) => f.endsWith(".js"));
22
+ }
23
+
24
+ function extractSchemaBlock(content) {
25
+ const marker = "new mongoose.Schema(";
26
+ const idx = content.indexOf(marker);
27
+ if (idx === -1) return null;
28
+ // Find the opening { after the marker
29
+ const braceStart = content.indexOf("{", idx + marker.length);
30
+ if (braceStart === -1) return null;
31
+ // Match balanced braces to find the outer schema object
32
+ let depth = 0;
33
+ for (let i = braceStart; i < content.length; i++) {
34
+ if (content[i] === "{") depth++;
35
+ else if (content[i] === "}") {
36
+ depth--;
37
+ if (depth === 0) return content.slice(braceStart + 1, i);
38
+ }
39
+ }
40
+ return null;
41
+ }
42
+
43
+ export function getModelSchema(modelName, { includeId = false } = {}) {
44
+ const modelFilePath = path.join(
45
+ getProjectRoot(),
46
+ "src",
47
+ "models",
48
+ modelName + ".js"
49
+ );
50
+ if (!fs.existsSync(modelFilePath)) {
51
+ return [];
52
+ }
53
+
54
+ const content = fs.readFileSync(modelFilePath, "utf8");
55
+ const schemaContent = extractSchemaBlock(content);
56
+
57
+ if (!schemaContent) {
58
+ return [];
59
+ }
60
+
61
+ const fieldRegex = /(\w+)\s*:\s*\{/g;
62
+ const fields = includeId ? ["_id"] : [];
63
+ let fieldMatch;
64
+ while ((fieldMatch = fieldRegex.exec(schemaContent)) !== null) {
65
+ fields.push(fieldMatch[1]);
66
+ }
67
+ return fields;
68
+ }
69
+
70
+ export function getModelSchemaDetailed(modelName) {
71
+ const modelFilePath = path.join(
72
+ getProjectRoot(),
73
+ "src",
74
+ "models",
75
+ modelName + ".js"
76
+ );
77
+ if (!fs.existsSync(modelFilePath)) {
78
+ return [];
79
+ }
80
+
81
+ const content = fs.readFileSync(modelFilePath, "utf8");
82
+ const schemaContent = extractSchemaBlock(content);
83
+ if (!schemaContent) return [];
84
+ const fields = [];
85
+ // Match each field block: fieldName: { type: ..., required: ..., unique: ..., default: ... }
86
+ const fieldRegex = /(\w+)\s*:\s*\{([^}]*)\}/g;
87
+ let fm;
88
+ while ((fm = fieldRegex.exec(schemaContent)) !== null) {
89
+ const name = fm[1];
90
+ const props = fm[2];
91
+ const typeMatch = props.match(/type:\s*([\w.]+)/);
92
+ const type = typeMatch
93
+ ? typeMatch[1].replace("mongoose.Schema.Types.", "")
94
+ : "String";
95
+ const required = /required:\s*true/.test(props);
96
+ const unique = /unique:\s*true/.test(props);
97
+ const defaultMatch = props.match(/default:\s*(.+?)(?:,\s*\w+:|$)/);
98
+ const field = { name, type, required, unique };
99
+ if (defaultMatch) {
100
+ field.default = defaultMatch[1].trim();
101
+ }
102
+ fields.push(field);
103
+ }
104
+ return fields;
105
+ }
106
+
107
+ export function checkMongoConfig() {
108
+ const envPath = path.join(getProjectRoot(), ".env.development");
109
+ return (
110
+ fs.existsSync(envPath) &&
111
+ fs.readFileSync(envPath, "utf8").includes("MONGO_URI")
112
+ );
113
+ }
114
+
115
+ export function checkRoutesDir() {
116
+ return fs.existsSync(path.join(getProjectRoot(), "src", "routes"));
117
+ }
118
+
119
+ export function checkModelsExist() {
120
+ const modelsDir = path.join(getProjectRoot(), "src", "models");
121
+ return fs.existsSync(modelsDir) && fs.readdirSync(modelsDir).length > 0;
122
+ }
@@ -0,0 +1,56 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { getProjectRoot } from "./project.js";
4
+
5
+ export function getRouteFilePath(routeFile) {
6
+ return path.join(getProjectRoot(), "src", "routes", routeFile);
7
+ }
8
+
9
+ export function addImportToRoute(routeFilePath, importStatement) {
10
+ let content = fs.readFileSync(routeFilePath, "utf8");
11
+ if (!content.includes(importStatement.trim())) {
12
+ content = importStatement.trim() + "\n" + content;
13
+ fs.writeFileSync(routeFilePath, content, "utf8");
14
+ }
15
+ return content;
16
+ }
17
+
18
+ export function insertCodeIntoRoute(routeFilePath, codeBlock) {
19
+ let content = fs.readFileSync(routeFilePath, "utf8");
20
+
21
+ // Prefer inserting before "export default router;" as it's more reliable
22
+ const exportIndex = content.lastIndexOf("export default router;");
23
+ if (exportIndex !== -1) {
24
+ content =
25
+ content.slice(0, exportIndex) +
26
+ codeBlock.trim() +
27
+ "\n\n" +
28
+ content.slice(exportIndex);
29
+ } else {
30
+ // Fallback to inserting before last "});"
31
+ const lastClose = content.lastIndexOf("});");
32
+ if (lastClose !== -1) {
33
+ content =
34
+ content.slice(0, lastClose) +
35
+ codeBlock.trim() +
36
+ "\n" +
37
+ content.slice(lastClose);
38
+ } else {
39
+ content += "\n" + codeBlock.trim() + "\n";
40
+ }
41
+ }
42
+
43
+ fs.writeFileSync(routeFilePath, content, "utf8");
44
+ }
45
+
46
+ export function detectHttpMethod(routeFilePath) {
47
+ const content = fs.readFileSync(routeFilePath, "utf8");
48
+ const methodRegex =
49
+ /router\.(get|post|put|patch|delete)\((['"`])\/.*?\2,\s*async\s*\(req,\s*res/;
50
+ const match = content.match(methodRegex);
51
+ return match ? match[1].toLowerCase() : "post";
52
+ }
53
+
54
+ export function getRequestDataSource(method) {
55
+ return method === "get" || method === "delete" ? "req.query" : "req.body";
56
+ }
package/lib/ui.js ADDED
@@ -0,0 +1,161 @@
1
+ import chalk from "chalk";
2
+ import gradient from "gradient-string";
3
+ import ora from "ora";
4
+
5
+ // Brand gradients
6
+ const brand = gradient(["#00c6ff", "#0072ff", "#7b2ff7"]);
7
+ const warm = gradient(["#f7971e", "#ffd200"]);
8
+ const cool = gradient(["#00f260", "#0575e6"]);
9
+ const fire = gradient(["#ff416c", "#ff4b2b"]);
10
+
11
+ // Colors
12
+ const dim = chalk.gray;
13
+ const accent = chalk.hex("#00c6ff");
14
+ const highlight = chalk.hex("#ffd200").bold;
15
+ const success = chalk.hex("#00f260").bold;
16
+ const warn = chalk.hex("#ffd200");
17
+ const err = chalk.hex("#ff416c").bold;
18
+ const info = chalk.hex("#0575e6");
19
+ const muted = chalk.hex("#888888");
20
+ const white = chalk.white;
21
+ const bold = chalk.white.bold;
22
+
23
+ const line = dim(" ─────────────────────────────────────────────────────");
24
+
25
+ const banner = `
26
+ ██╗ ██╗██████╗ ███╗ ██╗ ██████╗ ██████╗ ███████╗
27
+ ██║ ██║██╔══██╗████╗ ██║██╔═══██╗██╔══██╗██╔════╝
28
+ ███████║██████╔╝██╔██╗ ██║██║ ██║██║ ██║█████╗
29
+ ╚════██║██╔══██╗██║╚██╗██║██║ ██║██║ ██║██╔══╝
30
+ ██║██████╔╝██║ ╚████║╚██████╔╝██████╔╝███████╗
31
+ ╚═╝╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚══════╝`;
32
+
33
+ // ── Banner ──────────────────────────────────────────
34
+
35
+ export function showBanner() {
36
+ console.log(brand(banner));
37
+ console.log(line);
38
+ console.log(
39
+ dim(" ") + bold("4Brains Technologies") + dim(" | Node.js Project Generator")
40
+ );
41
+ console.log(line + "\n");
42
+ }
43
+
44
+ // ── Section Header ──────────────────────────────────
45
+
46
+ export function showHeader(command, description) {
47
+ console.log();
48
+ console.log(dim(" ┌─────────────────────────────────────────────────┐"));
49
+ console.log(
50
+ dim(" │ ") + accent("◆ ") + highlight(command) + dim(" ") + white(description) + dim(" │")
51
+ );
52
+ console.log(dim(" └─────────────────────────────────────────────────┘"));
53
+ console.log();
54
+ }
55
+
56
+ // ── Spinners ────────────────────────────────────────
57
+
58
+ export function createSpinner(text) {
59
+ return ora({
60
+ text: muted(text),
61
+ spinner: "dots12",
62
+ color: "cyan",
63
+ indent: 2,
64
+ });
65
+ }
66
+
67
+ // ── Step indicators ─────────────────────────────────
68
+
69
+ export function showStep(step, total, text) {
70
+ const progress = accent(`[${step}/${total}]`);
71
+ console.log(` ${progress} ${white(text)}`);
72
+ }
73
+
74
+ // ── Success ─────────────────────────────────────────
75
+
76
+ export function showSuccess(lines) {
77
+ console.log();
78
+ console.log(dim(" ┌─────────────────────────────────────────────────┐"));
79
+ console.log(dim(" │") + success(" ✔ Done!") + dim(new Array(42).join(" ") + "│"));
80
+ console.log(dim(" └─────────────────────────────────────────────────┘"));
81
+ console.log();
82
+ for (const l of lines) {
83
+ console.log(dim(" │ ") + white(l));
84
+ }
85
+ console.log();
86
+ }
87
+
88
+ // ── Task complete (for add-* commands) ──────────────
89
+
90
+ export function showTaskDone(title, details = []) {
91
+ console.log();
92
+ console.log(` ${success("✔")} ${cool(title)}`);
93
+ for (const d of details) {
94
+ console.log(` ${dim("→")} ${muted(d)}`);
95
+ }
96
+ console.log();
97
+ }
98
+
99
+ // ── Warning ─────────────────────────────────────────
100
+
101
+ export function showWarning(message) {
102
+ console.log(`\n ${warn("⚠")} ${warn(message)}\n`);
103
+ }
104
+
105
+ // ── Error ───────────────────────────────────────────
106
+
107
+ export function showError(message) {
108
+ console.log(`\n ${err("✖")} ${err(message)}\n`);
109
+ }
110
+
111
+ // ── Info ────────────────────────────────────────────
112
+
113
+ export function showInfo(message) {
114
+ console.log(` ${info("ℹ")} ${muted(message)}`);
115
+ }
116
+
117
+ // ── Next Steps ──────────────────────────────────────
118
+
119
+ export function showNextSteps(steps) {
120
+ console.log(accent.bold(" Next steps:\n"));
121
+ for (let i = 0; i < steps.length; i++) {
122
+ const num = highlight(` ${i + 1} `);
123
+ console.log(` ${num} ${white(steps[i])}`);
124
+ }
125
+ console.log();
126
+ console.log(line);
127
+ console.log(dim(" Happy coding! ") + brand("★ 4Brains"));
128
+ console.log(line + "\n");
129
+ }
130
+
131
+ // ── Installing deps (with spinner) ──────────────────
132
+
133
+ export async function installDeps(packages, opts = {}) {
134
+ const { execSync } = await import("child_process");
135
+ const names = Array.isArray(packages) ? packages : [packages];
136
+ const label = names.join(", ");
137
+ const spinner = createSpinner(`Installing ${label}...`);
138
+ spinner.start();
139
+ try {
140
+ execSync(`npm install ${names.join(" ")}`, {
141
+ stdio: "pipe",
142
+ ...opts,
143
+ });
144
+ spinner.succeed(success(`Installed ${label}`));
145
+ } catch (e) {
146
+ spinner.fail(err(`Failed to install ${label}`));
147
+ throw e;
148
+ }
149
+ }
150
+
151
+ // ── File action log ─────────────────────────────────
152
+
153
+ export function showFileAction(action, filePath) {
154
+ const icons = {
155
+ created: chalk.hex("#00f260")("+ "),
156
+ updated: chalk.hex("#0575e6")("~ "),
157
+ deleted: chalk.hex("#ff416c")("- "),
158
+ };
159
+ const icon = icons[action] || dim(" ");
160
+ console.log(` ${icon}${muted(filePath)}`);
161
+ }
package/package.json CHANGED
@@ -1,28 +1,18 @@
1
1
  {
2
2
  "name": "4bnode",
3
- "version": "3.5.3",
4
- "description": "A CLI tool to generate a Node.js app by 4Brains Technologies",
3
+ "version": "4.0.2",
4
+ "description": "A professional tool to generate a Node.js app by 4Brains Technologies",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "bin": {
8
- "4bnode": "index.js",
9
- "add-api": "add-api.js",
10
- "add-mongo": "add-mongo.js",
11
- "add-socket": "add-socket.js",
12
- "add-websocket": "add-websocket.js",
13
- "add-mongo-schema": "add-mongo-schema.js",
14
- "add-login": "add-login.js",
15
- "add-mongo-insert": "add-mongo-insert.js",
16
- "add-mongo-update": "add-mongo-update.js",
17
- "add-mongo-read": "add-mongo-read.js",
18
- "add-mongo-delete": "add-mongo-delete.js"
8
+ "4bnode": "index.js"
19
9
  },
20
10
  "scripts": {
21
11
  "test": "echo \"Error: no test specified\" && exit 1"
22
12
  },
23
13
  "repository": {
24
14
  "type": "git",
25
- "url": "https://github.com/pawan-4brains/4brains-node-generator"
15
+ "url": "https://github.com/pawan-4brains/4bnode"
26
16
  },
27
17
  "keywords": [
28
18
  "node",
@@ -33,8 +23,11 @@
33
23
  "Node.js skeleton"
34
24
  ],
35
25
  "author": "4Brains",
36
- "license": "ISC",
26
+ "license": "Proprietary",
37
27
  "dependencies": {
38
- "chalk": "^5.3.0"
28
+ "@inquirer/prompts": "^8.3.0",
29
+ "chalk": "^5.4.1",
30
+ "gradient-string": "^3.0.0",
31
+ "ora": "^9.3.0"
39
32
  }
40
33
  }
@@ -0,0 +1,115 @@
1
+ import React, { useState, useEffect, useCallback } from "react";
2
+ import { motion, AnimatePresence } from "framer-motion";
3
+ import { html, api, PAGE_TITLES } from "./lib.js";
4
+ import { AppContext } from "./context.js";
5
+ import { Sidebar } from "./components/Sidebar.js";
6
+ import { ToastContainer } from "./components/ToastContainer.js";
7
+ import { ConfirmDialog } from "./components/Modal.js";
8
+ import { DashboardPage } from "./pages/DashboardPage.js";
9
+ import { DatabasePage } from "./pages/DatabasePage.js";
10
+ import { SchemasPage } from "./pages/SchemasPage.js";
11
+ import { RoutesPage } from "./pages/RoutesPage.js";
12
+ import { ApiTesterPage } from "./pages/ApiTesterPage.js";
13
+ import { RealtimePage } from "./pages/RealtimePage.js";
14
+ import { SerialPortPage } from "./pages/SerialPortPage.js";
15
+ import { EnvironmentPage } from "./pages/EnvironmentPage.js";
16
+
17
+ function getHashPage() {
18
+ const hash = window.location.hash.replace("#/", "").replace("#", "");
19
+ return hash && PAGE_TITLES[hash] ? hash : "dashboard";
20
+ }
21
+
22
+ export function App() {
23
+ const [page, setPageState] = useState(getHashPage);
24
+ const [data, setData] = useState({
25
+ models: [],
26
+ routes: [],
27
+ mongoConfigured: false,
28
+ routesDirExists: false,
29
+ });
30
+ const [status, setStatus] = useState({});
31
+ const [toasts, setToasts] = useState([]);
32
+ const [confirmState, setConfirmState] = useState(null);
33
+
34
+ const showConfirm = useCallback((message, opts = {}) => {
35
+ return new Promise((resolve) => {
36
+ setConfirmState({ message, ...opts, resolve });
37
+ });
38
+ }, []);
39
+
40
+ const setPage = useCallback((p) => {
41
+ window.location.hash = "#/" + p;
42
+ }, []);
43
+
44
+ useEffect(() => {
45
+ function onHash() {
46
+ setPageState(getHashPage());
47
+ }
48
+ window.addEventListener("hashchange", onHash);
49
+ return () => window.removeEventListener("hashchange", onHash);
50
+ }, []);
51
+
52
+ const toast = useCallback((message, type = "info") => {
53
+ const id = Date.now();
54
+ setToasts((prev) => [...prev, { id, message, type }]);
55
+ setTimeout(
56
+ () => setToasts((prev) => prev.filter((t) => t.id !== id)),
57
+ 3500,
58
+ );
59
+ }, []);
60
+
61
+ const refresh = useCallback(async () => {
62
+ const [dashData, statusData] = await Promise.all([
63
+ api("/dashboard"),
64
+ api("/status"),
65
+ ]);
66
+ setData(dashData);
67
+ setStatus(statusData);
68
+ }, []);
69
+
70
+ useEffect(() => {
71
+ refresh().catch(() => {});
72
+ }, []);
73
+
74
+ const pages = {
75
+ dashboard: html`<${DashboardPage} />`,
76
+ database: html`<${DatabasePage} />`,
77
+ schemas: html`<${SchemasPage} />`,
78
+ routes: html`<${RoutesPage} />`,
79
+ "api-tester": html`<${ApiTesterPage} />`,
80
+ realtime: html`<${RealtimePage} />`,
81
+ serialport: html`<${SerialPortPage} />`,
82
+ env: html`<${EnvironmentPage} />`,
83
+ };
84
+
85
+ return html`<${AppContext.Provider}
86
+ value=${{ page, setPage, data, status, toasts, toast, refresh, showConfirm }}
87
+ >
88
+ <${Sidebar} />
89
+ <div className="main">
90
+ <div className="topbar">
91
+ <h2>${PAGE_TITLES[page] || "Dashboard"}</h2>
92
+ <span className="topbar-badge">Development</span>
93
+ </div>
94
+ <div className="content">
95
+ <${AnimatePresence} mode="wait">
96
+ <${motion.div} key=${page}
97
+ initial=${{ opacity: 0, y: 8 }}
98
+ animate=${{ opacity: 1, y: 0 }}
99
+ exit=${{ opacity: 0, y: -8 }}
100
+ transition=${{ duration: 0.15 }}>
101
+ ${pages[page] || pages.dashboard}
102
+ <//>
103
+ <//>
104
+ </div>
105
+ </div>
106
+ <${ToastContainer} />
107
+ ${confirmState && html`<${ConfirmDialog}
108
+ message=${confirmState.message}
109
+ confirmLabel=${confirmState.confirmLabel}
110
+ danger=${confirmState.danger}
111
+ onConfirm=${() => { confirmState.resolve(true); setConfirmState(null); }}
112
+ onCancel=${() => { confirmState.resolve(false); setConfirmState(null); }}
113
+ />`}
114
+ <//>`;
115
+ }