@naviyra/cloud-panel 0.0.1 → 0.0.3-beta
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/README.md +12 -1
- package/bin/naviyra.mjs +51 -8
- package/package.json +3 -2
- package/scripts/panel-control.mjs +214 -0
package/README.md
CHANGED
|
@@ -8,7 +8,18 @@ npx @naviyra/cloud-panel
|
|
|
8
8
|
|
|
9
9
|
Run that command in an empty folder. It copies the panel there, installs dependencies, asks in the terminal for the administrator email, password, and agent port, then opens the browser. JWT and agent secrets are generated. Node.js 22.9 or newer is required. Startup also requires PostgreSQL (on Windows, PostgreSQL 18 at its default location, or set `POSTGRES_BIN` in `.env`) and the [private runtime prerequisites](docs/managed-runtime.md). Sign-in details are written to `.runtime/local-login.txt`.
|
|
10
10
|
|
|
11
|
-
On Windows, that install also registers a scheduled task named **Naviyra Cloud Panel**. The task runs as the same account, starts the panel immediately, starts it again at the next sign-in (including after a reboot), and restarts it if it stops. A supervisor restarts the panel a few seconds after a crash; Task Scheduler restarts the supervisor if that process dies. The panel keeps running after the installer exits. Progress is written to `.runtime/service.log`.
|
|
11
|
+
On Windows, that install also registers a scheduled task named **Naviyra Cloud Panel**. The task runs as the same account, starts the panel immediately, starts it again at the next sign-in (including after a reboot), and restarts it if it stops. A supervisor restarts the panel a few seconds after a crash; Task Scheduler restarts the supervisor if that process dies. The panel keeps running after the installer exits. Progress is written to `.runtime/service.log`.
|
|
12
|
+
|
|
13
|
+
```powershell
|
|
14
|
+
npx @naviyra/cloud-panel status
|
|
15
|
+
npx @naviyra/cloud-panel open
|
|
16
|
+
npx @naviyra/cloud-panel stop
|
|
17
|
+
npx @naviyra/cloud-panel start
|
|
18
|
+
npx @naviyra/cloud-panel update
|
|
19
|
+
npx @naviyra/cloud-panel uninstall
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`status` prints the panel address and whether it is accepting sign-in. `open` launches that address in the browser. Run the installer from an empty folder you own, such as `D:\naviyra-install`. It will not install into `C:\Windows`. Run the installer from an elevated PowerShell when the panel must start before anyone signs in.
|
|
12
23
|
|
|
13
24
|
To install and configure prerequisites before leaving the panel running, use `npx @naviyra/cloud-panel --install-only`. Use `--no-browser` to start without opening a browser. Re-running the installer checks all workspace dependencies and preserves existing `.env` credentials. This package is separate from `@naviyra/hosting-panel`.
|
|
14
25
|
|
package/bin/naviyra.mjs
CHANGED
|
@@ -122,17 +122,40 @@ const cwd = process.cwd();
|
|
|
122
122
|
try {
|
|
123
123
|
const args = process.argv.slice(2);
|
|
124
124
|
for (const arg of args)
|
|
125
|
-
if (
|
|
125
|
+
if (
|
|
126
|
+
!["--help", "-h", "--install-only", "--no-browser"].includes(arg) &&
|
|
127
|
+
!["status", "open", "stop", "start", "update", "uninstall"].includes(arg)
|
|
128
|
+
)
|
|
126
129
|
throw Error(`Unknown option: ${arg}. Run with --help for usage.`);
|
|
130
|
+
const command = ["status", "open", "stop", "start", "update", "uninstall"].find(
|
|
131
|
+
(name) => args[0] === name,
|
|
132
|
+
);
|
|
133
|
+
if (command) {
|
|
134
|
+
const control = await import(
|
|
135
|
+
pathToFileURL(path.join(source, "scripts", "panel-control.mjs")).href
|
|
136
|
+
);
|
|
137
|
+
await control.execute(command, {
|
|
138
|
+
cwd,
|
|
139
|
+
source,
|
|
140
|
+
copy: copyTree,
|
|
141
|
+
install: (dir) => run("npm", ["install", "--omit=dev"], dir),
|
|
142
|
+
});
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
127
145
|
if (args.includes("--help") || args.includes("-h")) {
|
|
128
|
-
console.log(`Usage: npx @naviyra/cloud-panel [
|
|
146
|
+
console.log(`Usage: npx @naviyra/cloud-panel [status|open|stop|start|update|uninstall]
|
|
147
|
+
npx @naviyra/cloud-panel [--install-only] [--no-browser]
|
|
129
148
|
|
|
130
149
|
Installs into the current empty folder, or a naviyra subfolder.
|
|
131
150
|
In a terminal, asks for the administrator email, password, and agent port.
|
|
132
151
|
On Windows, also registers a scheduled task that keeps the panel running
|
|
133
|
-
and restarts it if it stops.
|
|
134
|
-
|
|
135
|
-
|
|
152
|
+
and restarts it if it stops.
|
|
153
|
+
status Show whether the panel is running and its address.
|
|
154
|
+
open Open the panel in the browser.
|
|
155
|
+
stop Stop the panel.
|
|
156
|
+
start Start the panel again.
|
|
157
|
+
update Replace the installed copy with this package and start it.
|
|
158
|
+
uninstall Stop the panel, remove the startup task, and delete the install folder.
|
|
136
159
|
--install-only Install, register the startup task, and leave it running.
|
|
137
160
|
--no-browser Do not open a browser after the panel is ready.
|
|
138
161
|
|
|
@@ -150,6 +173,14 @@ See docs/managed-runtime.md in the installed folder.`);
|
|
|
150
173
|
);
|
|
151
174
|
}
|
|
152
175
|
if (!(await isPanel(target))) {
|
|
176
|
+
const controlPath = path.join(source, "scripts", "panel-control.mjs");
|
|
177
|
+
if (await exists(controlPath)) {
|
|
178
|
+
const { isProtectedDir } = await import(pathToFileURL(controlPath).href);
|
|
179
|
+
if (isProtectedDir(target))
|
|
180
|
+
throw Error(
|
|
181
|
+
`Naviyra cannot install into ${target}. Open PowerShell in an empty folder you own, such as D:\\naviyra-install, and run npx @naviyra/cloud-panel there.`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
153
184
|
console.log(`Installing Naviyra into ${target}`);
|
|
154
185
|
await copyTree(source, target);
|
|
155
186
|
}
|
|
@@ -158,12 +189,24 @@ See docs/managed-runtime.md in the installed folder.`);
|
|
|
158
189
|
// created pg already while leaving Next.js or the agent dependencies missing.
|
|
159
190
|
console.log("Checking and installing dependencies…");
|
|
160
191
|
await run("npm", ["install", "--omit=dev"], target);
|
|
192
|
+
const controlPath = path.join(source, "scripts", "panel-control.mjs");
|
|
193
|
+
if (await exists(controlPath)) {
|
|
194
|
+
const { linkBin } = await import(pathToFileURL(controlPath).href);
|
|
195
|
+
await linkBin(target);
|
|
196
|
+
}
|
|
161
197
|
await run(process.execPath, ["scripts/setup-local.mjs"], target);
|
|
162
198
|
const background = await enableBackground(target);
|
|
163
199
|
if (background) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
200
|
+
const envText = await readFile(path.join(target, ".env"), "utf8").catch(() => "");
|
|
201
|
+
const port = Number((envText.match(/^PANEL_PORT=(.*)$/m)?.[1] ?? "3000").trim());
|
|
202
|
+
const origin =
|
|
203
|
+
Number.isInteger(port) && port > 0 && port < 65536
|
|
204
|
+
? `http://127.0.0.1:${port}`
|
|
205
|
+
: "http://127.0.0.1:3000";
|
|
206
|
+
console.log(`Panel address: ${origin}`);
|
|
207
|
+
console.log("Status: npx @naviyra/cloud-panel status");
|
|
208
|
+
console.log("Open: npx @naviyra/cloud-panel open");
|
|
209
|
+
console.log("Stop: npx @naviyra/cloud-panel stop");
|
|
167
210
|
if (!args.includes("--install-only") && !args.includes("--no-browser"))
|
|
168
211
|
await background.openPanelWhenReady(target);
|
|
169
212
|
} else if (args.includes("--install-only")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naviyra/cloud-panel",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3-beta",
|
|
4
4
|
"description": "Self-hosted Naviyra panel for projects, databases, DNS, and email routing.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"README.md"
|
|
22
22
|
],
|
|
23
23
|
"publishConfig": {
|
|
24
|
-
"access": "public"
|
|
24
|
+
"access": "public",
|
|
25
|
+
"tag": "beta"
|
|
25
26
|
},
|
|
26
27
|
"workspaces": [
|
|
27
28
|
"apps/*",
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
TASK_NAME,
|
|
6
|
+
panelOrigin,
|
|
7
|
+
removeBootTask,
|
|
8
|
+
runPowerShell,
|
|
9
|
+
startBootTask,
|
|
10
|
+
stopBootTask,
|
|
11
|
+
} from "./windows-boot.mjs";
|
|
12
|
+
|
|
13
|
+
export const COMMANDS = ["status", "open", "stop", "start", "update", "uninstall"];
|
|
14
|
+
|
|
15
|
+
export function isProtectedDir(dir) {
|
|
16
|
+
const resolved = path.resolve(dir);
|
|
17
|
+
const roots = [
|
|
18
|
+
process.env.WINDIR,
|
|
19
|
+
process.env.ProgramFiles,
|
|
20
|
+
process.env["ProgramFiles(x86)"],
|
|
21
|
+
].filter(Boolean);
|
|
22
|
+
return roots.some((root) => {
|
|
23
|
+
const base = path.resolve(root);
|
|
24
|
+
const prefix = base.toLowerCase() + path.sep;
|
|
25
|
+
return resolved.toLowerCase().startsWith(prefix) || resolved.toLowerCase() === base.toLowerCase();
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function canRemoveInstall(dir) {
|
|
30
|
+
const resolved = path.resolve(dir);
|
|
31
|
+
const base = path.basename(resolved).toLowerCase();
|
|
32
|
+
if (base === "system32" || base === "windows" || base === "syswow64") return false;
|
|
33
|
+
const windir = path.resolve(process.env.WINDIR || "C:\\Windows");
|
|
34
|
+
return resolved.toLowerCase() !== windir.toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function problemFromLog(text) {
|
|
38
|
+
const lines = String(text)
|
|
39
|
+
.split(/\r?\n/)
|
|
40
|
+
.map((line) => line.trim())
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
if (lines.some((line) => /permission denied/i.test(line)))
|
|
43
|
+
return "PostgreSQL could not create its data directory because this folder is protected. Install into an empty folder you own, such as D:\\naviyra-install.";
|
|
44
|
+
return lines.findLast((line) => /could not start|exited 1|error:/i.test(line)) || "";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatStatus({ dir, taskState, origin, ready, problem }) {
|
|
48
|
+
if (!dir && (!taskState || taskState === "missing"))
|
|
49
|
+
return "Naviyra is not installed. From an empty folder you own, run: npx @naviyra/cloud-panel";
|
|
50
|
+
const lines = [
|
|
51
|
+
ready ? "Naviyra is running." : "Naviyra is not ready.",
|
|
52
|
+
`Panel: ${origin || "http://127.0.0.1:3000"}`,
|
|
53
|
+
];
|
|
54
|
+
if (dir) lines.push(`Install folder: ${dir}`);
|
|
55
|
+
lines.push(`Startup task: ${taskState || "not registered"}`);
|
|
56
|
+
if (!ready && problem) lines.push(problem);
|
|
57
|
+
lines.push(
|
|
58
|
+
ready
|
|
59
|
+
? "Open the panel with: npx @naviyra/cloud-panel open"
|
|
60
|
+
: "After it is ready, open it with: npx @naviyra/cloud-panel open",
|
|
61
|
+
);
|
|
62
|
+
return lines.join("\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function isPanel(dir) {
|
|
66
|
+
try {
|
|
67
|
+
const pkg = JSON.parse(await readFile(path.join(dir, "package.json"), "utf8"));
|
|
68
|
+
await access(path.join(dir, "apps", "panel", "package.json"));
|
|
69
|
+
return pkg.name === "@naviyra/cloud-panel";
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function queryTask(run = runPowerShell) {
|
|
76
|
+
const result = await run(`$task = Get-ScheduledTask -TaskName ${"'" + TASK_NAME.replaceAll("'", "''") + "'"} -ErrorAction SilentlyContinue
|
|
77
|
+
if (-not $task) { Write-Output 'STATE=missing'; exit 0 }
|
|
78
|
+
Write-Output ('STATE=' + $task.State)
|
|
79
|
+
Write-Output ('DIR=' + $task.Actions[0].WorkingDirectory)
|
|
80
|
+
`);
|
|
81
|
+
const state = result.output.match(/STATE=(.*)/)?.[1]?.trim() || "missing";
|
|
82
|
+
const dir = result.output.match(/DIR=(.*)/)?.[1]?.trim() || "";
|
|
83
|
+
return { state, dir };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function panelHealthy(origin) {
|
|
87
|
+
try {
|
|
88
|
+
const response = await fetch(`${origin}/api/me`, {
|
|
89
|
+
redirect: "manual",
|
|
90
|
+
signal: AbortSignal.timeout(2000),
|
|
91
|
+
});
|
|
92
|
+
const body = await response.json();
|
|
93
|
+
return response.status === 401 && body.error === "Please sign in";
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function openBrowser(origin) {
|
|
100
|
+
const file = process.platform === "win32" ? "explorer.exe" : "xdg-open";
|
|
101
|
+
const browser = spawn(file, [origin], { windowsHide: true, stdio: "ignore" });
|
|
102
|
+
browser.once("error", () => console.log(`Open ${origin} in your browser.`));
|
|
103
|
+
browser.unref();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function locate(options) {
|
|
107
|
+
const task = options.queryTask ? await options.queryTask() : await queryTask(options.runPowerShell);
|
|
108
|
+
if (task.dir && (await isPanel(task.dir))) return { ...task, dir: task.dir };
|
|
109
|
+
if (options.cwd && (await isPanel(options.cwd)))
|
|
110
|
+
return { state: task.state, dir: options.cwd };
|
|
111
|
+
return { state: task.state || "missing", dir: "" };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function readStatus(dir, taskState) {
|
|
115
|
+
let envText = "";
|
|
116
|
+
let logText = "";
|
|
117
|
+
if (dir) {
|
|
118
|
+
try {
|
|
119
|
+
envText = await readFile(path.join(dir, ".env"), "utf8");
|
|
120
|
+
} catch {
|
|
121
|
+
envText = "";
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
logText = await readFile(path.join(dir, ".runtime", "service.log"), "utf8");
|
|
125
|
+
} catch {
|
|
126
|
+
logText = "";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const origin = panelOrigin(envText);
|
|
130
|
+
const ready = await panelHealthy(origin);
|
|
131
|
+
return {
|
|
132
|
+
dir,
|
|
133
|
+
taskState,
|
|
134
|
+
origin,
|
|
135
|
+
ready,
|
|
136
|
+
problem: ready ? "" : problemFromLog(logText),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function linkBin(root) {
|
|
141
|
+
const binDir = path.join(root, "node_modules", ".bin");
|
|
142
|
+
await mkdir(binDir, { recursive: true });
|
|
143
|
+
await writeFile(
|
|
144
|
+
path.join(binDir, "cloud-panel.cmd"),
|
|
145
|
+
'@ECHO off\r\nnode "%~dp0\\..\\..\\bin\\naviyra.mjs" %*\r\n',
|
|
146
|
+
);
|
|
147
|
+
await writeFile(
|
|
148
|
+
path.join(binDir, "cloud-panel.ps1"),
|
|
149
|
+
[
|
|
150
|
+
"$node = (Get-Command node -ErrorAction Stop).Source",
|
|
151
|
+
'& $node (Join-Path $PSScriptRoot "..\\..\\bin\\naviyra.mjs") @args',
|
|
152
|
+
"exit $LASTEXITCODE",
|
|
153
|
+
"",
|
|
154
|
+
].join("\n"),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function execute(command, options = {}) {
|
|
159
|
+
const located = await locate(options);
|
|
160
|
+
if (command === "status") {
|
|
161
|
+
console.log(formatStatus(await readStatus(located.dir, located.state)));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (!located.dir)
|
|
165
|
+
throw Error("Naviyra is not installed. From an empty folder you own, run: npx @naviyra/cloud-panel");
|
|
166
|
+
const root = located.dir;
|
|
167
|
+
const run = options.runPowerShell;
|
|
168
|
+
if (command === "stop") {
|
|
169
|
+
const stopped = await stopBootTask({ root, runPowerShell: run });
|
|
170
|
+
if (!stopped.ok) throw Error(stopped.message || "Could not stop Naviyra.");
|
|
171
|
+
console.log("Naviyra is stopped.");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (command === "start") {
|
|
175
|
+
const started = await startBootTask(run);
|
|
176
|
+
if (!started.ok) throw Error(started.message || "Could not start Naviyra.");
|
|
177
|
+
console.log("Naviyra is starting. Check it with: npx @naviyra/cloud-panel status");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (command === "open") {
|
|
181
|
+
const status = await readStatus(root, located.state);
|
|
182
|
+
if (!status.ready) {
|
|
183
|
+
console.log(formatStatus(status));
|
|
184
|
+
throw Error("The panel is not ready to open yet.");
|
|
185
|
+
}
|
|
186
|
+
openBrowser(status.origin);
|
|
187
|
+
console.log(`Opened ${status.origin}`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (command === "uninstall") {
|
|
191
|
+
const removed = await removeBootTask({ root, runPowerShell: run });
|
|
192
|
+
if (!removed.ok) throw Error(removed.message || "Could not remove the startup task.");
|
|
193
|
+
if (canRemoveInstall(root) && (await isPanel(root))) {
|
|
194
|
+
await rm(root, { recursive: true, force: true });
|
|
195
|
+
console.log(`Uninstalled Naviyra and removed ${root}`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
console.log(`${removed.message} The install folder was left at ${root}`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (command === "update") {
|
|
202
|
+
if (!options.source) throw Error("Cannot find the new Naviyra package to copy.");
|
|
203
|
+
if (path.resolve(options.source) === path.resolve(root))
|
|
204
|
+
throw Error("Run npx @naviyra/cloud-panel update so the newest package can replace this install.");
|
|
205
|
+
await options.copy(options.source, root);
|
|
206
|
+
await options.install(root);
|
|
207
|
+
await linkBin(root);
|
|
208
|
+
const started = await startBootTask(run);
|
|
209
|
+
if (!started.ok) throw Error(started.message || "Update copied, but Naviyra did not start.");
|
|
210
|
+
console.log(`Updated Naviyra in ${root}. Check it with: npx @naviyra/cloud-panel status`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
throw Error(`Unknown command: ${command}`);
|
|
214
|
+
}
|