@join-pantheon/cli 0.32.1 → 0.33.0-rc.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/dist/accounts.js +1 -1
- package/dist/board.js +1417 -0
- package/dist/claude-config.js +189 -0
- package/dist/cli.js +351 -101
- package/dist/client.js +16 -0
- package/dist/command-state.js +2 -1
- package/dist/dbus.js +92 -13
- package/dist/doctor.js +36 -5
- package/dist/folder-guard.js +89 -0
- package/dist/git-safe.js +3 -3
- package/dist/host-env.js +47 -1
- package/dist/install.js +76 -10
- package/dist/installer.js +2 -1
- package/dist/ledger.js +2 -2
- package/dist/machine.js +174 -3
- package/dist/managed.js +27 -1
- package/dist/notice-linux.js +501 -0
- package/dist/notice-windows.js +323 -0
- package/dist/notifier.js +12 -3
- package/dist/quickstart.js +28 -11
- package/dist/relay.js +64 -9
- package/dist/service.js +455 -29
- package/dist/setup-guide.js +22 -5
- package/dist/store.js +18 -2
- package/dist/uninstall.js +1 -1
- package/dist/upgrade.js +249 -4
- package/dist/uri-handler.js +335 -0
- package/dist/win-host.js +993 -0
- package/dist/worker-policy.js +3 -0
- package/dist/worker.js +11 -11
- package/helper/macos/prebuilt/manifest.json +2 -2
- package/helper/macos/prebuilt/pantheon-hud +0 -0
- package/helper/windows/pantheon-host.cs +170 -0
- package/helper/windows/pantheon-notice.ps1 +127 -0
- package/helper/windows/pantheon-tray.ps1 +731 -0
- package/helper/windows/pantheon.ico +0 -0
- package/installer/install.ps1 +238 -19
- package/installer/install.sh +36 -1
- package/package.json +2 -2
package/dist/service.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { writeFileSync, mkdirSync, existsSync, unlinkSync, readFileSync } from "node:fs";
|
|
1
|
+
import { writeFileSync, mkdirSync, existsSync, lstatSync, unlinkSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { homedir, platform, userInfo } from "node:os";
|
|
4
4
|
import { which } from "./which.js";
|
|
5
|
-
import { dirname, isAbsolute, join, posix, resolve } from "node:path";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, posix, resolve } from "node:path";
|
|
6
|
+
import { linuxGraphicalSession } from "./host-env.js";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { launcher } from "./install.js";
|
|
8
|
+
import { isVersionManagedPath, launcher } from "./install.js";
|
|
8
9
|
import { childNodeFlags, managedLauncher, managedLayout } from "./managed.js";
|
|
9
10
|
import { CONFIG_DIR } from "./config.js";
|
|
11
|
+
import { HOST_KINDS, LEGACY_TASK_NAME, clearTrayQuit, ensureHostShim, fromUtf16le, hostAlive, hostArgv, hostChildRunning, hostFiles, hostLaunch, isHostKind, launchFileText, legacyTaskIsUsers, readDefinition, readHostState, taskName, taskUserId, taskXml, utf16le, windowsPowerShell, writeDefinition, } from "./win-host.js";
|
|
10
12
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
11
13
|
const LABEL = (kind) => `dev.pantheon.${kind}`;
|
|
12
14
|
export function borrowedHome() {
|
|
@@ -19,14 +21,55 @@ export function borrowedHome() {
|
|
|
19
21
|
return false;
|
|
20
22
|
}
|
|
21
23
|
}
|
|
24
|
+
export function borrowedScheduler(env = process.env) {
|
|
25
|
+
if (env.PANTHEON_SERVICES_ANY_HOME === "1")
|
|
26
|
+
return false;
|
|
27
|
+
if (borrowedHome())
|
|
28
|
+
return true;
|
|
29
|
+
if (!env.PANTHEON_HOME)
|
|
30
|
+
return false;
|
|
31
|
+
try {
|
|
32
|
+
return resolve(env.PANTHEON_HOME).toLowerCase() !== resolve(join(userInfo().homedir, ".pantheon")).toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function borrowedProfile(env = process.env, profile = () => userInfo().homedir) {
|
|
39
|
+
if (env.PANTHEON_SERVICES_ANY_HOME === "1")
|
|
40
|
+
return false;
|
|
41
|
+
if (borrowedScheduler(env))
|
|
42
|
+
return true;
|
|
43
|
+
let real;
|
|
44
|
+
try {
|
|
45
|
+
real = resolve(profile()).toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return [env.HOME, env.USERPROFILE].some((h) => !!h && resolve(h).toLowerCase() !== real);
|
|
51
|
+
}
|
|
52
|
+
const isSchtasks = (c) => /(^|[\\/])schtasks(\.exe)?$/i.test(c);
|
|
22
53
|
const READ_ONLY = (c, a) => c === "launchctl" ? ["print", "print-disabled", "list"].includes(a[0] ?? "")
|
|
23
54
|
: c === "systemctl" ? a.some((x) => ["is-enabled", "is-active", "status", "show", "show-environment"].includes(x))
|
|
24
|
-
:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
55
|
+
: isSchtasks(c) ? (a[0] ?? "").toLowerCase() === "/query"
|
|
56
|
+
: true;
|
|
57
|
+
export function serviceRefusal(c, a, borrowed = { home: borrowedHome, scheduler: () => borrowedScheduler() }) {
|
|
58
|
+
if (READ_ONLY(c, a))
|
|
59
|
+
return null;
|
|
60
|
+
if ((c === "launchctl" || c === "systemctl") && borrowed.home()) {
|
|
61
|
+
return `refused: ${c} ${a[0] ?? ""} would change this user's real services, but HOME (${homedir()}) is not the user's home — a test or a sandbox. Set PANTHEON_SERVICES_ANY_HOME=1 to allow it.`;
|
|
62
|
+
}
|
|
63
|
+
if (isSchtasks(c) && borrowed.scheduler()) {
|
|
64
|
+
return `refused: schtasks ${a[0] ?? ""} would change this user's real Task Scheduler task, but this Pantheon home (${CONFIG_DIR}) is not the user's own — a test or a sandbox. Set PANTHEON_SERVICES_ANY_HOME=1 to allow it.`;
|
|
28
65
|
}
|
|
29
|
-
return
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function guardedExecFile(c, a) {
|
|
69
|
+
const refused = serviceRefusal(c, a);
|
|
70
|
+
if (refused)
|
|
71
|
+
throw new Error(refused);
|
|
72
|
+
return execFileSync(c, a, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }) ?? "";
|
|
30
73
|
}
|
|
31
74
|
export const realSys = () => ({
|
|
32
75
|
platform: platform(),
|
|
@@ -51,11 +94,8 @@ export function servicePath(envPath = process.env.PATH ?? "", managed = managedL
|
|
|
51
94
|
const last = managed ? [posix.dirname(managed.node)] : [];
|
|
52
95
|
return [...new Set([...first, ...envPath.split(":"), ...SERVICE_BASE_PATH, ...last].filter(Boolean))].join(":");
|
|
53
96
|
}
|
|
54
|
-
export function servicesUnavailable(
|
|
55
|
-
|
|
56
|
-
return null;
|
|
57
|
-
return "not available — Pantheon does not run in the background on Windows yet. "
|
|
58
|
-
+ "Your agent still answers peers whenever it is open; nothing runs here while you are away.";
|
|
97
|
+
export function servicesUnavailable(_plat = platform()) {
|
|
98
|
+
return null;
|
|
59
99
|
}
|
|
60
100
|
export function plistPath(kind = "worker") {
|
|
61
101
|
return join(homedir(), "Library", "LaunchAgents", `${LABEL(kind)}.plist`);
|
|
@@ -157,15 +197,16 @@ const systemdValue = (v) => {
|
|
|
157
197
|
throw new Error(`a path with a line break in it cannot be written into a systemd unit: ${JSON.stringify(v)}`);
|
|
158
198
|
return v.replace(/%/g, "%%");
|
|
159
199
|
};
|
|
160
|
-
export function systemdUnit(kind, cwd, args, path = servicePath()) {
|
|
200
|
+
export function systemdUnit(kind, cwd, args, path = servicePath(), opts = {}) {
|
|
161
201
|
const description = kind === "relay"
|
|
162
202
|
? "pantheon relay — the coordination plane for a space"
|
|
163
203
|
: kind === "worker"
|
|
164
204
|
? "pantheon worker — answer peers while away"
|
|
165
205
|
: "pantheon notifier — alert a person when their decision is required";
|
|
206
|
+
const graphical = kind === "notifier" && !!opts.graphical;
|
|
166
207
|
return `[Unit]
|
|
167
208
|
Description=${description}
|
|
168
|
-
After=network-online.target
|
|
209
|
+
${graphical ? "After=graphical-session.target\nPartOf=graphical-session.target" : "After=network-online.target"}
|
|
169
210
|
|
|
170
211
|
[Service]
|
|
171
212
|
Type=simple
|
|
@@ -176,9 +217,47 @@ Restart=always
|
|
|
176
217
|
RestartSec=30
|
|
177
218
|
|
|
178
219
|
[Install]
|
|
179
|
-
WantedBy
|
|
220
|
+
WantedBy=${graphical ? "graphical-session.target" : "default.target"}
|
|
180
221
|
`;
|
|
181
222
|
}
|
|
223
|
+
export const SESSION_ENV_VARS = ["DBUS_SESSION_BUS_ADDRESS", "DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY"];
|
|
224
|
+
export function sessionEnvToImport(managerEnv, shellEnv = process.env, exists = existsSync) {
|
|
225
|
+
if (shellEnv.SSH_CONNECTION || shellEnv.SSH_CLIENT || shellEnv.SSH_TTY)
|
|
226
|
+
return [];
|
|
227
|
+
if (!linuxGraphicalSession(shellEnv))
|
|
228
|
+
return [];
|
|
229
|
+
if (!shellEnv.DBUS_SESSION_BUS_ADDRESS || managerEnv.DBUS_SESSION_BUS_ADDRESS)
|
|
230
|
+
return [];
|
|
231
|
+
const runtime = managerEnv.XDG_RUNTIME_DIR || shellEnv.XDG_RUNTIME_DIR;
|
|
232
|
+
if (runtime && exists(join(runtime, "bus")))
|
|
233
|
+
return [];
|
|
234
|
+
return ["DBUS_SESSION_BUS_ADDRESS"];
|
|
235
|
+
}
|
|
236
|
+
export function dropOtherTargetLink(unit, graphical) {
|
|
237
|
+
const other = graphical ? "default.target" : "graphical-session.target";
|
|
238
|
+
const link = join(dirname(unit), `${other}.wants`, basename(unit));
|
|
239
|
+
try {
|
|
240
|
+
lstatSync(link);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
rmSync(link, { force: true });
|
|
247
|
+
return link;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
export function graphicalSessionActive(sys = realSys()) {
|
|
254
|
+
try {
|
|
255
|
+
return String(sys.exec("systemctl", ["--user", "is-active", "graphical-session.target"])).trim() === "active";
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
182
261
|
export function lingerEnabled(sys = realSys()) {
|
|
183
262
|
try {
|
|
184
263
|
const out = sys.exec("loginctl", ["show-user", sys.user || String(sys.uid), "--property=Linger"]);
|
|
@@ -193,6 +272,8 @@ export function installService(kind, cwd, args, sys = realSys()) {
|
|
|
193
272
|
const noServices = servicesUnavailable(sys.platform);
|
|
194
273
|
if (noServices)
|
|
195
274
|
throw new Error(noServices.replace(/^not available — /, ""));
|
|
275
|
+
if (sys.platform === "win32")
|
|
276
|
+
return installWindows(kind, resolve(cwd), args, sys);
|
|
196
277
|
const out = [];
|
|
197
278
|
let loaded = true;
|
|
198
279
|
let reason;
|
|
@@ -233,13 +314,35 @@ export function installService(kind, cwd, args, sys = realSys()) {
|
|
|
233
314
|
out.push(` stop: pantheon service uninstall ${kind}`);
|
|
234
315
|
}
|
|
235
316
|
else {
|
|
236
|
-
if (kind === "menubar"
|
|
237
|
-
throw new Error(
|
|
317
|
+
if (kind === "menubar") {
|
|
318
|
+
throw new Error(`the menu bar runs on macOS only, and this machine reports ${sys.platform} — on Linux, pantheon board is the view`);
|
|
238
319
|
}
|
|
239
|
-
const
|
|
320
|
+
const managerEnv = systemdUserEnv(sys);
|
|
321
|
+
const graphical = kind === "notifier" && graphicalSessionActive(sys);
|
|
322
|
+
const unit = unitPath(kind, managerEnv);
|
|
240
323
|
mkdirSync(dirname(unit), { recursive: true });
|
|
241
|
-
writeFileSync(unit, systemdUnit(kind, abs, args));
|
|
324
|
+
writeFileSync(unit, systemdUnit(kind, abs, args, servicePath(), { graphical }));
|
|
242
325
|
out.push(` wrote ${unit}`);
|
|
326
|
+
const dropped = dropOtherTargetLink(unit, graphical);
|
|
327
|
+
if (dropped)
|
|
328
|
+
out.push(` removed ${dropped} (an earlier install's start-up link)`);
|
|
329
|
+
if (kind === "notifier") {
|
|
330
|
+
const missing = sessionEnvToImport(managerEnv);
|
|
331
|
+
if (missing.length) {
|
|
332
|
+
try {
|
|
333
|
+
sys.exec("systemctl", ["--user", "import-environment", ...missing]);
|
|
334
|
+
out.push(` gave systemd this desktop session's bus address (${missing.join(", ")}); it lasts until systemd --user stops, and is good only while this session is`);
|
|
335
|
+
out.push(` ! your desktop does not pass it to systemd at login — for alerts after the next login, add this to your session's autostart:`);
|
|
336
|
+
out.push(` dbus-update-activation-environment --systemd ${missing.join(" ")}`);
|
|
337
|
+
}
|
|
338
|
+
catch (e) {
|
|
339
|
+
out.push(` could not pass ${missing.join(", ")} to systemd (${e.message.split("\n")[0]}) — the notifier cannot reach this session's bus until you add dbus-update-activation-environment --systemd ${missing.join(" ")} to your session's autostart`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
out.push(graphical
|
|
343
|
+
? " starts with your desktop session (graphical-session.target) and stops when you log out"
|
|
344
|
+
: " starts with your user session (default.target) — this desktop does not use graphical-session.target");
|
|
345
|
+
}
|
|
243
346
|
try {
|
|
244
347
|
sys.exec("systemctl", ["--user", "daemon-reload"]);
|
|
245
348
|
sys.exec("systemctl", ["--user", "enable", "--now", `pantheon-${kind}`]);
|
|
@@ -262,7 +365,10 @@ export function uninstallService(kind, sys = realSys()) {
|
|
|
262
365
|
const strays = unitPaths(kind, {}).filter((u) => existsSync(u));
|
|
263
366
|
for (const u of strays)
|
|
264
367
|
unlinkSync(u);
|
|
265
|
-
|
|
368
|
+
const host = uninstallWindows(kind, sys);
|
|
369
|
+
if (!host)
|
|
370
|
+
return strays.length ? strays.map((u) => ` removed ${u}`) : [` no ${kind} service installed`];
|
|
371
|
+
return [...host, ...strays.map((u) => ` removed ${u}`)];
|
|
266
372
|
}
|
|
267
373
|
if (sys.platform === "darwin") {
|
|
268
374
|
const p = plistPath(kind);
|
|
@@ -306,6 +412,10 @@ export function uninstallService(kind, sys = realSys()) {
|
|
|
306
412
|
return out;
|
|
307
413
|
}
|
|
308
414
|
export function disableService(kind, sys = realSys()) {
|
|
415
|
+
if (sys.platform === "win32") {
|
|
416
|
+
disableWindows(kind, sys);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
309
419
|
if (sys.platform === "darwin") {
|
|
310
420
|
try {
|
|
311
421
|
sys.exec("launchctl", ["bootout", `gui/${sys.uid}/${LABEL(kind)}`]);
|
|
@@ -326,6 +436,11 @@ function outOf(exec, c, a) {
|
|
|
326
436
|
}
|
|
327
437
|
}
|
|
328
438
|
export function serviceState(kind, exec = realExec, plat = platform(), uid = process.getuid?.() ?? 501) {
|
|
439
|
+
if (plat === "win32") {
|
|
440
|
+
const f = hostFiles();
|
|
441
|
+
const loaded = outOf(exec, "schtasks", ["/Query", "/TN", taskName(exec)]).ok;
|
|
442
|
+
return { disabled: isHostKind(kind) && existsSync(f.disabled(kind)), loaded, running: isHostKind(kind) && !!hostChildRunning(kind), ...(isHostKind(kind) ? { from: f.definition(kind) } : {}) };
|
|
443
|
+
}
|
|
329
444
|
if (plat === "darwin") {
|
|
330
445
|
const dis = outOf(exec, "launchctl", ["print-disabled", `gui/${uid}`]).out;
|
|
331
446
|
const disabled = new RegExp(`"${LABEL(kind).replace(/\./g, "\\.")}"\\s*=>\\s*(disabled|true)`).test(dis);
|
|
@@ -344,8 +459,20 @@ export function restartService(kind, exec = realExec, opts = {}) {
|
|
|
344
459
|
const uid = process.getuid?.() ?? 501;
|
|
345
460
|
const st = serviceState(kind, exec, plat, uid);
|
|
346
461
|
if (st.disabled)
|
|
347
|
-
return [` ${kind}: left stopped (you stopped it) — start it again: pantheon service install ${kind}`];
|
|
462
|
+
return [` ${kind}: left stopped (you stopped it) — start it again: pantheon service ${plat === "win32" ? "start" : "install"} ${kind}`];
|
|
348
463
|
try {
|
|
464
|
+
if (plat === "win32") {
|
|
465
|
+
if (hostAlive(readHostState())) {
|
|
466
|
+
const f = hostFiles();
|
|
467
|
+
mkdirSync(f.dir, { recursive: true });
|
|
468
|
+
writeFileSync(f.restart(kind), new Date().toISOString());
|
|
469
|
+
return [` ${kind}: restarted on the new code (the Pantheon host starts it again within seconds)`];
|
|
470
|
+
}
|
|
471
|
+
if (!st.loaded)
|
|
472
|
+
return [` ${kind}: the ${taskName(exec)} task is not registered — pantheon service install ${kind}`];
|
|
473
|
+
exec("schtasks", ["/Run", "/TN", taskName(exec)]);
|
|
474
|
+
return [` ${kind}: the Pantheon host was not running — started it, and the ${kind} with it, on the new code`];
|
|
475
|
+
}
|
|
349
476
|
if (plat === "darwin") {
|
|
350
477
|
if (st.loaded && st.from && resolve(st.from) !== resolve(plistPath(kind)) && existsSync(plistPath(kind))) {
|
|
351
478
|
exec("launchctl", ["bootout", `gui/${uid}/${LABEL(kind)}`]);
|
|
@@ -377,8 +504,20 @@ export function restartService(kind, exec = realExec, opts = {}) {
|
|
|
377
504
|
return [` ${kind}: could not restart (${e.message.split("\n")[0]}) — pantheon service restart ${kind}`];
|
|
378
505
|
}
|
|
379
506
|
}
|
|
380
|
-
export function refreshServiceDefinition(kind, plat = platform()) {
|
|
507
|
+
export function refreshServiceDefinition(kind, plat = platform(), sys) {
|
|
381
508
|
try {
|
|
509
|
+
if (plat === "win32") {
|
|
510
|
+
if (!isHostKind(kind) || !existsSync(hostFiles().definition(kind)))
|
|
511
|
+
return { changed: false };
|
|
512
|
+
const s = sys ?? realSys();
|
|
513
|
+
const t = ensureTask(s, undefined, { migrate: "unattended" });
|
|
514
|
+
const name = taskName(s.exec, s.env);
|
|
515
|
+
const said = [
|
|
516
|
+
...(t.changed ? [` ${kind}: the ${name} task now runs ${t.launch.command} — from the host's next start`] : []),
|
|
517
|
+
...(t.migrated ? [` removed the "${LEGACY_TASK_NAME}" task an earlier Pantheon registered — ${name} starts the host from your next logon (the one running now keeps running)`] : []),
|
|
518
|
+
];
|
|
519
|
+
return said.length ? { changed: false, line: said.join("\n") } : { changed: false };
|
|
520
|
+
}
|
|
382
521
|
if (plat === "darwin") {
|
|
383
522
|
const p = plistPath(kind);
|
|
384
523
|
if (!existsSync(p))
|
|
@@ -397,21 +536,21 @@ export function refreshServiceDefinition(kind, plat = platform()) {
|
|
|
397
536
|
writeFileSync(p, servicePlist(kind, cwd, logDir, argv.slice(at + 1)));
|
|
398
537
|
return { changed: true, line: ` ${kind}: its log now goes to ${join(logDir, `${kind}.log`)}${log ? ` (was ${unxml(log)})` : ""}` };
|
|
399
538
|
}
|
|
400
|
-
if (plat === "win32")
|
|
401
|
-
return { changed: false };
|
|
402
539
|
const u = existingUnitPath(kind);
|
|
403
540
|
if (!u)
|
|
404
541
|
return { changed: false };
|
|
405
542
|
const unit = readFileSync(u, "utf8");
|
|
406
|
-
|
|
407
|
-
|
|
543
|
+
const dropped = dropOtherTargetLink(u, kind === "notifier" && /^WantedBy=graphical-session\.target/m.test(unit));
|
|
544
|
+
if (/^Environment="PATH=/m.test(unit) && !/^Environment="PATH=.*\$\$/m.test(unit)) {
|
|
545
|
+
return dropped ? { changed: true, line: ` ${kind}: removed a second start-up link (${dropped})` } : { changed: false };
|
|
546
|
+
}
|
|
408
547
|
const exec = /^ExecStart=(.*)$/m.exec(unit)?.[1] ?? "";
|
|
409
548
|
const words = exec.startsWith('"') ? [...exec.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((m) => m[1].replace(/\\(.)/g, "$1").replace(/%%/g, "%").replace(/\$\$/g, "$")) : exec.split(/\s+/);
|
|
410
549
|
const at = words.indexOf(kind);
|
|
411
550
|
const cwd = (/^WorkingDirectory=(.*)$/m.exec(unit)?.[1] ?? homedir()).replace(/%%/g, "%");
|
|
412
551
|
if (at < 0)
|
|
413
552
|
return { changed: false };
|
|
414
|
-
writeFileSync(u, systemdUnit(kind, cwd, words.slice(at + 1)));
|
|
553
|
+
writeFileSync(u, systemdUnit(kind, cwd, words.slice(at + 1), servicePath(), { graphical: /^WantedBy=graphical-session\.target/m.test(unit) }));
|
|
415
554
|
return { changed: true, line: ` ${kind}: its unit now quotes its paths and carries your PATH` };
|
|
416
555
|
}
|
|
417
556
|
catch (e) {
|
|
@@ -422,11 +561,298 @@ const unxml = (s) => s.replace(/</g, "<").replace(/>/g, ">").replace(/&
|
|
|
422
561
|
export function serviceInstalled(kind, plat = platform()) {
|
|
423
562
|
try {
|
|
424
563
|
if (plat === "win32")
|
|
425
|
-
return
|
|
564
|
+
return isHostKind(kind) && existsSync(hostFiles().definition(kind));
|
|
426
565
|
return plat === "darwin" ? existsSync(plistPath(kind)) : existingUnitPath(kind) !== null;
|
|
427
566
|
}
|
|
428
567
|
catch {
|
|
429
568
|
return false;
|
|
430
569
|
}
|
|
431
570
|
}
|
|
571
|
+
const blockingSleep = (ms) => { try {
|
|
572
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
573
|
+
}
|
|
574
|
+
catch { } };
|
|
575
|
+
function waitFor(done, ms, sleep) {
|
|
576
|
+
for (let waited = 0;; waited += 200) {
|
|
577
|
+
if (done())
|
|
578
|
+
return true;
|
|
579
|
+
if (waited >= ms)
|
|
580
|
+
return false;
|
|
581
|
+
sleep(200);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function nonDefaultHome() {
|
|
585
|
+
const ph = process.env.PANTHEON_HOME;
|
|
586
|
+
if (!ph)
|
|
587
|
+
return null;
|
|
588
|
+
return resolve(ph).toLowerCase() === resolve(join(homedir(), ".pantheon")).toLowerCase() ? null : resolve(ph);
|
|
589
|
+
}
|
|
590
|
+
export function windowsVersionManaged(p) {
|
|
591
|
+
return isVersionManagedPath(p)
|
|
592
|
+
|| /[\\/]nvm[\\/]v\d+\.\d+\.\d+[\\/]/i.test(p)
|
|
593
|
+
|| /[\\/]volta[\\/]tools[\\/]/i.test(p)
|
|
594
|
+
|| /[\\/]fnm[\\/]node-versions[\\/]/i.test(p)
|
|
595
|
+
|| /[\\/]fnm_multishells[\\/]/i.test(p)
|
|
596
|
+
|| /[\\/]scoop[\\/]apps[\\/]nodejs(?:-lts)?[\\/]\d/i.test(p);
|
|
597
|
+
}
|
|
598
|
+
export function hostRuntime(o) {
|
|
599
|
+
if (o.layout)
|
|
600
|
+
return { node: o.layout.node, cli: o.layout.cli, flags: ["--use-system-ca"] };
|
|
601
|
+
const moving = windowsVersionManaged;
|
|
602
|
+
if (!moving(o.execPath) && !moving(o.cli))
|
|
603
|
+
return { node: o.execPath, cli: o.cli, flags: o.flags };
|
|
604
|
+
if (o.installed && o.exists(o.installed.node) && o.exists(o.installed.cli))
|
|
605
|
+
return { node: o.installed.node, cli: o.installed.cli, flags: ["--use-system-ca"] };
|
|
606
|
+
if (!moving(o.cli)) {
|
|
607
|
+
const node = o.nodesOnPath.find((n) => !!n && !moving(n) && o.exists(n));
|
|
608
|
+
if (node)
|
|
609
|
+
return { node, cli: o.cli, flags: o.flags };
|
|
610
|
+
}
|
|
611
|
+
throw new Error(`this Pantheon runs from ${moving(o.cli) ? o.cli : o.execPath}, a folder a Node version manager replaces when you switch Node versions — a logon task naming it would start nothing after the next switch. Install Pantheon with its own Node (irm https://relay.joinpantheon.network/install.ps1 | iex) or with a Node from nodejs.org (npm install -g @join-pantheon/cli), then run this again`);
|
|
612
|
+
}
|
|
613
|
+
function installedManagedLayout(env) {
|
|
614
|
+
const local = env.LOCALAPPDATA;
|
|
615
|
+
return local ? managedLayout(`${local.replace(/[\\/]+$/, "")}\\Pantheon\\runtime\\node\\node.exe`, "win32") : null;
|
|
616
|
+
}
|
|
617
|
+
export function desiredTask(sys, log, rt) {
|
|
618
|
+
const env = sys.env ?? process.env;
|
|
619
|
+
const runtime = rt || !sys.hostRuntime ? hostRuntime({
|
|
620
|
+
execPath: process.execPath, cli: resolve(HERE, "cli.js"), flags: childNodeFlags(),
|
|
621
|
+
layout: managedLayout(process.execPath, "win32"), installed: installedManagedLayout(env),
|
|
622
|
+
nodesOnPath: [which("node") ?? ""], exists: existsSync,
|
|
623
|
+
...rt,
|
|
624
|
+
}) : sys.hostRuntime;
|
|
625
|
+
const shim = ensureHostShim({ exec: (c, a) => sys.exec(c, a), env, ...(log ? { log } : {}) });
|
|
626
|
+
const home = nonDefaultHome();
|
|
627
|
+
const launch = hostLaunch({ shim, node: runtime.node, flags: runtime.flags, cli: runtime.cli, home, launch: hostFiles().launch, powershell: windowsPowerShell(env) });
|
|
628
|
+
const name = taskName(sys.exec, env);
|
|
629
|
+
return { launch, argv: hostArgv(runtime), name, xml: taskXml({ launch, userId: taskUserId(sys.user, env), workingDirectory: homedir(), name }) };
|
|
630
|
+
}
|
|
631
|
+
export function ensureTask(sys, log, opts = {}) {
|
|
632
|
+
const f = hostFiles();
|
|
633
|
+
const { xml, launch, argv, name } = desiredTask(sys, log);
|
|
634
|
+
const text = launchFileText(argv);
|
|
635
|
+
let had = null;
|
|
636
|
+
try {
|
|
637
|
+
had = readFileSync(f.launch, "utf8");
|
|
638
|
+
}
|
|
639
|
+
catch { }
|
|
640
|
+
if (had !== text) {
|
|
641
|
+
mkdirSync(f.dir, { recursive: true });
|
|
642
|
+
writeFileSync(`${f.launch}.next`, text);
|
|
643
|
+
renameSync(`${f.launch}.next`, f.launch);
|
|
644
|
+
}
|
|
645
|
+
let last = null;
|
|
646
|
+
try {
|
|
647
|
+
last = fromUtf16le(readFileSync(f.taskXml));
|
|
648
|
+
}
|
|
649
|
+
catch { }
|
|
650
|
+
let registered = false;
|
|
651
|
+
try {
|
|
652
|
+
sys.exec("schtasks", ["/Query", "/TN", name]);
|
|
653
|
+
registered = true;
|
|
654
|
+
}
|
|
655
|
+
catch { }
|
|
656
|
+
let changed = false;
|
|
657
|
+
if (!registered || last !== xml) {
|
|
658
|
+
mkdirSync(f.dir, { recursive: true });
|
|
659
|
+
const next = `${f.taskXml}.next`;
|
|
660
|
+
writeFileSync(next, utf16le(xml));
|
|
661
|
+
try {
|
|
662
|
+
sys.exec("schtasks", ["/Create", "/XML", next, "/TN", name, "/F"]);
|
|
663
|
+
}
|
|
664
|
+
catch (e) {
|
|
665
|
+
rmSync(next, { force: true });
|
|
666
|
+
throw e;
|
|
667
|
+
}
|
|
668
|
+
renameSync(next, f.taskXml);
|
|
669
|
+
changed = true;
|
|
670
|
+
}
|
|
671
|
+
const migrated = opts.migrate ? replaceLegacyTask(sys, name, opts.migrate === "unattended", log) : false;
|
|
672
|
+
return { changed, launch, migrated };
|
|
673
|
+
}
|
|
674
|
+
function replaceLegacyTask(sys, name, unattended, log) {
|
|
675
|
+
const env = sys.env ?? process.env;
|
|
676
|
+
if (!legacyTaskIsUsers(sys.exec, env))
|
|
677
|
+
return false;
|
|
678
|
+
if (!unattended)
|
|
679
|
+
for (const l of stopWindowsHost(sys))
|
|
680
|
+
log?.(l);
|
|
681
|
+
try {
|
|
682
|
+
sys.exec("schtasks", ["/Delete", "/TN", LEGACY_TASK_NAME, "/F"]);
|
|
683
|
+
log?.(` removed the "${LEGACY_TASK_NAME}" task an earlier Pantheon registered — it is ${name} now, so a second person on this PC can have their own${unattended && hostUp() ? " (the host running now keeps running; from your next logon the new task starts it)" : ""}`);
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
catch (e) {
|
|
687
|
+
log?.(` could not remove the older "${LEGACY_TASK_NAME}" task (${e.message.split("\n")[0]}) — schtasks /Delete /TN ${LEGACY_TASK_NAME} /F`);
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const hostUp = () => hostAlive(readHostState());
|
|
692
|
+
const windowsBorrowed = (sys) => (sys.borrowed ?? (() => borrowedScheduler(sys.env ?? process.env)))();
|
|
693
|
+
function installWindows(kind, cwd, args, sys) {
|
|
694
|
+
if (!isHostKind(kind)) {
|
|
695
|
+
throw new Error(kind === "menubar"
|
|
696
|
+
? "the menu bar is macOS's — on Windows the Pantheon tray icon runs with the background host that pantheon service install worker (or notifier) sets up"
|
|
697
|
+
: "the relay does not run as a Windows background service — run it in a terminal (pantheon relay), or use the hosted relay");
|
|
698
|
+
}
|
|
699
|
+
const out = [];
|
|
700
|
+
let loaded = true;
|
|
701
|
+
let reason;
|
|
702
|
+
const f = hostFiles();
|
|
703
|
+
out.push(` wrote ${writeDefinition({ kind, cwd, args })}`);
|
|
704
|
+
rmSync(f.disabled(kind), { force: true });
|
|
705
|
+
if (clearTrayQuit())
|
|
706
|
+
out.push(" the tray icon, which you had quit, shows again");
|
|
707
|
+
mkdirSync(serviceLogDir(), { recursive: true });
|
|
708
|
+
const name = taskName(sys.exec, sys.env);
|
|
709
|
+
try {
|
|
710
|
+
const t = ensureTask(sys, (l) => out.push(l), { migrate: true });
|
|
711
|
+
if (t.changed) {
|
|
712
|
+
out.push(` registered the ${name} task — it starts Pantheon's background host at logon, hidden${t.launch.how === "powershell" ? " (through PowerShell: a console may show for a moment at logon)" : ""}`);
|
|
713
|
+
}
|
|
714
|
+
if (hostUp())
|
|
715
|
+
out.push(` the host is running — it starts the ${kind} within seconds, and restarts it if it stops`);
|
|
716
|
+
else {
|
|
717
|
+
sys.exec("schtasks", ["/Run", "/TN", name]);
|
|
718
|
+
out.push(` started — the ${kind} now starts at logon and restarts if it stops`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
catch (e) {
|
|
722
|
+
const why = e.message.split("\n")[0];
|
|
723
|
+
loaded = false;
|
|
724
|
+
reason = why;
|
|
725
|
+
if (windowsBorrowed(sys)) {
|
|
726
|
+
reason = `this Pantheon home (${CONFIG_DIR}) is not your own, so Task Scheduler was not touched`;
|
|
727
|
+
out.push(` left unregistered — ${reason}; your own services are unchanged`);
|
|
728
|
+
}
|
|
729
|
+
else
|
|
730
|
+
out.push(` could not register or start the ${name} task: ${why}`);
|
|
731
|
+
}
|
|
732
|
+
out.push(` logs: ${serviceLogFile(kind)}`);
|
|
733
|
+
out.push(` stop: pantheon service uninstall ${kind}`);
|
|
734
|
+
return Object.assign(out, { loaded, ...(reason ? { reason } : {}) });
|
|
735
|
+
}
|
|
736
|
+
export function stopWindowsHost(sys = realSys()) {
|
|
737
|
+
const f = hostFiles();
|
|
738
|
+
if (!hostUp())
|
|
739
|
+
return [];
|
|
740
|
+
mkdirSync(f.dir, { recursive: true });
|
|
741
|
+
writeFileSync(f.stop, new Date().toISOString());
|
|
742
|
+
const gone = waitFor(() => !hostUp(), 10_000, sys.sleep ?? blockingSleep);
|
|
743
|
+
if (gone)
|
|
744
|
+
return [" the Pantheon host stopped (the tray with it)"];
|
|
745
|
+
const env = sys.env ?? process.env;
|
|
746
|
+
try {
|
|
747
|
+
sys.exec("schtasks", ["/End", "/TN", taskName(sys.exec, env)]);
|
|
748
|
+
}
|
|
749
|
+
catch { }
|
|
750
|
+
if (legacyTaskIsUsers(sys.exec, env)) {
|
|
751
|
+
try {
|
|
752
|
+
sys.exec("schtasks", ["/End", "/TN", LEGACY_TASK_NAME]);
|
|
753
|
+
}
|
|
754
|
+
catch { }
|
|
755
|
+
}
|
|
756
|
+
const pid = readHostState()?.pid;
|
|
757
|
+
return [` the Pantheon host did not stop in time — ended its task${hostUp() ? `; pid ${pid} still answers: taskkill /PID ${pid} /T /F` : ""}`];
|
|
758
|
+
}
|
|
759
|
+
function uninstallWindows(kind, sys) {
|
|
760
|
+
if (!isHostKind(kind))
|
|
761
|
+
return null;
|
|
762
|
+
const f = hostFiles();
|
|
763
|
+
const def = f.definition(kind);
|
|
764
|
+
if (!existsSync(def))
|
|
765
|
+
return null;
|
|
766
|
+
const out = [];
|
|
767
|
+
rmSync(def, { force: true });
|
|
768
|
+
rmSync(f.disabled(kind), { force: true });
|
|
769
|
+
rmSync(f.restart(kind), { force: true });
|
|
770
|
+
const others = HOST_KINDS.filter((k) => k !== kind && existsSync(f.definition(k)));
|
|
771
|
+
const sleep = sys.sleep ?? blockingSleep;
|
|
772
|
+
if (others.length) {
|
|
773
|
+
const stopped = waitFor(() => !hostChildRunning(kind), 6_000, sleep);
|
|
774
|
+
out.push(stopped ? " stopped" : " the host stops it within seconds");
|
|
775
|
+
out.push(` removed ${def}`);
|
|
776
|
+
out.push(` the Pantheon host keeps running the ${others.join(" and ")}`);
|
|
777
|
+
return out;
|
|
778
|
+
}
|
|
779
|
+
out.push(...stopWindowsHost(sys));
|
|
780
|
+
const env = sys.env ?? process.env;
|
|
781
|
+
const name = taskName(sys.exec, env);
|
|
782
|
+
const remove = (task) => {
|
|
783
|
+
try {
|
|
784
|
+
sys.exec("schtasks", ["/Delete", "/TN", task, "/F"]);
|
|
785
|
+
out.push(` removed the ${task} task`);
|
|
786
|
+
if (task === name) {
|
|
787
|
+
rmSync(f.taskXml, { force: true });
|
|
788
|
+
rmSync(f.launch, { force: true });
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
catch (e) {
|
|
792
|
+
let still = false;
|
|
793
|
+
try {
|
|
794
|
+
sys.exec("schtasks", ["/Query", "/TN", task]);
|
|
795
|
+
still = true;
|
|
796
|
+
}
|
|
797
|
+
catch { }
|
|
798
|
+
if (still) {
|
|
799
|
+
out.push(windowsBorrowed(sys)
|
|
800
|
+
? ` left the ${task} task alone — this Pantheon home (${CONFIG_DIR}) is not your own; with nothing installed here its host exits at once`
|
|
801
|
+
: ` could not remove the ${task} task (${e.message.split("\n")[0]}) — schtasks /Delete /TN ${task} /F`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
remove(name);
|
|
806
|
+
if (legacyTaskIsUsers(sys.exec, env))
|
|
807
|
+
remove(LEGACY_TASK_NAME);
|
|
808
|
+
out.push(` removed ${def}`);
|
|
809
|
+
if (!hostUp()) {
|
|
810
|
+
try {
|
|
811
|
+
rmSync(f.dir, { recursive: true, force: true });
|
|
812
|
+
out.push(` removed ${f.dir}`);
|
|
813
|
+
}
|
|
814
|
+
catch { }
|
|
815
|
+
}
|
|
816
|
+
return out;
|
|
817
|
+
}
|
|
818
|
+
function disableWindows(kind, sys) {
|
|
819
|
+
if (!isHostKind(kind))
|
|
820
|
+
throw new Error(`the ${kind} is not a background service on Windows`);
|
|
821
|
+
const f = hostFiles();
|
|
822
|
+
mkdirSync(f.dir, { recursive: true });
|
|
823
|
+
writeFileSync(f.disabled(kind), new Date().toISOString());
|
|
824
|
+
waitFor(() => !hostChildRunning(kind), 6_000, sys.sleep ?? blockingSleep);
|
|
825
|
+
}
|
|
826
|
+
export function enableService(kind, sys = realSys()) {
|
|
827
|
+
if (sys.platform !== "win32")
|
|
828
|
+
throw new Error("service start is the Windows host's; elsewhere: pantheon service install");
|
|
829
|
+
if (!isHostKind(kind) || !readDefinition(kind))
|
|
830
|
+
throw new Error(`the ${kind} is not installed here — pantheon service install ${kind}`);
|
|
831
|
+
const f = hostFiles();
|
|
832
|
+
const was = existsSync(f.disabled(kind));
|
|
833
|
+
rmSync(f.disabled(kind), { force: true });
|
|
834
|
+
const tray = clearTrayQuit() ? [" the tray icon, which you had quit, shows again"] : [];
|
|
835
|
+
if (hostUp())
|
|
836
|
+
return [` ${kind}: ${was ? "resumed" : "already on"} — the Pantheon host ${was ? "starts it within seconds" : "runs it"}`, ...tray];
|
|
837
|
+
const lines = [];
|
|
838
|
+
ensureTask(sys, (l) => lines.push(l), { migrate: true });
|
|
839
|
+
sys.exec("schtasks", ["/Run", "/TN", taskName(sys.exec, sys.env)]);
|
|
840
|
+
return [...lines, ` ${kind}: ${was ? "resumed" : "on"} — started the Pantheon host, which runs it`, ...tray];
|
|
841
|
+
}
|
|
842
|
+
export function showTray(sys = realSys()) {
|
|
843
|
+
const f = hostFiles();
|
|
844
|
+
const quit = clearTrayQuit();
|
|
845
|
+
if (!HOST_KINDS.some((k) => existsSync(f.definition(k)))) {
|
|
846
|
+
return [" the tray runs with Pantheon's background host, and nothing is installed here — pantheon service install notifier (or worker)"];
|
|
847
|
+
}
|
|
848
|
+
if (hostUp()) {
|
|
849
|
+
mkdirSync(f.dir, { recursive: true });
|
|
850
|
+
writeFileSync(f.restart("tray"), new Date().toISOString());
|
|
851
|
+
return [` ${quit ? "the tray icon shows again within seconds" : "the tray icon is started again within seconds"}`];
|
|
852
|
+
}
|
|
853
|
+
const lines = [];
|
|
854
|
+
ensureTask(sys, (l) => lines.push(l), { migrate: true });
|
|
855
|
+
sys.exec("schtasks", ["/Run", "/TN", taskName(sys.exec, sys.env)]);
|
|
856
|
+
return [...lines, " started the Pantheon host, and the tray icon with it"];
|
|
857
|
+
}
|
|
432
858
|
//# sourceMappingURL=service.js.map
|
package/dist/setup-guide.js
CHANGED
|
@@ -7,6 +7,7 @@ import { applyProfileInit, loadProfile, profilePath, saveProfile, wakeMode } fro
|
|
|
7
7
|
import { installService as realInstall, uninstallService as realUninstall, serviceInstalled as realInstalled } from "./service.js";
|
|
8
8
|
import { personaDir } from "./persona.js";
|
|
9
9
|
import { hudBinaryPath } from "./notifier.js";
|
|
10
|
+
import { hostEnv } from "./host-env.js";
|
|
10
11
|
import { grant, inWokenRun, loadPolicy, revoke, savePolicy, unsandboxedAllowed } from "./worker-policy.js";
|
|
11
12
|
export const DECISION_AREAS = [
|
|
12
13
|
{ id: "product", label: "Product — what we build, for whom, and what goes in a release" },
|
|
@@ -175,6 +176,8 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
175
176
|
const serves = ctx.project ? { path: ctx.project, repos: true } : ctx.serves ?? null;
|
|
176
177
|
const os = deps.platform ?? process.platform;
|
|
177
178
|
const mac = os === "darwin";
|
|
179
|
+
const linuxDesktop = os === "linux" && (deps.desktop ?? hostEnv(process.env, os).desktop);
|
|
180
|
+
const winDesktop = os === "win32" && (deps.desktop ?? hostEnv(process.env, os).desktop);
|
|
178
181
|
const install = deps.installService ?? realInstall;
|
|
179
182
|
const uninstall = deps.uninstallService ?? realUninstall;
|
|
180
183
|
const installed = deps.serviceInstalled ?? realInstalled;
|
|
@@ -295,7 +298,7 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
295
298
|
warnings.push(`profile saved but not published (${e.message}) — run \`pantheon profile publish\` when the relay is reachable`);
|
|
296
299
|
}
|
|
297
300
|
}
|
|
298
|
-
const wiring = deps.applyAgentWiring ?? ((ap) => installForUser(ap).map((t) => ({ tool: t.tool, status: t.status })));
|
|
301
|
+
const wiring = deps.applyAgentWiring ?? ((ap) => installForUser(ap, undefined, { direct: true }).map((t) => ({ tool: t.tool, status: t.status })));
|
|
299
302
|
try {
|
|
300
303
|
const rows = wiring(a.approval);
|
|
301
304
|
const failed = rows.filter((t) => /^failed/.test(t.status));
|
|
@@ -385,13 +388,27 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
385
388
|
applied.push(`alerts off — decisions still wait in ${waitIn}`);
|
|
386
389
|
}
|
|
387
390
|
else if (optOut.notifications) {
|
|
388
|
-
applied.push(`alerts: not installed (--no-notifications) — decisions wait in ${waitIn}. Your choice (${chosenAlerts.toLowerCase()}) is saved for when you turn them on${mac ? ": pantheon service install notifier" : ""}`);
|
|
391
|
+
applied.push(`alerts: not installed (--no-notifications) — decisions wait in ${waitIn}. Your choice (${chosenAlerts.toLowerCase()}) is saved for when you turn them on${mac || linuxDesktop || winDesktop ? ": pantheon service install notifier" : ""}`);
|
|
389
392
|
}
|
|
390
|
-
else if (!
|
|
391
|
-
applied.push(`alerts: none
|
|
393
|
+
else if (os === "linux" && !linuxDesktop) {
|
|
394
|
+
applied.push(`alerts: none in this session — desktop alerts need a graphical login, and this is SSH or headless. Decisions wait in ${waitIn} and pantheon board --watch (your choice, ${chosenAlerts.toLowerCase()}, is saved)`);
|
|
395
|
+
}
|
|
396
|
+
else if (os === "win32" && !winDesktop) {
|
|
397
|
+
applied.push(`alerts: none in this session — desktop alerts need the Windows desktop, and this is a remote shell (SSH). Decisions wait in ${waitIn} and pantheon board; set Pantheon up from a session at the desktop for alerts (your choice, ${chosenAlerts.toLowerCase()}, is saved)`);
|
|
398
|
+
}
|
|
399
|
+
else if (!mac && !linuxDesktop && !winDesktop) {
|
|
400
|
+
applied.push(`alerts: none on this machine — desktop alerts run on macOS, Windows and Linux desktops, and this is ${os}. Decisions wait in ${waitIn} (your choice, ${chosenAlerts.toLowerCase()}, is saved)`);
|
|
392
401
|
}
|
|
393
402
|
else {
|
|
394
|
-
if (!installed("notifier")) {
|
|
403
|
+
if (!installed("notifier") && (linuxDesktop || winDesktop)) {
|
|
404
|
+
try {
|
|
405
|
+
install("notifier", deps.home ?? homedir(), []);
|
|
406
|
+
}
|
|
407
|
+
catch (e) {
|
|
408
|
+
warnings.push(`alerts did not install: ${e.message}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
else if (!installed("notifier")) {
|
|
395
412
|
if (existsSync(hudBinaryPath(deps.home))) {
|
|
396
413
|
try {
|
|
397
414
|
install("notifier", deps.home ?? homedir(), []);
|