@naviyra/cloud-panel 0.0.0-stage → 0.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.
- package/.env.example +40 -0
- package/README.md +145 -2
- package/apps/agent/package.json +13 -0
- package/apps/agent/src/cloudflare.mjs +733 -0
- package/apps/agent/src/native-worker.mjs +52 -0
- package/apps/agent/src/native.mjs +457 -0
- package/apps/agent/src/runtime.mjs +760 -0
- package/apps/agent/src/server.mjs +195 -0
- package/apps/agent/src/static-server.mjs +53 -0
- package/apps/panel/AGENTS.md +9 -0
- package/apps/panel/CLAUDE.md +1 -0
- package/apps/panel/app/api/[...path]/route.js +445 -0
- package/apps/panel/app/globals.css +2057 -0
- package/apps/panel/app/icon.png +0 -0
- package/apps/panel/app/layout.jsx +18 -0
- package/apps/panel/app/login/page.jsx +108 -0
- package/apps/panel/app/page.jsx +33 -0
- package/apps/panel/app/ui/cloudflare.jsx +926 -0
- package/apps/panel/app/ui/dashboard.jsx +1487 -0
- package/apps/panel/app/ui/databases.jsx +293 -0
- package/apps/panel/app/ui/skeleton.jsx +196 -0
- package/apps/panel/app/ui/theme.jsx +35 -0
- package/apps/panel/lib/agent.mjs +83 -0
- package/apps/panel/lib/auth.mjs +48 -0
- package/apps/panel/lib/database-runtime.mjs +243 -0
- package/apps/panel/lib/databases.mjs +292 -0
- package/apps/panel/lib/db.mjs +6 -0
- package/apps/panel/next.config.mjs +8 -0
- package/apps/panel/package.json +22 -0
- package/apps/panel/public/naviyra-logo.png +0 -0
- package/bin/naviyra.mjs +215 -0
- package/db/001_init.sql +28 -0
- package/db/002_native_runtimes.sql +3 -0
- package/db/003_databases.sql +8 -0
- package/db/004_database_engines.sql +4 -0
- package/deploy/naviyra-agent.service +17 -0
- package/docker-compose.yml +18 -0
- package/docs/managed-runtime.md +52 -0
- package/docs/native-hosts.md +48 -0
- package/package.json +61 -4
- package/packages/runtime/artifacts.json +32 -0
- package/packages/runtime/index.cjs +526 -0
- package/packages/runtime/index.mjs +10 -0
- package/packages/runtime/package.json +6 -0
- package/packages/shared/index.mjs +85 -0
- package/packages/shared/package.json +9 -0
- package/scripts/migrate.mjs +18 -0
- package/scripts/panel-control.mjs +195 -0
- package/scripts/postgres-local.mjs +97 -0
- package/scripts/runtime-download-manifest.mjs +30 -0
- package/scripts/runtime.mjs +20 -0
- package/scripts/seed.mjs +35 -0
- package/scripts/service-supervisor.mjs +164 -0
- package/scripts/setup-local.mjs +215 -0
- package/scripts/start-naviyra.mjs +303 -0
- package/scripts/windows-boot.mjs +259 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
8
|
+
|
|
9
|
+
export function envAssignment(name, value) {
|
|
10
|
+
const plain = /^[A-Za-z0-9@._:/+-]+$/.test(value);
|
|
11
|
+
const encoded = plain
|
|
12
|
+
? value
|
|
13
|
+
: `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
14
|
+
return `${name}=${encoded}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function applySettings(template, settings) {
|
|
18
|
+
const replacements = [
|
|
19
|
+
[
|
|
20
|
+
"JWT_SECRET=replace-with-at-least-32-random-characters",
|
|
21
|
+
envAssignment("JWT_SECRET", settings.jwt),
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
"AGENT_TOKEN=replace-with-at-least-32-random-characters",
|
|
25
|
+
envAssignment("AGENT_TOKEN", settings.token),
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
"ADMIN_PASSWORD=replace-with-a-strong-password",
|
|
29
|
+
envAssignment("ADMIN_PASSWORD", settings.password),
|
|
30
|
+
],
|
|
31
|
+
[
|
|
32
|
+
"ADMIN_EMAIL=admin@example.com",
|
|
33
|
+
envAssignment("ADMIN_EMAIL", settings.email),
|
|
34
|
+
],
|
|
35
|
+
["POSTGRES_PORT=55432", `POSTGRES_PORT=${settings.postgresPort}`],
|
|
36
|
+
[
|
|
37
|
+
"postgresql://naviyra:naviyra@127.0.0.1:55432/naviyra",
|
|
38
|
+
`postgresql://naviyra:naviyra@127.0.0.1:${settings.postgresPort}/naviyra`,
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
"AGENT_URL=http://127.0.0.1:4001",
|
|
42
|
+
`AGENT_URL=http://127.0.0.1:${settings.agentPort}`,
|
|
43
|
+
],
|
|
44
|
+
["AGENT_PORT=4001", `AGENT_PORT=${settings.agentPort}`],
|
|
45
|
+
["PANEL_PORT=3000", `PANEL_PORT=${settings.panelPort}`],
|
|
46
|
+
];
|
|
47
|
+
let env = template;
|
|
48
|
+
for (const [from, to] of replacements) {
|
|
49
|
+
if (!env.includes(from))
|
|
50
|
+
throw Error(`Environment template is missing ${from.split("=")[0]}.`);
|
|
51
|
+
env = env.replace(from, to);
|
|
52
|
+
}
|
|
53
|
+
return env;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function askPort(ask, label, fallback, used, log) {
|
|
57
|
+
for (;;) {
|
|
58
|
+
const raw = (await ask(label)).trim();
|
|
59
|
+
const port = raw === "" ? fallback : Number(raw);
|
|
60
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
61
|
+
log("Enter a port from 1 to 65535.");
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (used.includes(port)) {
|
|
65
|
+
log("That port is already used by another Naviyra service.");
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
return port;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function promptSettings(ask, askSecret, log = console.log) {
|
|
73
|
+
log("Enter the panel settings. Press Enter to accept a default.");
|
|
74
|
+
let email = "";
|
|
75
|
+
for (;;) {
|
|
76
|
+
const typed = (await ask("Administrator email [admin@example.com]: ")).trim();
|
|
77
|
+
email = (typed || "admin@example.com").toLowerCase();
|
|
78
|
+
if (emailPattern.test(email)) break;
|
|
79
|
+
log("Enter an email address.");
|
|
80
|
+
}
|
|
81
|
+
let password = "";
|
|
82
|
+
for (;;) {
|
|
83
|
+
password = await askSecret("Administrator password (12+ characters): ");
|
|
84
|
+
if (password.length < 12) {
|
|
85
|
+
log("Use at least 12 characters.");
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (password !== (await askSecret("Confirm password: "))) {
|
|
89
|
+
log("Passwords do not match.");
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
const agentPort = await askPort(
|
|
95
|
+
ask,
|
|
96
|
+
"Agent port [4001]: ",
|
|
97
|
+
4001,
|
|
98
|
+
[3000, 55432],
|
|
99
|
+
log,
|
|
100
|
+
);
|
|
101
|
+
return {
|
|
102
|
+
email,
|
|
103
|
+
password,
|
|
104
|
+
panelPort: 3000,
|
|
105
|
+
agentPort,
|
|
106
|
+
postgresPort: 55432,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function defaultAsk(question) {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const rl = createInterface({
|
|
113
|
+
input: process.stdin,
|
|
114
|
+
output: process.stdout,
|
|
115
|
+
});
|
|
116
|
+
rl.question(question)
|
|
117
|
+
.then((answer) => {
|
|
118
|
+
rl.close();
|
|
119
|
+
resolve(answer);
|
|
120
|
+
})
|
|
121
|
+
.catch((error) => {
|
|
122
|
+
rl.close();
|
|
123
|
+
reject(error);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function defaultAskSecret(question) {
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
const input = process.stdin;
|
|
131
|
+
const output = process.stdout;
|
|
132
|
+
output.write(question);
|
|
133
|
+
input.setRawMode(true);
|
|
134
|
+
input.resume();
|
|
135
|
+
let value = "";
|
|
136
|
+
const finish = (result) => {
|
|
137
|
+
input.setRawMode(false);
|
|
138
|
+
input.pause();
|
|
139
|
+
input.off("data", onData);
|
|
140
|
+
output.write("\n");
|
|
141
|
+
resolve(result);
|
|
142
|
+
};
|
|
143
|
+
const onData = (chunk) => {
|
|
144
|
+
for (const char of chunk.toString("utf8")) {
|
|
145
|
+
if (char === "\u0003") {
|
|
146
|
+
input.setRawMode(false);
|
|
147
|
+
process.exit(130);
|
|
148
|
+
}
|
|
149
|
+
if (char === "\r" || char === "\n") {
|
|
150
|
+
finish(value);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (char === "\u007f" || char === "\b") {
|
|
154
|
+
if (value) output.write("\b \b");
|
|
155
|
+
value = value.slice(0, -1);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (char < " ") continue;
|
|
159
|
+
value += char;
|
|
160
|
+
output.write("*");
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
input.on("data", onData);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function createLocalEnv(options = {}) {
|
|
168
|
+
const readExisting = options.readExisting ?? (() => readFile(".env"));
|
|
169
|
+
try {
|
|
170
|
+
await readExisting();
|
|
171
|
+
console.log(".env already exists; left unchanged.");
|
|
172
|
+
return { created: false };
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error.code !== "ENOENT") throw error;
|
|
175
|
+
}
|
|
176
|
+
const secret = () => randomBytes(32).toString("hex");
|
|
177
|
+
const tty = options.tty ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
178
|
+
const settings = tty
|
|
179
|
+
? await promptSettings(
|
|
180
|
+
options.ask ?? defaultAsk,
|
|
181
|
+
options.askSecret ?? defaultAskSecret,
|
|
182
|
+
options.log ?? console.log,
|
|
183
|
+
)
|
|
184
|
+
: {
|
|
185
|
+
email: "admin@example.com",
|
|
186
|
+
password: secret().slice(0, 24),
|
|
187
|
+
panelPort: 3000,
|
|
188
|
+
agentPort: 4001,
|
|
189
|
+
postgresPort: 55432,
|
|
190
|
+
};
|
|
191
|
+
const template = await (options.readTemplate ?? (() => readFile(".env.example", "utf8")))();
|
|
192
|
+
const env = applySettings(template, {
|
|
193
|
+
...settings,
|
|
194
|
+
jwt: secret(),
|
|
195
|
+
token: secret(),
|
|
196
|
+
});
|
|
197
|
+
await (options.writeEnv ??
|
|
198
|
+
((text) => writeFile(".env", text, { flag: "wx", mode: 0o600 })))(env);
|
|
199
|
+
await mkdir(".runtime", { recursive: true });
|
|
200
|
+
const login = `Local installation\nURL: http://127.0.0.1:${settings.panelPort}\nEmail: ${settings.email}\nPassword: ${settings.password}\n`;
|
|
201
|
+
await (options.writeLogin ??
|
|
202
|
+
((text) => writeFile(".runtime/local-login.txt", text, { mode: 0o600 })))(login);
|
|
203
|
+
console.log("Created .env. Sign-in details saved to .runtime/local-login.txt.");
|
|
204
|
+
return { created: true, settings };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const isMain =
|
|
208
|
+
process.argv[1] &&
|
|
209
|
+
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
210
|
+
if (isMain) {
|
|
211
|
+
createLocalEnv().catch((error) => {
|
|
212
|
+
console.error(error.message);
|
|
213
|
+
process.exitCode = 1;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, writeFile, unlink, access } from "node:fs/promises";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import net from "node:net";
|
|
6
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
7
|
+
|
|
8
|
+
const root = fileURLToPath(new URL("../", import.meta.url));
|
|
9
|
+
process.chdir(root);
|
|
10
|
+
const lock = path.join(root, ".runtime", "launcher.lock");
|
|
11
|
+
const children = new Set();
|
|
12
|
+
let ownsLock = false;
|
|
13
|
+
let stopping = false;
|
|
14
|
+
|
|
15
|
+
async function exists(file) {
|
|
16
|
+
try {
|
|
17
|
+
await access(file);
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function launch(args, cwd = root) {
|
|
24
|
+
const child = spawn(process.execPath, args, {
|
|
25
|
+
cwd,
|
|
26
|
+
env: process.env,
|
|
27
|
+
shell: false,
|
|
28
|
+
windowsHide: true,
|
|
29
|
+
stdio: "inherit",
|
|
30
|
+
});
|
|
31
|
+
children.add(child);
|
|
32
|
+
child.on("error", (error) => {
|
|
33
|
+
child.startError = error;
|
|
34
|
+
children.delete(child);
|
|
35
|
+
});
|
|
36
|
+
child.on("exit", () => children.delete(child));
|
|
37
|
+
return child;
|
|
38
|
+
}
|
|
39
|
+
async function command(args, cwd) {
|
|
40
|
+
const child = launch(args, cwd);
|
|
41
|
+
await new Promise((resolve, reject) => {
|
|
42
|
+
child.once("error", reject);
|
|
43
|
+
child.once("exit", (code) =>
|
|
44
|
+
code === 0
|
|
45
|
+
? resolve()
|
|
46
|
+
: reject(Error("A startup step failed. See the output above.")),
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async function stop() {
|
|
51
|
+
if (stopping) return;
|
|
52
|
+
stopping = true;
|
|
53
|
+
for (const child of children) {
|
|
54
|
+
if (child.exitCode !== null || !child.pid) continue;
|
|
55
|
+
if (process.platform === "win32") {
|
|
56
|
+
await new Promise((resolve) => {
|
|
57
|
+
const killer = spawn(
|
|
58
|
+
"taskkill.exe",
|
|
59
|
+
["/PID", String(child.pid), "/T", "/F"],
|
|
60
|
+
{ windowsHide: true, stdio: "ignore" },
|
|
61
|
+
);
|
|
62
|
+
killer.once("error", resolve);
|
|
63
|
+
killer.once("exit", resolve);
|
|
64
|
+
});
|
|
65
|
+
} else child.kill("SIGTERM");
|
|
66
|
+
}
|
|
67
|
+
if (ownsLock) await unlink(lock).catch(() => {});
|
|
68
|
+
}
|
|
69
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
70
|
+
process.once(signal, () => {
|
|
71
|
+
void stop().then(() => {
|
|
72
|
+
process.exitCode = 0;
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
async function listening(port) {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const socket = net.createConnection({ host: "127.0.0.1", port });
|
|
79
|
+
const finish = (result) => {
|
|
80
|
+
socket.destroy();
|
|
81
|
+
resolve(result);
|
|
82
|
+
};
|
|
83
|
+
socket.once("connect", () => finish(true));
|
|
84
|
+
socket.once("error", () => finish(false));
|
|
85
|
+
socket.setTimeout(1500, () => finish(false));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async function healthy(url, agent = false) {
|
|
89
|
+
try {
|
|
90
|
+
const response = await fetch(url, {
|
|
91
|
+
signal: AbortSignal.timeout(4000),
|
|
92
|
+
redirect: "manual",
|
|
93
|
+
headers: agent
|
|
94
|
+
? { Authorization: `Bearer ${process.env.AGENT_TOKEN}` }
|
|
95
|
+
: {},
|
|
96
|
+
});
|
|
97
|
+
const body = await response.json();
|
|
98
|
+
return agent
|
|
99
|
+
? response.ok && body.ok === true
|
|
100
|
+
: response.status === 401 && body.error === "Please sign in";
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function waitFor(url, child, agent = false) {
|
|
106
|
+
for (let attempt = 0; attempt < 90; attempt++) {
|
|
107
|
+
if (child.startError)
|
|
108
|
+
throw Error(
|
|
109
|
+
"Could not launch a service. Check the Node.js installation.",
|
|
110
|
+
);
|
|
111
|
+
if (child.exitCode !== null)
|
|
112
|
+
throw Error("A service stopped during startup. See the output above.");
|
|
113
|
+
if (await healthy(url, agent)) return;
|
|
114
|
+
await delay(1000);
|
|
115
|
+
}
|
|
116
|
+
throw Error("A service did not become ready within 90 attempts.");
|
|
117
|
+
}
|
|
118
|
+
function openBrowser(url) {
|
|
119
|
+
if (process.argv.includes("--no-browser")) return;
|
|
120
|
+
const file = process.platform === "win32" ? "explorer.exe" : "xdg-open";
|
|
121
|
+
const browser = spawn(file, [url], { windowsHide: true, stdio: "ignore" });
|
|
122
|
+
browser.on("error", () => console.log(`Open ${url} in your browser.`));
|
|
123
|
+
browser.unref();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function main() {
|
|
127
|
+
try {
|
|
128
|
+
console.log("Starting Naviyra…");
|
|
129
|
+
if (!(await exists(path.join(root, "node_modules", "pg"))))
|
|
130
|
+
throw Error(
|
|
131
|
+
"Run npm install once in the Naviyra folder, then double-click Start Naviyra.cmd again.",
|
|
132
|
+
);
|
|
133
|
+
if (!(await exists(path.join(root, ".env"))))
|
|
134
|
+
await command(["scripts/setup-local.mjs"]);
|
|
135
|
+
process.loadEnvFile(path.join(root, ".env"));
|
|
136
|
+
const panelPort = Number(process.env.PANEL_PORT || 3000);
|
|
137
|
+
const agentPort = Number(process.env.AGENT_PORT || 4001);
|
|
138
|
+
if (
|
|
139
|
+
![panelPort, agentPort].every(
|
|
140
|
+
(port) => Number.isInteger(port) && port > 0 && port < 65536,
|
|
141
|
+
) ||
|
|
142
|
+
panelPort === agentPort
|
|
143
|
+
)
|
|
144
|
+
throw Error("PANEL_PORT and AGENT_PORT must be different valid ports.");
|
|
145
|
+
const origin = `http://127.0.0.1:${panelPort}`;
|
|
146
|
+
const panelHealth = `${origin}/api/me`;
|
|
147
|
+
const agentHealth = `http://127.0.0.1:${agentPort}/health`;
|
|
148
|
+
const configuredAgent = new URL(
|
|
149
|
+
process.env.AGENT_URL || `http://127.0.0.1:${agentPort}`,
|
|
150
|
+
);
|
|
151
|
+
if (
|
|
152
|
+
!["127.0.0.1", "localhost"].includes(configuredAgent.hostname) ||
|
|
153
|
+
Number(configuredAgent.port || 80) !== agentPort
|
|
154
|
+
)
|
|
155
|
+
throw Error(
|
|
156
|
+
"The single-click launcher requires AGENT_URL to point to this server's AGENT_PORT.",
|
|
157
|
+
);
|
|
158
|
+
const publicOrigin = process.env.PUBLIC_ORIGIN;
|
|
159
|
+
if (publicOrigin && new URL(publicOrigin).origin !== origin)
|
|
160
|
+
console.log(
|
|
161
|
+
`Configured public panel address: ${new URL(publicOrigin).origin}`,
|
|
162
|
+
);
|
|
163
|
+
const browserUrl = publicOrigin ? new URL(publicOrigin).origin : origin;
|
|
164
|
+
await mkdir(path.dirname(lock), { recursive: true });
|
|
165
|
+
try {
|
|
166
|
+
await writeFile(lock, String(process.pid), { flag: "wx" });
|
|
167
|
+
ownsLock = true;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (error.code !== "EEXIST") throw error;
|
|
170
|
+
const pid = Number(await readFile(lock, "utf8"));
|
|
171
|
+
if (!Number.isInteger(pid) || pid < 1)
|
|
172
|
+
throw Error(
|
|
173
|
+
"The launcher lock is invalid. Remove .runtime/launcher.lock after closing other launchers.",
|
|
174
|
+
);
|
|
175
|
+
let alive = true;
|
|
176
|
+
try {
|
|
177
|
+
process.kill(pid, 0);
|
|
178
|
+
} catch (probe) {
|
|
179
|
+
if (probe.code === "ESRCH") alive = false;
|
|
180
|
+
}
|
|
181
|
+
if (!alive) {
|
|
182
|
+
await unlink(lock);
|
|
183
|
+
await writeFile(lock, String(process.pid), { flag: "wx" });
|
|
184
|
+
ownsLock = true;
|
|
185
|
+
} else {
|
|
186
|
+
console.log(
|
|
187
|
+
"Naviyra is already starting or running. Waiting for the panel…",
|
|
188
|
+
);
|
|
189
|
+
for (let attempt = 0; attempt < 900; attempt++) {
|
|
190
|
+
if (
|
|
191
|
+
(await healthy(panelHealth)) &&
|
|
192
|
+
(await healthy(agentHealth, true))
|
|
193
|
+
) {
|
|
194
|
+
openBrowser(browserUrl);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
process.kill(pid, 0);
|
|
199
|
+
} catch {
|
|
200
|
+
throw Error("The other launcher stopped. Run this launcher again.");
|
|
201
|
+
}
|
|
202
|
+
await delay(1000);
|
|
203
|
+
}
|
|
204
|
+
throw Error(
|
|
205
|
+
"The other launcher is still busy. Check its window for progress.",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log("Checking the panel database…");
|
|
211
|
+
const { default: pg } = await import("pg");
|
|
212
|
+
async function databaseReady() {
|
|
213
|
+
const client = new pg.Client({
|
|
214
|
+
connectionString: process.env.DATABASE_URL,
|
|
215
|
+
connectionTimeoutMillis: 4000,
|
|
216
|
+
});
|
|
217
|
+
try {
|
|
218
|
+
await client.connect();
|
|
219
|
+
await client.query("SELECT 1");
|
|
220
|
+
} finally {
|
|
221
|
+
await client.end().catch(() => {});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
await databaseReady();
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (!["ECONNREFUSED", "3D000"].includes(error.code))
|
|
228
|
+
throw Error(
|
|
229
|
+
"Cannot connect to the panel database. Check DATABASE_URL and database credentials.",
|
|
230
|
+
);
|
|
231
|
+
await command(["--env-file=.env", "scripts/postgres-local.mjs"]);
|
|
232
|
+
await databaseReady();
|
|
233
|
+
}
|
|
234
|
+
await command(["--env-file=.env", "scripts/migrate.mjs"]);
|
|
235
|
+
const client = new pg.Client({
|
|
236
|
+
connectionString: process.env.DATABASE_URL,
|
|
237
|
+
});
|
|
238
|
+
let seed;
|
|
239
|
+
await client.connect();
|
|
240
|
+
try {
|
|
241
|
+
seed = !(
|
|
242
|
+
await client.query("SELECT 1 FROM users WHERE role='admin' LIMIT 1")
|
|
243
|
+
).rowCount;
|
|
244
|
+
} finally {
|
|
245
|
+
await client.end();
|
|
246
|
+
}
|
|
247
|
+
if (seed) await command(["--env-file=.env", "scripts/seed.mjs"]);
|
|
248
|
+
console.log("Starting Naviyra's private Docker runtime…");
|
|
249
|
+
await command(["--env-file=.env", "scripts/runtime.mjs", "start"]);
|
|
250
|
+
|
|
251
|
+
if (!(await healthy(agentHealth, true))) {
|
|
252
|
+
if (await listening(agentPort))
|
|
253
|
+
throw Error(
|
|
254
|
+
`Port ${agentPort} is occupied by another service or an agent with a different token.`,
|
|
255
|
+
);
|
|
256
|
+
console.log("Starting the agent…");
|
|
257
|
+
const child = launch(["src/server.mjs"], path.join(root, "apps/agent"));
|
|
258
|
+
await waitFor(agentHealth, child, true);
|
|
259
|
+
child.once("exit", () => {
|
|
260
|
+
if (!stopping) {
|
|
261
|
+
console.error("The agent stopped. Restart Naviyra.");
|
|
262
|
+
void stop().then(() => {
|
|
263
|
+
process.exitCode = 1;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
} else console.log("Agent is already running.");
|
|
268
|
+
|
|
269
|
+
if (!(await healthy(panelHealth))) {
|
|
270
|
+
if (await listening(panelPort))
|
|
271
|
+
throw Error(`Port ${panelPort} is occupied by another service.`);
|
|
272
|
+
const next = path.join(root, "node_modules/next/dist/bin/next");
|
|
273
|
+
const panelRoot = path.join(root, "apps/panel");
|
|
274
|
+
console.log("Preparing the production panel…");
|
|
275
|
+
await command([next, "build"], panelRoot);
|
|
276
|
+
const child = launch(
|
|
277
|
+
[next, "start", "--hostname", "127.0.0.1", "--port", String(panelPort)],
|
|
278
|
+
panelRoot,
|
|
279
|
+
);
|
|
280
|
+
await waitFor(panelHealth, child);
|
|
281
|
+
child.once("exit", () => {
|
|
282
|
+
if (!stopping) {
|
|
283
|
+
console.error("The panel stopped. Restart Naviyra.");
|
|
284
|
+
void stop().then(() => {
|
|
285
|
+
process.exitCode = 1;
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
} else console.log("Panel is already running.");
|
|
290
|
+
console.log(`\nNaviyra is ready: ${browserUrl}`);
|
|
291
|
+
openBrowser(browserUrl);
|
|
292
|
+
if (children.size)
|
|
293
|
+
console.log(
|
|
294
|
+
"Keep this window open. Press Ctrl+C to stop the panel and agent started here. Database data is preserved.",
|
|
295
|
+
);
|
|
296
|
+
else await stop();
|
|
297
|
+
} catch (error) {
|
|
298
|
+
console.error(`\nNaviyra could not start: ${error.message}`);
|
|
299
|
+
await stop();
|
|
300
|
+
process.exitCode = 1;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
await main();
|