@naviyra/cloud-panel 0.0.3-beta2 → 0.0.3-beta3
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/apps/agent/src/cloudflare.mjs +1 -0
- package/apps/agent/src/files.mjs +162 -0
- package/apps/agent/src/host-tools.mjs +518 -0
- package/apps/agent/src/native.mjs +157 -32
- package/apps/agent/src/runtime.mjs +70 -37
- package/apps/agent/src/server.mjs +61 -6
- package/apps/panel/app/api/[...path]/route.js +94 -13
- package/apps/panel/app/globals.css +262 -3
- package/apps/panel/app/layout.jsx +2 -1
- package/apps/panel/app/login/page.jsx +3 -8
- package/apps/panel/app/page.jsx +17 -7
- package/apps/panel/app/ui/cloudflare.jsx +72 -47
- package/apps/panel/app/ui/confirm.jsx +109 -0
- package/apps/panel/app/ui/dashboard.jsx +87 -72
- package/apps/panel/app/ui/databases.jsx +168 -59
- package/apps/panel/app/ui/project-tools.jsx +604 -0
- package/apps/panel/app/ui/toast.jsx +90 -0
- package/apps/panel/lib/agent.mjs +13 -2
- package/apps/panel/lib/databases.mjs +120 -0
- package/apps/panel/lib/request-origin.mjs +29 -29
- package/package.json +1 -1
- package/packages/shared/index.mjs +49 -0
- package/scripts/panel-control.mjs +214 -214
- package/scripts/service-supervisor.mjs +164 -164
- package/scripts/windows-boot.mjs +259 -259
package/scripts/windows-boot.mjs
CHANGED
|
@@ -1,259 +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
|
-
}
|
|
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
|
+
}
|