@naviyra/cloud-panel 0.0.0-stage → 0.0.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.
Files changed (55) hide show
  1. package/.env.example +40 -0
  2. package/README.md +134 -2
  3. package/apps/agent/package.json +13 -0
  4. package/apps/agent/src/cloudflare.mjs +733 -0
  5. package/apps/agent/src/native-worker.mjs +52 -0
  6. package/apps/agent/src/native.mjs +457 -0
  7. package/apps/agent/src/runtime.mjs +760 -0
  8. package/apps/agent/src/server.mjs +195 -0
  9. package/apps/agent/src/static-server.mjs +53 -0
  10. package/apps/panel/AGENTS.md +9 -0
  11. package/apps/panel/CLAUDE.md +1 -0
  12. package/apps/panel/app/api/[...path]/route.js +445 -0
  13. package/apps/panel/app/globals.css +2057 -0
  14. package/apps/panel/app/icon.png +0 -0
  15. package/apps/panel/app/layout.jsx +18 -0
  16. package/apps/panel/app/login/page.jsx +108 -0
  17. package/apps/panel/app/page.jsx +33 -0
  18. package/apps/panel/app/ui/cloudflare.jsx +926 -0
  19. package/apps/panel/app/ui/dashboard.jsx +1487 -0
  20. package/apps/panel/app/ui/databases.jsx +293 -0
  21. package/apps/panel/app/ui/skeleton.jsx +196 -0
  22. package/apps/panel/app/ui/theme.jsx +35 -0
  23. package/apps/panel/lib/agent.mjs +83 -0
  24. package/apps/panel/lib/auth.mjs +48 -0
  25. package/apps/panel/lib/database-runtime.mjs +243 -0
  26. package/apps/panel/lib/databases.mjs +292 -0
  27. package/apps/panel/lib/db.mjs +6 -0
  28. package/apps/panel/next.config.mjs +8 -0
  29. package/apps/panel/package.json +22 -0
  30. package/apps/panel/public/naviyra-logo.png +0 -0
  31. package/bin/naviyra.mjs +177 -0
  32. package/db/001_init.sql +28 -0
  33. package/db/002_native_runtimes.sql +3 -0
  34. package/db/003_databases.sql +8 -0
  35. package/db/004_database_engines.sql +4 -0
  36. package/deploy/naviyra-agent.service +17 -0
  37. package/docker-compose.yml +18 -0
  38. package/docs/managed-runtime.md +52 -0
  39. package/docs/native-hosts.md +48 -0
  40. package/package.json +61 -4
  41. package/packages/runtime/artifacts.json +32 -0
  42. package/packages/runtime/index.cjs +526 -0
  43. package/packages/runtime/index.mjs +10 -0
  44. package/packages/runtime/package.json +6 -0
  45. package/packages/shared/index.mjs +85 -0
  46. package/packages/shared/package.json +9 -0
  47. package/scripts/migrate.mjs +18 -0
  48. package/scripts/postgres-local.mjs +97 -0
  49. package/scripts/runtime-download-manifest.mjs +30 -0
  50. package/scripts/runtime.mjs +20 -0
  51. package/scripts/seed.mjs +35 -0
  52. package/scripts/service-supervisor.mjs +164 -0
  53. package/scripts/setup-local.mjs +215 -0
  54. package/scripts/start-naviyra.mjs +303 -0
  55. package/scripts/windows-boot.mjs +259 -0
@@ -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();
@@ -0,0 +1,259 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdir, readFile } from "node:fs/promises";
3
+ import { fileURLToPath } from "node:url";
4
+ import path from "node:path";
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+
7
+ export const TASK_NAME = "Naviyra Cloud Panel";
8
+
9
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
+
11
+ export function psQuote(value) {
12
+ return `'${String(value).replaceAll("'", "''")}'`;
13
+ }
14
+
15
+ export function panelOrigin(envText) {
16
+ const match = String(envText).match(/^PANEL_PORT=(.*)$/m);
17
+ const port = Number((match?.[1] ?? "3000").trim());
18
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
19
+ return "http://127.0.0.1:3000";
20
+ return `http://127.0.0.1:${port}`;
21
+ }
22
+
23
+ export function registrationScript({
24
+ taskName = TASK_NAME,
25
+ nodePath,
26
+ scriptPath,
27
+ workingDirectory,
28
+ userId,
29
+ mode,
30
+ }) {
31
+ const triggers =
32
+ mode === "boot"
33
+ ? `$startup = New-ScheduledTaskTrigger -AtStartup
34
+ $startup.Delay = 'PT30S'
35
+ $logon = New-ScheduledTaskTrigger -AtLogOn -User $user
36
+ $triggers = @($startup, $logon)
37
+ $principal = New-ScheduledTaskPrincipal -UserId $user -LogonType S4U -RunLevel Limited`
38
+ : `$triggers = New-ScheduledTaskTrigger -AtLogOn -User $user
39
+ $principal = New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive -RunLevel Limited`;
40
+ return `$ErrorActionPreference = 'Stop'
41
+ $node = ${psQuote(nodePath)}
42
+ $script = ${psQuote(scriptPath)}
43
+ $workdir = ${psQuote(workingDirectory)}
44
+ $user = ${psQuote(userId)}
45
+ $task = ${psQuote(taskName)}
46
+ $action = New-ScheduledTaskAction -Execute $node -Argument ('"' + $script + '"') -WorkingDirectory $workdir
47
+ $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit (New-TimeSpan -Seconds 0)
48
+ ${triggers}
49
+ Register-ScheduledTask -TaskName $task -Action $action -Trigger $triggers -Settings $settings -Principal $principal -Force | Out-Null
50
+ Write-Output 'REGISTERED ${mode}'
51
+ `;
52
+ }
53
+
54
+ function taskScript(command, taskName = TASK_NAME) {
55
+ const task = psQuote(taskName);
56
+ if (command === "start")
57
+ return `$ErrorActionPreference = 'Stop'
58
+ try { Start-ScheduledTask -TaskName ${task} }
59
+ catch {
60
+ if ($_.Exception.Message -match 'already running|currently running') { exit 0 }
61
+ throw
62
+ }
63
+ Write-Output 'STARTED'
64
+ `;
65
+ if (command === "stop")
66
+ return `Stop-ScheduledTask -TaskName ${task} -ErrorAction SilentlyContinue
67
+ Write-Output 'STOPPED'
68
+ `;
69
+ return `Unregister-ScheduledTask -TaskName ${task} -Confirm:$false -ErrorAction SilentlyContinue
70
+ Write-Output 'REMOVED'
71
+ `;
72
+ }
73
+
74
+ export function runPowerShell(script) {
75
+ return new Promise((resolve) => {
76
+ const child = spawn(
77
+ "powershell.exe",
78
+ ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script],
79
+ { windowsHide: true, stdio: ["ignore", "pipe", "pipe"] },
80
+ );
81
+ let output = "";
82
+ child.stdout?.on("data", (chunk) => {
83
+ output += chunk;
84
+ });
85
+ child.stderr?.on("data", (chunk) => {
86
+ output += chunk;
87
+ });
88
+ child.once("error", (error) => resolve({ code: 1, output: error.message }));
89
+ child.once("exit", (code) => resolve({ code: code ?? 1, output }));
90
+ });
91
+ }
92
+
93
+ function windowsUser() {
94
+ return new Promise((resolve) => {
95
+ const child = spawn("whoami.exe", [], {
96
+ windowsHide: true,
97
+ stdio: ["ignore", "pipe", "pipe"],
98
+ });
99
+ let output = "";
100
+ child.stdout?.on("data", (chunk) => {
101
+ output += chunk;
102
+ });
103
+ child.once("error", () => resolve(""));
104
+ child.once("exit", (code) => resolve(code === 0 ? output.trim() : ""));
105
+ });
106
+ }
107
+
108
+ export async function installBootTask(options = {}) {
109
+ const installRoot = options.root ?? root;
110
+ const nodePath = options.nodePath ?? process.execPath;
111
+ const userId = options.userId ?? (await windowsUser());
112
+ const run = options.runPowerShell ?? runPowerShell;
113
+ if (!userId)
114
+ return { ok: false, message: "Could not determine the Windows account for the startup task." };
115
+ const scriptPath = path.join(installRoot, "scripts", "service-supervisor.mjs");
116
+ let last = "Task Scheduler did not accept the startup task.";
117
+ for (const mode of ["boot", "logon"]) {
118
+ const result = await run(
119
+ registrationScript({
120
+ nodePath,
121
+ scriptPath,
122
+ workingDirectory: installRoot,
123
+ userId,
124
+ mode,
125
+ }),
126
+ );
127
+ if (result.code === 0 && result.output.includes(`REGISTERED ${mode}`)) {
128
+ const message =
129
+ mode === "boot"
130
+ ? `Registered "${TASK_NAME}" to start at Windows boot as ${userId} and restart if it stops.`
131
+ : `Registered "${TASK_NAME}" to start when ${userId} signs in and restart if it stops.`;
132
+ return { ok: true, mode, message };
133
+ }
134
+ last = result.output.trim() || last;
135
+ }
136
+ return { ok: false, message: last };
137
+ }
138
+
139
+ export async function startBootTask(run = runPowerShell) {
140
+ const result = await run(taskScript("start"));
141
+ if (result.code === 0) return { ok: true };
142
+ return { ok: false, message: result.output.trim() };
143
+ }
144
+
145
+ async function servicePid(installRoot) {
146
+ try {
147
+ const pid = Number(
148
+ await readFile(path.join(installRoot, ".runtime", "service.pid"), "utf8"),
149
+ );
150
+ return Number.isInteger(pid) && pid > 0 ? pid : 0;
151
+ } catch {
152
+ return 0;
153
+ }
154
+ }
155
+
156
+ function taskkill(pid) {
157
+ return new Promise((resolve) => {
158
+ const child = spawn("taskkill.exe", ["/PID", String(pid), "/T", "/F"], {
159
+ windowsHide: true,
160
+ stdio: "ignore",
161
+ });
162
+ child.once("error", resolve);
163
+ child.once("exit", resolve);
164
+ });
165
+ }
166
+
167
+ export async function stopBootTask(options = {}) {
168
+ const run = options.runPowerShell ?? runPowerShell;
169
+ const installRoot = options.root ?? root;
170
+ const pid = await servicePid(installRoot);
171
+ if (pid && process.platform === "win32") await taskkill(pid);
172
+ const result = await run(taskScript("stop"));
173
+ return { ok: result.code === 0, message: result.output.trim() };
174
+ }
175
+
176
+ export async function removeBootTask(options = {}) {
177
+ const run = options.runPowerShell ?? runPowerShell;
178
+ await stopBootTask({ ...options, runPowerShell: run });
179
+ const result = await run(taskScript("remove"));
180
+ return {
181
+ ok: result.code === 0,
182
+ message:
183
+ result.code === 0
184
+ ? `Removed the "${TASK_NAME}" startup task.`
185
+ : result.output.trim(),
186
+ };
187
+ }
188
+
189
+ export async function openPanelWhenReady(installRoot = root) {
190
+ let envText = "";
191
+ try {
192
+ envText = await readFile(path.join(installRoot, ".env"), "utf8");
193
+ } catch {
194
+ envText = "";
195
+ }
196
+ const origin = panelOrigin(envText);
197
+ for (let attempt = 0; attempt < 300; attempt++) {
198
+ try {
199
+ const response = await fetch(`${origin}/api/me`, {
200
+ redirect: "manual",
201
+ signal: AbortSignal.timeout(2000),
202
+ });
203
+ const body = await response.json();
204
+ if (response.status === 401 && body.error === "Please sign in") {
205
+ const browser = spawn("explorer.exe", [origin], {
206
+ windowsHide: true,
207
+ stdio: "ignore",
208
+ });
209
+ browser.once("error", () => {});
210
+ browser.unref();
211
+ console.log(`Naviyra is ready: ${origin}`);
212
+ return;
213
+ }
214
+ } catch {
215
+ // The panel build can take several minutes on first start.
216
+ }
217
+ await delay(1000);
218
+ }
219
+ console.log(
220
+ `Naviyra is still starting. Open ${origin} when it is ready. Progress is in .runtime/service.log.`,
221
+ );
222
+ }
223
+
224
+ async function main() {
225
+ const command = process.argv[2] || "install";
226
+ if (!["install", "start", "stop", "remove"].includes(command))
227
+ throw Error("Usage: node scripts/windows-boot.mjs install|start|stop|remove");
228
+ if (process.platform !== "win32")
229
+ throw Error("The Naviyra startup task is only registered on Windows.");
230
+ await mkdir(path.join(root, ".runtime"), { recursive: true });
231
+ if (command === "install") {
232
+ const installed = await installBootTask({ root });
233
+ if (!installed.ok) throw Error(installed.message);
234
+ console.log(installed.message);
235
+ const started = await startBootTask();
236
+ if (!started.ok) throw Error(started.message || "Could not start the task.");
237
+ console.log("Naviyra is starting in the background.");
238
+ return;
239
+ }
240
+ if (command === "start") {
241
+ const started = await startBootTask();
242
+ if (!started.ok) throw Error(started.message || "Could not start the task.");
243
+ console.log("Naviyra startup task is running.");
244
+ return;
245
+ }
246
+ const result =
247
+ command === "stop"
248
+ ? await stopBootTask({ root })
249
+ : await removeBootTask({ root });
250
+ if (!result.ok) throw Error(result.message || `Could not ${command} the task.`);
251
+ console.log(result.message || `Naviyra startup task ${command} complete.`);
252
+ }
253
+
254
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
255
+ main().catch((error) => {
256
+ console.error(error.message);
257
+ process.exitCode = 1;
258
+ });
259
+ }