4bnode 4.2.0 → 4.2.1
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.
- package/add-bonjour.js +29 -1
- package/add-email.js +33 -15
- package/add-serialport.js +133 -41
- package/index.js +3 -3
- package/lib/codegen.js +1442 -36
- package/lib/codegen.test.js +233 -13
- package/package.json +1 -1
- package/skeleton/.4bnode/assets/js/App.js +116 -7
- package/skeleton/.4bnode/assets/js/components/CommandPalette.js +165 -0
- package/skeleton/.4bnode/assets/js/components/SerialWorkspace.js +634 -0
- package/skeleton/.4bnode/assets/js/components/Sidebar.js +64 -64
- package/skeleton/.4bnode/assets/js/lib.js +76 -0
- package/skeleton/.4bnode/assets/js/pages/ApiTesterPage.js +113 -53
- package/skeleton/.4bnode/assets/js/pages/DashboardPage.js +240 -403
- package/skeleton/.4bnode/assets/js/pages/DiscoveryPage.js +52 -3
- package/skeleton/.4bnode/assets/js/pages/MailPage.js +37 -19
- package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +1 -1
- package/skeleton/.4bnode/assets/js/pages/SchemasPage.js +5 -5
- package/skeleton/.4bnode/assets/js/pages/SecurityPage.js +154 -3
- package/skeleton/.4bnode/assets/js/pages/SerialPortPage.js +455 -139
- package/skeleton/.4bnode/codegen.js +1442 -36
- package/skeleton/.4bnode/dev-api.js +568 -95
- package/skeleton/.4bnode/ui.html +1241 -240
- package/skeleton/.env.example +23 -3
- package/skeleton/_gitignore +1 -0
- package/skeleton/index.js +8 -0
- package/skeleton/public/index.html +345 -237
- package/skeleton/src/discovery.js +128 -6
- package/skeleton/src/middleware/apiKey.js +63 -20
package/add-bonjour.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import path from "path";
|
|
5
|
-
import { input } from "@inquirer/prompts";
|
|
5
|
+
import { input, select } from "@inquirer/prompts";
|
|
6
|
+
import { pathToFileURL } from "url";
|
|
6
7
|
import { getProjectRoot } from "./lib/project.js";
|
|
7
8
|
import { updateEnvFile } from "./lib/env.js";
|
|
8
9
|
import { readIndex, writeIndex } from "./lib/indexFile.js";
|
|
@@ -45,6 +46,30 @@ async function main() {
|
|
|
45
46
|
fs.writeFileSync(discoveryPath, buildDiscoveryModule(), "utf8");
|
|
46
47
|
showFileAction(existed ? "updated" : "created", "src/discovery.js");
|
|
47
48
|
|
|
49
|
+
// 1b. Which network to advertise on. Load the module just written so the CLI
|
|
50
|
+
// detects networks exactly like the running app and the dashboard do.
|
|
51
|
+
let iface = "";
|
|
52
|
+
try {
|
|
53
|
+
const { listNetworkInterfaces } = await import(pathToFileURL(discoveryPath).href);
|
|
54
|
+
const nets = listNetworkInterfaces();
|
|
55
|
+
if (nets.length > 1) {
|
|
56
|
+
iface = await select({
|
|
57
|
+
message: "Several networks are connected. Which one should Bonjour advertise on?",
|
|
58
|
+
choices: [
|
|
59
|
+
...nets.map((n) => ({
|
|
60
|
+
name: `${n.label} ${n.address}${n.selfAssigned ? " (no network: self-assigned IP)" : ""}`,
|
|
61
|
+
value: n.name,
|
|
62
|
+
})),
|
|
63
|
+
{ name: "All networks", value: "" },
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
} else if (nets.length === 1) {
|
|
67
|
+
showInfo(`Network: ${nets[0].label} ${nets[0].address}`);
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Detection is best-effort; blank means "all networks".
|
|
71
|
+
}
|
|
72
|
+
|
|
48
73
|
// 2. Wire it into index.js (idempotent — a no-op if already wired).
|
|
49
74
|
try {
|
|
50
75
|
const before = readIndex();
|
|
@@ -66,6 +91,7 @@ async function main() {
|
|
|
66
91
|
|
|
67
92
|
// 3. Configuration in .env.
|
|
68
93
|
updateEnvFile(envPath, "BONJOUR_ENABLED", "on");
|
|
94
|
+
updateEnvFile(envPath, "BONJOUR_INTERFACE", iface);
|
|
69
95
|
if (name) updateEnvFile(envPath, "BONJOUR_NAME", name);
|
|
70
96
|
if (type && type !== "http") updateEnvFile(envPath, "BONJOUR_TYPE", type);
|
|
71
97
|
showFileAction("updated", ".env");
|
|
@@ -76,8 +102,10 @@ async function main() {
|
|
|
76
102
|
showTaskDone("Bonjour / mDNS discovery enabled", [
|
|
77
103
|
name ? `Service name: ${name}` : "Service name: app name + hostname",
|
|
78
104
|
`Service type: _${type}._tcp`,
|
|
105
|
+
`Network: ${iface || "all networks"} (change with BONJOUR_INTERFACE or in the dashboard)`,
|
|
79
106
|
"Advertises once the server is listening; sends goodbye packets on clean shutdown",
|
|
80
107
|
"Browse peers on the network from the /_dev dashboard → Discovery",
|
|
108
|
+
"Bonjour is off by default in new projects; this command turned it on (BONJOUR_ENABLED=on)",
|
|
81
109
|
"Disable anytime with BONJOUR_ENABLED=off in .env",
|
|
82
110
|
]);
|
|
83
111
|
}
|
package/add-email.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import os from "os";
|
|
6
|
+
import crypto from "crypto";
|
|
6
7
|
import { createRequire } from "module";
|
|
7
8
|
import { pathToFileURL } from "url";
|
|
8
9
|
import { input, select, confirm, password } from "@inquirer/prompts";
|
|
@@ -15,6 +16,8 @@ import {
|
|
|
15
16
|
isOfficialMailDomain,
|
|
16
17
|
OFFICIAL_MAIL_DOMAIN_PASSWORD,
|
|
17
18
|
OFFICIAL_MAIL_NOTIFY,
|
|
19
|
+
SANCHARAK_API_URL,
|
|
20
|
+
sancharakKeyProblem,
|
|
18
21
|
} from "./lib/codegen.js";
|
|
19
22
|
|
|
20
23
|
// The 4brains.in domain can't be configured directly — require the shared
|
|
@@ -48,11 +51,18 @@ async function sendOfficialNotify(root, ctx) {
|
|
|
48
51
|
"Time: " + new Date().toISOString(),
|
|
49
52
|
].join("\n");
|
|
50
53
|
|
|
51
|
-
if (ctx.kind === "
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
if (ctx.kind === "sancharak") {
|
|
55
|
+
const res = await fetch(SANCHARAK_API_URL, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: {
|
|
58
|
+
Authorization: "Bearer " + ctx.apiKey,
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
"Idempotency-Key": crypto.randomUUID(),
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify({ from: ctx.from, to: [OFFICIAL_MAIL_NOTIFY], subject, text, stream: "tx" }),
|
|
63
|
+
signal: AbortSignal.timeout(15000),
|
|
64
|
+
});
|
|
65
|
+
if (!res.ok) throw new Error("Sancharak " + res.status + ": " + (await res.text()).slice(0, 200));
|
|
56
66
|
return;
|
|
57
67
|
}
|
|
58
68
|
const nodemailer = (await import(pathToFileURL(require.resolve("nodemailer")).href)).default;
|
|
@@ -86,17 +96,23 @@ async function main() {
|
|
|
86
96
|
let dep;
|
|
87
97
|
let notifyCtx = null; // set when an authorized 4brains.in sender is configured
|
|
88
98
|
|
|
89
|
-
if (preset.type === "
|
|
90
|
-
//
|
|
91
|
-
const apiKey = (await password({
|
|
92
|
-
|
|
99
|
+
if (preset.type === "sancharak") {
|
|
100
|
+
// Sancharak HTTP API: an API key + From (+ optional Reply-to). No package needed.
|
|
101
|
+
const apiKey = (await password({
|
|
102
|
+
message: "Sancharak API key (snk_...):",
|
|
103
|
+
mask: "*",
|
|
104
|
+
validate: (v) => (!v.trim() ? true : sancharakKeyProblem(v) || true),
|
|
105
|
+
})).trim();
|
|
106
|
+
const from = (await input({ message: "From address (e.g. Your App <hello@yourdomain.com>):", default: "" })).trim();
|
|
107
|
+
const replyTo = (await input({ message: "Reply-to address (optional):", default: "" })).trim();
|
|
93
108
|
await ensureOfficialAllowed(from);
|
|
94
109
|
if (isOfficialMailDomain(from)) updateEnvFile(envPath, "OFFICIAL_MAIL_AUTHORIZED", "1");
|
|
95
|
-
if (apiKey) updateEnvFile(envPath, "
|
|
110
|
+
if (apiKey) updateEnvFile(envPath, "SANCHARAK_API_KEY", apiKey);
|
|
96
111
|
if (from) updateEnvFile(envPath, "MAIL_FROM", from);
|
|
97
|
-
|
|
112
|
+
if (replyTo) updateEnvFile(envPath, "MAIL_REPLY_TO", replyTo);
|
|
113
|
+
dep = null;
|
|
98
114
|
summary.push(from ? `From: ${from}` : "From: (set MAIL_FROM in .env)");
|
|
99
|
-
if (isOfficialMailDomain(from) && apiKey) notifyCtx = { kind: "
|
|
115
|
+
if (isOfficialMailDomain(from) && apiKey) notifyCtx = { kind: "sancharak", from, apiKey };
|
|
100
116
|
} else {
|
|
101
117
|
// Custom SMTP → nodemailer. TLS selects the port (465 vs 587).
|
|
102
118
|
const host = (await input({ message: "SMTP host:", default: preset.host || "" })).trim();
|
|
@@ -123,15 +139,17 @@ async function main() {
|
|
|
123
139
|
showFileAction("updated", ".env");
|
|
124
140
|
|
|
125
141
|
fs.mkdirSync(dir, { recursive: true });
|
|
126
|
-
// Rewrite the mailer
|
|
127
|
-
|
|
142
|
+
// Rewrite the mailer if missing, on the old single-backend version, or on the
|
|
143
|
+
// old Postmaster (Resend) version that Sancharak replaces.
|
|
144
|
+
const existingMailer = fs.existsSync(mailerPath) ? fs.readFileSync(mailerPath, "utf8") : "";
|
|
145
|
+
if (!existingMailer || !existingMailer.includes("MAIL_PROVIDER") || existingMailer.includes("import('resend')")) {
|
|
128
146
|
fs.writeFileSync(mailerPath, buildMailerService(), "utf8");
|
|
129
147
|
showFileAction(fs.existsSync(mailerPath) ? "updated" : "created", "src/services/mailer.js");
|
|
130
148
|
} else {
|
|
131
149
|
showInfo("src/services/mailer.js already up to date — keeping it.");
|
|
132
150
|
}
|
|
133
151
|
|
|
134
|
-
await installDeps(dep);
|
|
152
|
+
if (dep) await installDeps(dep);
|
|
135
153
|
|
|
136
154
|
if (notifyCtx) {
|
|
137
155
|
try {
|
package/add-serialport.js
CHANGED
|
@@ -1,71 +1,163 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import fs from "fs";
|
|
3
4
|
import path from "path";
|
|
4
|
-
import {
|
|
5
|
+
import { pathToFileURL } from "url";
|
|
6
|
+
import { input, select, confirm } from "@inquirer/prompts";
|
|
5
7
|
import { getProjectRoot } from "./lib/project.js";
|
|
6
|
-
import {
|
|
7
|
-
import { addImport, insertBeforeListen } from "./lib/indexFile.js";
|
|
8
|
+
import { readIndex, writeIndex } from "./lib/indexFile.js";
|
|
8
9
|
import {
|
|
9
10
|
showHeader,
|
|
10
11
|
showTaskDone,
|
|
11
12
|
showError,
|
|
13
|
+
showInfo,
|
|
12
14
|
showFileAction,
|
|
13
15
|
installDeps,
|
|
14
16
|
} from "./lib/ui.js";
|
|
17
|
+
import {
|
|
18
|
+
buildSerialDeviceModule,
|
|
19
|
+
buildSerialDevicesModule,
|
|
20
|
+
buildSerialRouter,
|
|
21
|
+
normalizeSerialDevice,
|
|
22
|
+
wireSerialIntoIndex,
|
|
23
|
+
SERIAL_DEPS,
|
|
24
|
+
SERIAL_BAUD_RATES,
|
|
25
|
+
} from "./lib/codegen.js";
|
|
15
26
|
|
|
16
27
|
async function main() {
|
|
17
|
-
showHeader("add-serialport", "
|
|
28
|
+
showHeader("add-serialport", "Talk to Arduino / ESP32 boards over USB serial");
|
|
18
29
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
showInfo(
|
|
31
|
+
"Devices are found by USB serial number, USB vendor id, or a firmware handshake " +
|
|
32
|
+
"(ID?), never a fixed COM port. They reconnect by themselves after a reset or unplug.",
|
|
33
|
+
);
|
|
23
34
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
validate: (v) => (!isNaN(parseInt(v, 10)) ? true : "Enter a valid number."),
|
|
28
|
-
});
|
|
35
|
+
const root = getProjectRoot();
|
|
36
|
+
const serialDir = path.join(root, "src", "serial");
|
|
37
|
+
const configFile = path.join(serialDir, "devices.json");
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
// 1. Dependencies first, so the port scan below can run.
|
|
40
|
+
await installDeps(SERIAL_DEPS);
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
// 2. Generated files (never overwrite ones the user may have edited).
|
|
43
|
+
const files = [
|
|
44
|
+
[path.join(serialDir, "SerialDevice.js"), buildSerialDeviceModule()],
|
|
45
|
+
[path.join(serialDir, "devices.js"), buildSerialDevicesModule()],
|
|
46
|
+
[path.join(root, "src", "routes", "serial.js"), buildSerialRouter()],
|
|
47
|
+
[configFile, "[]\n"],
|
|
48
|
+
];
|
|
49
|
+
for (const [file, content] of files) {
|
|
50
|
+
if (fs.existsSync(file)) continue;
|
|
51
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
52
|
+
fs.writeFileSync(file, content, "utf8");
|
|
53
|
+
showFileAction("created", path.relative(root, file));
|
|
54
|
+
}
|
|
35
55
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
56
|
+
// 3. Wire index.js (idempotent).
|
|
57
|
+
const before = readIndex();
|
|
58
|
+
const after = wireSerialIntoIndex(before);
|
|
59
|
+
if (after !== before) {
|
|
60
|
+
writeIndex(after);
|
|
61
|
+
showFileAction("updated", "index.js");
|
|
62
|
+
}
|
|
42
63
|
|
|
43
|
-
|
|
64
|
+
// 4. Describe the first device, using the boards plugged in right now.
|
|
65
|
+
console.log();
|
|
66
|
+
const mod = await import(pathToFileURL(path.join(serialDir, "SerialDevice.js")).href);
|
|
67
|
+
let ports = [];
|
|
68
|
+
try {
|
|
69
|
+
ports = (await mod.listPorts()).filter((p) => p.usb);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
showInfo("Could not scan ports (" + e.message + "). You can enter the details by hand.");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Auto-detect: nothing plugged in yet? Wait for a board to appear.
|
|
75
|
+
if (!ports.length && (await confirm({ message: "No USB serial board found. Wait while you plug one in?", default: true }))) {
|
|
76
|
+
showInfo("Plug in your board now (waiting up to 60s)…");
|
|
77
|
+
for (let i = 0; i < 60 && !ports.length; i++) {
|
|
78
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
79
|
+
try {
|
|
80
|
+
ports = (await mod.listPorts()).filter((p) => p.usb);
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
83
|
+
if (ports.length) showInfo(`Detected ${ports.map((p) => `${p.path} (${p.vendor || p.manufacturer || "unknown chip"})`).join(", ")}`);
|
|
84
|
+
else showInfo("Still nothing. Check the cable (some are charge-only) and the driver, or enter the details by hand.");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let port = null;
|
|
88
|
+
if (ports.length === 1 && ports[0]) {
|
|
89
|
+
port = ports[0];
|
|
90
|
+
showInfo(`Using ${port.path}`);
|
|
91
|
+
} else if (ports.length) {
|
|
92
|
+
port = await select({
|
|
93
|
+
message: "Which board?",
|
|
94
|
+
choices: [
|
|
95
|
+
...ports.map((p) => ({
|
|
96
|
+
name: `${p.path} ${p.vendor || p.manufacturer || "unknown chip"} ${p.vendorId}:${p.productId || "????"}${p.serialNumber ? " SN " + p.serialNumber : " (no serial number)"}`,
|
|
97
|
+
value: p,
|
|
98
|
+
})),
|
|
99
|
+
{ name: "It's not plugged in / enter details by hand", value: null },
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
}
|
|
44
103
|
|
|
45
|
-
|
|
46
|
-
|
|
104
|
+
const baudRate = await select({
|
|
105
|
+
message: "Baud rate (must match the firmware's Serial.begin):",
|
|
106
|
+
default: 115200,
|
|
107
|
+
choices: SERIAL_BAUD_RATES.map((b) => ({ name: String(b), value: b })),
|
|
47
108
|
});
|
|
48
109
|
|
|
49
|
-
|
|
50
|
-
|
|
110
|
+
let identity = null;
|
|
111
|
+
if (port && (await confirm({ message: `Ask ${port.path} for its identity (sends ID?, resets most boards)?`, default: !port.serialNumber }))) {
|
|
112
|
+
identity = await mod.probeIdentity(port.path, { baudRate });
|
|
113
|
+
showInfo(identity ? `Board says: ${identity}` : "No reply to ID?. Check the baud rate and firmware.");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const strategy = await select({
|
|
117
|
+
message: "Find this device by:",
|
|
118
|
+
default: port && port.serialNumber ? "serialNumber" : identity ? "identity" : port ? "vendorId" : "identity",
|
|
119
|
+
choices: [
|
|
120
|
+
{ name: "USB serial number (best for identical boards)", value: "serialNumber" },
|
|
121
|
+
{ name: "Firmware identity (reply to ID?)", value: "identity" },
|
|
122
|
+
{ name: "USB vendor id (one board of that type)", value: "vendorId" },
|
|
123
|
+
{ name: "Fixed USB port (last resort: identical boards with no serial number or ID reply)", value: "path" },
|
|
124
|
+
],
|
|
51
125
|
});
|
|
52
|
-
} catch (error) {
|
|
53
|
-
console.error("Error initializing SerialPort:", error);
|
|
54
|
-
}`;
|
|
55
126
|
|
|
56
|
-
|
|
127
|
+
const match = {};
|
|
128
|
+
if (strategy === "serialNumber") {
|
|
129
|
+
match.serialNumber = await input({ message: "USB serial number:", default: (port && port.serialNumber) || "" });
|
|
130
|
+
} else if (strategy === "identity") {
|
|
131
|
+
match.identity = await input({ message: "Reply contains:", default: identity ? identity.split(":")[0] : "" });
|
|
132
|
+
if (port && port.vendorId) match.vendorId = port.vendorId;
|
|
133
|
+
} else if (strategy === "vendorId") {
|
|
134
|
+
match.vendorId = await input({ message: "USB vendor id (4 hex digits):", default: (port && port.vendorId) || "" });
|
|
135
|
+
if (port && port.productId) match.productId = port.productId;
|
|
136
|
+
} else {
|
|
137
|
+
match.path = await input({ message: "Port path:", default: (port && port.path) || (process.platform === "win32" ? "COM3" : "/dev/ttyUSB0") });
|
|
138
|
+
}
|
|
57
139
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
140
|
+
const name = await input({
|
|
141
|
+
message: "Device name (used in code and URLs):",
|
|
142
|
+
default: identity ? identity.split(":")[0].toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/^-+|-+$/g, "") || "device" : "device",
|
|
143
|
+
validate: (v) => (/^[a-zA-Z][a-zA-Z0-9_-]{0,39}$/.test(v.trim()) ? true : "Start with a letter; letters, numbers, - or _ only."),
|
|
144
|
+
});
|
|
62
145
|
|
|
63
|
-
|
|
146
|
+
const device = normalizeSerialDevice({ name: name.trim(), match, baudRate });
|
|
147
|
+
const list = (() => {
|
|
148
|
+
try { const l = JSON.parse(fs.readFileSync(configFile, "utf8")); return Array.isArray(l) ? l : []; } catch { return []; }
|
|
149
|
+
})().filter((d) => d.name !== device.name);
|
|
150
|
+
list.push(device);
|
|
151
|
+
fs.writeFileSync(configFile, JSON.stringify(list, null, 2) + "\n", "utf8");
|
|
152
|
+
showFileAction("updated", "src/serial/devices.json");
|
|
64
153
|
|
|
65
|
-
showTaskDone("
|
|
66
|
-
`
|
|
67
|
-
|
|
68
|
-
|
|
154
|
+
showTaskDone("Serial device added", [
|
|
155
|
+
`Device "${device.name}" found by ${Object.entries(device.match).map(([k, v]) => k + "=" + v).join(", ")} at ${device.baudRate} baud`,
|
|
156
|
+
"Connects at startup and reconnects automatically after a reset or unplug",
|
|
157
|
+
`HTTP: POST /api/serial/devices/${device.name}/send {"command": "ID?"} (needs x-api-key)`,
|
|
158
|
+
`Code: await getDevice('${device.name}').send('LED:ON') from ./src/serial/devices.js`,
|
|
159
|
+
"New boards are detected when plugged in and added automatically (SERIAL_AUTO_ADD=off to disable)",
|
|
160
|
+
"Manage devices and watch live data in the /_dev dashboard → SerialPort",
|
|
69
161
|
]);
|
|
70
162
|
}
|
|
71
163
|
|
package/index.js
CHANGED
|
@@ -245,7 +245,7 @@ const addOptions = [
|
|
|
245
245
|
script: "add-websocket.js",
|
|
246
246
|
},
|
|
247
247
|
{
|
|
248
|
-
name: "SerialPort
|
|
248
|
+
name: "SerialPort Arduino/ESP32 devices (auto-detect + reconnect)",
|
|
249
249
|
value: "serialport",
|
|
250
250
|
script: "add-serialport.js",
|
|
251
251
|
},
|
|
@@ -255,7 +255,7 @@ const addOptions = [
|
|
|
255
255
|
script: "add-email.js",
|
|
256
256
|
},
|
|
257
257
|
{
|
|
258
|
-
name: "Bonjour/mDNS Advertise
|
|
258
|
+
name: "Bonjour/mDNS Advertise on the LAN (off by default; pick the network)",
|
|
259
259
|
value: "bonjour",
|
|
260
260
|
script: "add-bonjour.js",
|
|
261
261
|
},
|
|
@@ -348,7 +348,7 @@ function showHelp() {
|
|
|
348
348
|
console.log(` ${c("add security")} Helmet, rate-limit, RBAC, validation`);
|
|
349
349
|
console.log(` ${c("add socket")} Socket.IO integration`);
|
|
350
350
|
console.log(` ${c("add websocket")} Native WebSocket support`);
|
|
351
|
-
console.log(` ${c("add serialport")}
|
|
351
|
+
console.log(` ${c("add serialport")} Arduino/ESP32 serial devices (auto-detect + reconnect)`);
|
|
352
352
|
console.log(` ${c("add email")} Transactional email (Brevo, SendGrid, SMTP)`);
|
|
353
353
|
console.log(` ${c("add bonjour")} Advertise on the local network (Bonjour/mDNS)`);
|
|
354
354
|
console.log();
|