@naviyra/cloud-panel 0.0.1 → 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/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`. Run the installer from an elevated PowerShell when the panel must start before anyone signs in. Stop it with `node scripts/windows-boot.mjs stop`, or remove it with `node scripts/windows-boot.mjs remove`.
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 (!["--help", "-h", "--install-only", "--no-browser"].includes(arg))
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 [--install-only] [--no-browser]
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. The task uses the account that runs the
134
- installer, starts again at the next sign-in, and starts at boot when the
135
- installer itself is elevated.
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
  }
@@ -161,9 +192,16 @@ See docs/managed-runtime.md in the installed folder.`);
161
192
  await run(process.execPath, ["scripts/setup-local.mjs"], target);
162
193
  const background = await enableBackground(target);
163
194
  if (background) {
164
- console.log(
165
- "Naviyra will keep running in the background. Stop it with: node scripts/windows-boot.mjs stop",
166
- );
195
+ const envText = await readFile(path.join(target, ".env"), "utf8").catch(() => "");
196
+ const port = Number((envText.match(/^PANEL_PORT=(.*)$/m)?.[1] ?? "3000").trim());
197
+ const origin =
198
+ Number.isInteger(port) && port > 0 && port < 65536
199
+ ? `http://127.0.0.1:${port}`
200
+ : "http://127.0.0.1:3000";
201
+ console.log(`Panel address: ${origin}`);
202
+ console.log("Status: npx @naviyra/cloud-panel status");
203
+ console.log("Open: npx @naviyra/cloud-panel open");
204
+ console.log("Stop: npx @naviyra/cloud-panel stop");
167
205
  if (!args.includes("--install-only") && !args.includes("--no-browser"))
168
206
  await background.openPanelWhenReady(target);
169
207
  } 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.1",
3
+ "version": "0.0.2",
4
4
  "description": "Self-hosted Naviyra panel for projects, databases, DNS, and email routing.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,195 @@
1
+ import { spawn } from "node:child_process";
2
+ import { access, readFile, rm } 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 execute(command, options = {}) {
141
+ const located = await locate(options);
142
+ if (command === "status") {
143
+ console.log(formatStatus(await readStatus(located.dir, located.state)));
144
+ return;
145
+ }
146
+ if (!located.dir)
147
+ throw Error("Naviyra is not installed. From an empty folder you own, run: npx @naviyra/cloud-panel");
148
+ const root = located.dir;
149
+ const run = options.runPowerShell;
150
+ if (command === "stop") {
151
+ const stopped = await stopBootTask({ root, runPowerShell: run });
152
+ if (!stopped.ok) throw Error(stopped.message || "Could not stop Naviyra.");
153
+ console.log("Naviyra is stopped.");
154
+ return;
155
+ }
156
+ if (command === "start") {
157
+ const started = await startBootTask(run);
158
+ if (!started.ok) throw Error(started.message || "Could not start Naviyra.");
159
+ console.log("Naviyra is starting. Check it with: npx @naviyra/cloud-panel status");
160
+ return;
161
+ }
162
+ if (command === "open") {
163
+ const status = await readStatus(root, located.state);
164
+ if (!status.ready) {
165
+ console.log(formatStatus(status));
166
+ throw Error("The panel is not ready to open yet.");
167
+ }
168
+ openBrowser(status.origin);
169
+ console.log(`Opened ${status.origin}`);
170
+ return;
171
+ }
172
+ if (command === "uninstall") {
173
+ const removed = await removeBootTask({ root, runPowerShell: run });
174
+ if (!removed.ok) throw Error(removed.message || "Could not remove the startup task.");
175
+ if (canRemoveInstall(root) && (await isPanel(root))) {
176
+ await rm(root, { recursive: true, force: true });
177
+ console.log(`Uninstalled Naviyra and removed ${root}`);
178
+ return;
179
+ }
180
+ console.log(`${removed.message} The install folder was left at ${root}`);
181
+ return;
182
+ }
183
+ if (command === "update") {
184
+ if (!options.source) throw Error("Cannot find the new Naviyra package to copy.");
185
+ if (path.resolve(options.source) === path.resolve(root))
186
+ throw Error("Run npx @naviyra/cloud-panel update so the newest package can replace this install.");
187
+ await options.copy(options.source, root);
188
+ await options.install(root);
189
+ const started = await startBootTask(run);
190
+ if (!started.ok) throw Error(started.message || "Update copied, but Naviyra did not start.");
191
+ console.log(`Updated Naviyra in ${root}. Check it with: npx @naviyra/cloud-panel status`);
192
+ return;
193
+ }
194
+ throw Error(`Unknown command: ${command}`);
195
+ }