9remote 2.0.2 → 2.0.7
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/cli.cjs +1 -1
- package/dist/install.cjs +2 -0
- package/dist/ptyDaemon.cjs +1 -1
- package/dist/server.cjs +1 -1
- package/dist/ui/assets/{index-COWVKicT.css → index-BMHG73CL.css} +1 -1
- package/dist/ui/assets/index-Bg86Demx.js +8 -0
- package/dist/ui/index.html +2 -2
- package/package.json +4 -6
- package/cli/index.js +0 -1330
- package/cli/scripts/install.js +0 -19
- package/cli/utils/apiKey.js +0 -77
- package/cli/utils/assets/trayIcon.ico +0 -0
- package/cli/utils/cloudflared.js +0 -493
- package/cli/utils/machineId.js +0 -22
- package/cli/utils/permissions.js +0 -45
- package/cli/utils/pids.js +0 -114
- package/cli/utils/state.js +0 -115
- package/cli/utils/token.js +0 -32
- package/cli/utils/tray.js +0 -251
- package/cli/utils/tui.js +0 -445
- package/cli/utils/updateChecker.js +0 -358
- package/dist/ui/assets/index-BWfJSBGG.js +0 -8
- package/index.js +0 -275
- package/lib/constants.js +0 -64
- package/lib/deviceApproval.js +0 -152
- package/lib/router.js +0 -134
- package/lib/socketio.js +0 -240
package/cli/utils/pids.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Centralized PID management for 9remote long-lived processes.
|
|
3
|
-
*
|
|
4
|
-
* We track ONLY two PIDs — the minimum needed to safely release file locks
|
|
5
|
-
* during `npm i -g 9remote@latest` without killing unrelated apps on the
|
|
6
|
-
* machine:
|
|
7
|
-
*
|
|
8
|
-
* - agent → the CLI that holds dist/cli.cjs open. Its children
|
|
9
|
-
* (server node.exe + tray helper binary) die automatically
|
|
10
|
-
* when we kill the agent tree (`taskkill /F /T`).
|
|
11
|
-
* - cloudflared → spawned as a child of agent too, but tracked separately
|
|
12
|
-
* because it may become orphan if agent crashes, and
|
|
13
|
-
* because `killCloudflared()` is called at runtime for
|
|
14
|
-
* network-change restarts.
|
|
15
|
-
*
|
|
16
|
-
* PID files live in ~/.9remote/pids/<name>.pid. Update scripts kill by PID
|
|
17
|
-
* only — never by image name (taskkill /IM) or commandline match, since
|
|
18
|
-
* those would also terminate unrelated node.exe / cloudflared.exe processes.
|
|
19
|
-
*/
|
|
20
|
-
import fs from "fs";
|
|
21
|
-
import path from "path";
|
|
22
|
-
import os from "os";
|
|
23
|
-
import { execSync } from "child_process";
|
|
24
|
-
|
|
25
|
-
const PIDS_DIR = path.join(os.homedir(), ".9remote", "pids");
|
|
26
|
-
|
|
27
|
-
function ensureDir() {
|
|
28
|
-
try {
|
|
29
|
-
if (!fs.existsSync(PIDS_DIR)) fs.mkdirSync(PIDS_DIR, { recursive: true });
|
|
30
|
-
} catch {}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function pidPath(name) {
|
|
34
|
-
return path.join(PIDS_DIR, `${name}.pid`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Write PID to ~/.9remote/pids/<name>.pid. Silent fail. */
|
|
38
|
-
export function writePid(name, pid) {
|
|
39
|
-
if (!pid) return;
|
|
40
|
-
try {
|
|
41
|
-
ensureDir();
|
|
42
|
-
fs.writeFileSync(pidPath(name), String(pid));
|
|
43
|
-
} catch {}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Read PID from file, or null if missing/invalid. */
|
|
47
|
-
export function readPid(name) {
|
|
48
|
-
try {
|
|
49
|
-
const raw = fs.readFileSync(pidPath(name), "utf8").trim();
|
|
50
|
-
const pid = parseInt(raw, 10);
|
|
51
|
-
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
52
|
-
} catch {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Remove PID file. Silent fail. */
|
|
58
|
-
export function clearPid(name) {
|
|
59
|
-
try {
|
|
60
|
-
fs.unlinkSync(pidPath(name));
|
|
61
|
-
} catch {}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Check if a PID is still alive (does not kill). */
|
|
65
|
-
export function isAlive(pid) {
|
|
66
|
-
if (!pid) return false;
|
|
67
|
-
try {
|
|
68
|
-
// Signal 0 = existence check on all platforms
|
|
69
|
-
process.kill(pid, 0);
|
|
70
|
-
return true;
|
|
71
|
-
} catch {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Kill one tracked process by name. Uses SIGKILL on POSIX, `taskkill /F /PID`
|
|
78
|
-
* on Windows. Clears the PID file regardless of outcome.
|
|
79
|
-
* Returns true if a kill was attempted, false if no PID on record.
|
|
80
|
-
*/
|
|
81
|
-
export function killByName(name) {
|
|
82
|
-
const pid = readPid(name);
|
|
83
|
-
clearPid(name);
|
|
84
|
-
if (!pid) return false;
|
|
85
|
-
try {
|
|
86
|
-
if (process.platform === "win32") {
|
|
87
|
-
// taskkill /T also terminates the entire child tree (needed for systray
|
|
88
|
-
// helper which spawns its own child, and for detached background agent).
|
|
89
|
-
execSync(`taskkill /F /T /PID ${pid}`, { stdio: "ignore", windowsHide: true });
|
|
90
|
-
} else {
|
|
91
|
-
process.kill(pid, "SIGKILL");
|
|
92
|
-
}
|
|
93
|
-
} catch {}
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Names of every tracked PID. Shared with update shell scripts.
|
|
98
|
-
* NOTE: ptyDaemon is intentionally excluded — we never kill it to keep
|
|
99
|
-
* terminal sessions alive across agent restarts / updates. */
|
|
100
|
-
export const TRACKED_NAMES = ["cloudflared", "agent"];
|
|
101
|
-
|
|
102
|
-
/** Kill every tracked 9remote process. Safe to call repeatedly. */
|
|
103
|
-
export function killAll() {
|
|
104
|
-
// Cloudflared first so it can't reconnect mid-shutdown; agent's `/F /T`
|
|
105
|
-
// then sweeps server + tray (direct children).
|
|
106
|
-
for (const name of TRACKED_NAMES) {
|
|
107
|
-
killByName(name);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Absolute path to the PIDs directory (for shell scripts). */
|
|
112
|
-
export function getPidsDir() {
|
|
113
|
-
return PIDS_DIR;
|
|
114
|
-
}
|
package/cli/utils/state.js
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import os from "os";
|
|
4
|
-
|
|
5
|
-
const STATE_DIR = path.join(os.homedir(), ".9remote");
|
|
6
|
-
const STATE_FILE = path.join(STATE_DIR, "state.json");
|
|
7
|
-
const KEYS_FILE = path.join(STATE_DIR, "keys.json");
|
|
8
|
-
const CMD_FILE = path.join(STATE_DIR, "cmd.json");
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Ensure state directory exists
|
|
12
|
-
*/
|
|
13
|
-
function ensureDir() {
|
|
14
|
-
if (!fs.existsSync(STATE_DIR)) {
|
|
15
|
-
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// ==================== STATE ====================
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Load state from file
|
|
23
|
-
*/
|
|
24
|
-
export function loadState() {
|
|
25
|
-
try {
|
|
26
|
-
ensureDir();
|
|
27
|
-
if (!fs.existsSync(STATE_FILE)) return null;
|
|
28
|
-
return JSON.parse(fs.readFileSync(STATE_FILE, "utf8"));
|
|
29
|
-
} catch {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Save state to file
|
|
36
|
-
*/
|
|
37
|
-
export function saveState(state) {
|
|
38
|
-
try {
|
|
39
|
-
ensureDir();
|
|
40
|
-
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
41
|
-
} catch (error) {
|
|
42
|
-
console.error("Error saving state:", error);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Clear state file
|
|
48
|
-
*/
|
|
49
|
-
export function clearState() {
|
|
50
|
-
try {
|
|
51
|
-
if (fs.existsSync(STATE_FILE)) {
|
|
52
|
-
fs.unlinkSync(STATE_FILE);
|
|
53
|
-
}
|
|
54
|
-
} catch {}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ==================== KEYS ====================
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Load single key from file
|
|
61
|
-
* @returns {{ machineId: string, key: string | null, name: string, createdAt: string | null }}
|
|
62
|
-
*/
|
|
63
|
-
export function loadKey() {
|
|
64
|
-
try {
|
|
65
|
-
ensureDir();
|
|
66
|
-
if (!fs.existsSync(KEYS_FILE)) {
|
|
67
|
-
return { machineId: null, key: null, name: "Default", createdAt: null };
|
|
68
|
-
}
|
|
69
|
-
return JSON.parse(fs.readFileSync(KEYS_FILE, "utf8"));
|
|
70
|
-
} catch {
|
|
71
|
-
return { machineId: null, key: null, name: "Default", createdAt: null };
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ==================== IPC CMD ====================
|
|
76
|
-
|
|
77
|
-
/** Write a command for CLI to pick up */
|
|
78
|
-
export function writeCmd(cmd) {
|
|
79
|
-
try {
|
|
80
|
-
ensureDir();
|
|
81
|
-
fs.writeFileSync(CMD_FILE, JSON.stringify({ cmd, ts: Date.now() }));
|
|
82
|
-
} catch {}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Read and clear pending command (returns null if none) */
|
|
86
|
-
export function readAndClearCmd() {
|
|
87
|
-
try {
|
|
88
|
-
if (!fs.existsSync(CMD_FILE)) return null;
|
|
89
|
-
const data = JSON.parse(fs.readFileSync(CMD_FILE, "utf8"));
|
|
90
|
-
fs.unlinkSync(CMD_FILE);
|
|
91
|
-
return data.cmd;
|
|
92
|
-
} catch {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Save single key to file (overwrites existing)
|
|
99
|
-
*/
|
|
100
|
-
export function saveKey(machineId, key, name = "Default") {
|
|
101
|
-
try {
|
|
102
|
-
ensureDir();
|
|
103
|
-
const data = {
|
|
104
|
-
machineId,
|
|
105
|
-
key,
|
|
106
|
-
name,
|
|
107
|
-
createdAt: new Date().toISOString()
|
|
108
|
-
};
|
|
109
|
-
fs.writeFileSync(KEYS_FILE, JSON.stringify(data, null, 2));
|
|
110
|
-
return data;
|
|
111
|
-
} catch (error) {
|
|
112
|
-
console.error("Error saving key:", error);
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
}
|
package/cli/utils/token.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { browserFetch } from "../../lib/constants.js";
|
|
2
|
-
|
|
3
|
-
const TEMP_KEY_EXPIRY_MINUTES = 30;
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Create temp key on Worker for API key
|
|
7
|
-
* @param {string} apiKey - API key
|
|
8
|
-
* @param {string} workerUrl - Worker URL
|
|
9
|
-
* @returns {Promise<{tempKey: string, expiresAt: number} | null>}
|
|
10
|
-
*/
|
|
11
|
-
export async function createTempKey(apiKey, workerUrl) {
|
|
12
|
-
try {
|
|
13
|
-
const response = await browserFetch(`${workerUrl}/api/temp-key/create`, {
|
|
14
|
-
method: "POST",
|
|
15
|
-
headers: { "Content-Type": "application/json" },
|
|
16
|
-
body: JSON.stringify({
|
|
17
|
-
apiKey,
|
|
18
|
-
expiryMinutes: TEMP_KEY_EXPIRY_MINUTES
|
|
19
|
-
})
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
if (!response.ok) {
|
|
23
|
-
const error = await response.json();
|
|
24
|
-
throw new Error(error.error || "Failed to create temp key");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return await response.json();
|
|
28
|
-
} catch (error) {
|
|
29
|
-
console.error("Error creating temp key:", error);
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
}
|
package/cli/utils/tray.js
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* System tray module — optional, silent fail if not supported
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { spawn } from "child_process";
|
|
6
|
-
import fs from "fs";
|
|
7
|
-
import path from "path";
|
|
8
|
-
import { fileURLToPath } from "url";
|
|
9
|
-
|
|
10
|
-
let trayInstance = null;
|
|
11
|
-
let trayState = { port: 0, tunnelUrl: "", running: false };
|
|
12
|
-
|
|
13
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
-
|
|
15
|
-
// PNG 64x64 base64 for macOS/Linux (generated from agent/ui/public/favicon.svg)
|
|
16
|
-
const TRAY_ICON_PNG = "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";
|
|
17
|
-
|
|
18
|
-
// Windows requires ICO format; resolve ICO path across dev + bundled layouts
|
|
19
|
-
const ICO_CANDIDATES = [
|
|
20
|
-
path.join(__dirname, "assets", "trayIcon.ico"), // dev: agent/cli/utils/assets/
|
|
21
|
-
path.join(__dirname, "..", "assets", "trayIcon.ico"), // bundled: agent/dist/assets/
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
function getIconBase64() {
|
|
25
|
-
if (process.platform === "win32") {
|
|
26
|
-
for (const p of ICO_CANDIDATES) {
|
|
27
|
-
try {
|
|
28
|
-
if (fs.existsSync(p)) return fs.readFileSync(p).toString("base64");
|
|
29
|
-
} catch {}
|
|
30
|
-
}
|
|
31
|
-
return TRAY_ICON_PNG;
|
|
32
|
-
}
|
|
33
|
-
return TRAY_ICON_PNG;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function isTraySupported() {
|
|
37
|
-
const platform = process.platform;
|
|
38
|
-
if (!["darwin", "win32", "linux"].includes(platform)) return false;
|
|
39
|
-
if (platform === "linux" && !process.env.DISPLAY) return false;
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function buildTooltip() {
|
|
44
|
-
const { port, tunnelUrl, running } = trayState;
|
|
45
|
-
const status = running
|
|
46
|
-
? (tunnelUrl ? "Tunnel ON" : "Local only")
|
|
47
|
-
: "Idle";
|
|
48
|
-
const url = `http://localhost:${port}`;
|
|
49
|
-
// Windows tray tooltip limit is ~127 chars; keep it compact but informative
|
|
50
|
-
return `9Remote • ${status}\nLocal: ${url}\nRight-click to open / quit`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function buildMenu() {
|
|
54
|
-
const { port, tunnelUrl } = trayState;
|
|
55
|
-
const isWin = process.platform === "win32";
|
|
56
|
-
const statusLine = tunnelUrl
|
|
57
|
-
? `9Remote (Port ${port}) • Tunnel ON`
|
|
58
|
-
: `9Remote (Port ${port}) • Local only`;
|
|
59
|
-
return {
|
|
60
|
-
icon: getIconBase64(),
|
|
61
|
-
title: isWin ? `9Remote - Port ${port}` : "",
|
|
62
|
-
tooltip: buildTooltip(),
|
|
63
|
-
items: [
|
|
64
|
-
{ title: statusLine, tooltip: tunnelUrl || `http://localhost:${port}`, checked: false, enabled: false },
|
|
65
|
-
{ title: "Open Web UI", tooltip: `Open http://localhost:${port} in your browser`, checked: false, enabled: true },
|
|
66
|
-
{ title: "Shutdown", tooltip: "Stop 9Remote server, tunnel and quit", checked: false, enabled: true },
|
|
67
|
-
],
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Initialize system tray
|
|
73
|
-
* @param {{ port: number, onQuit: () => void, onOpenUI: () => void }} options
|
|
74
|
-
*/
|
|
75
|
-
export async function initTray({ port, onQuit, onOpenUI }) {
|
|
76
|
-
if (!isTraySupported()) return null;
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const mod = await import("systray");
|
|
80
|
-
const SysTray = mod.default?.default || mod.default;
|
|
81
|
-
|
|
82
|
-
trayState = { port, tunnelUrl: "", running: true };
|
|
83
|
-
|
|
84
|
-
trayInstance = new SysTray({ menu: buildMenu(), debug: false, copyDir: true });
|
|
85
|
-
// Tray helper is a direct child of agent; no separate PID file needed.
|
|
86
|
-
// `taskkill /F /T /PID <agent>` at update time terminates it along with
|
|
87
|
-
// the agent tree — safer than matching by image name.
|
|
88
|
-
|
|
89
|
-
trayInstance.onClick((action) => {
|
|
90
|
-
if (action.item.title === "Open Web UI") {
|
|
91
|
-
onOpenUI?.();
|
|
92
|
-
} else if (action.item.title === "Shutdown") {
|
|
93
|
-
onQuit?.();
|
|
94
|
-
killTray();
|
|
95
|
-
setTimeout(() => process.exit(0), 500);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
trayInstance.onReady(() => {});
|
|
100
|
-
trayInstance.onError(() => {});
|
|
101
|
-
|
|
102
|
-
return trayInstance;
|
|
103
|
-
} catch {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Update tray tooltip / status so the user knows current tunnel state.
|
|
110
|
-
* Safe to call before tray is ready — values are captured for next refresh.
|
|
111
|
-
*/
|
|
112
|
-
export function updateTrayTooltip({ tunnelUrl, running } = {}) {
|
|
113
|
-
if (tunnelUrl !== undefined) trayState.tunnelUrl = tunnelUrl;
|
|
114
|
-
if (running !== undefined) trayState.running = running;
|
|
115
|
-
if (!trayInstance) return;
|
|
116
|
-
try {
|
|
117
|
-
trayInstance.sendAction({
|
|
118
|
-
type: "update-menu",
|
|
119
|
-
menu: buildMenu(),
|
|
120
|
-
seq_id: -1,
|
|
121
|
-
});
|
|
122
|
-
} catch {}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function killTray() {
|
|
126
|
-
const instance = trayInstance;
|
|
127
|
-
trayInstance = null;
|
|
128
|
-
if (instance) {
|
|
129
|
-
try { instance.kill(true); } catch {}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Show a native OS notification (balloon tip on Windows, toast on macOS/Linux).
|
|
135
|
-
* Silent fail if the platform tool isn't available.
|
|
136
|
-
* @param {{ title?: string, message: string }} opts
|
|
137
|
-
*/
|
|
138
|
-
export function showTrayNotification({ title = "9Remote", message }) {
|
|
139
|
-
const platform = process.platform;
|
|
140
|
-
if (!message) return;
|
|
141
|
-
|
|
142
|
-
if (platform === "win32") {
|
|
143
|
-
// Prefer Windows 10/11 Toast Notification (WinRT) so the popup actually
|
|
144
|
-
// appears on modern Windows (ShowBalloonTip is effectively deprecated and
|
|
145
|
-
// only lands silently in Action Center on Win10+). Falls back to the
|
|
146
|
-
// classic NotifyIcon balloon tip if WinRT is unavailable (older Windows,
|
|
147
|
-
// Server Core, constrained PowerShell).
|
|
148
|
-
const esc = (s) =>
|
|
149
|
-
String(s)
|
|
150
|
-
.replace(/&/g, "&")
|
|
151
|
-
.replace(/</g, "<")
|
|
152
|
-
.replace(/>/g, ">")
|
|
153
|
-
.replace(/'/g, "'")
|
|
154
|
-
.replace(/"/g, """);
|
|
155
|
-
const escPs = (s) => String(s).replace(/'/g, "''");
|
|
156
|
-
const appId = "9Remote";
|
|
157
|
-
const toastXml =
|
|
158
|
-
`<toast><visual><binding template="ToastText02">` +
|
|
159
|
-
`<text id="1">${esc(title)}</text>` +
|
|
160
|
-
`<text id="2">${esc(message)}</text>` +
|
|
161
|
-
`</binding></visual></toast>`;
|
|
162
|
-
const ps = [
|
|
163
|
-
`try {`,
|
|
164
|
-
`[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime] > $null;`,
|
|
165
|
-
`[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType=WindowsRuntime] > $null;`,
|
|
166
|
-
`$xml = New-Object Windows.Data.Xml.Dom.XmlDocument;`,
|
|
167
|
-
`$xml.LoadXml('${escPs(toastXml)}');`,
|
|
168
|
-
`$toast = [Windows.UI.Notifications.ToastNotification]::new($xml);`,
|
|
169
|
-
`[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('${escPs(appId)}').Show($toast);`,
|
|
170
|
-
`} catch {`,
|
|
171
|
-
`Add-Type -AssemblyName System.Windows.Forms;`,
|
|
172
|
-
`$n = New-Object System.Windows.Forms.NotifyIcon;`,
|
|
173
|
-
`$n.Icon = [System.Drawing.SystemIcons]::Information;`,
|
|
174
|
-
`$n.BalloonTipTitle = '${escPs(title)}';`,
|
|
175
|
-
`$n.BalloonTipText = '${escPs(message)}';`,
|
|
176
|
-
`$n.Visible = $true;`,
|
|
177
|
-
`$n.ShowBalloonTip(5000);`,
|
|
178
|
-
`Start-Sleep -Seconds 6;`,
|
|
179
|
-
`$n.Dispose();`,
|
|
180
|
-
`}`,
|
|
181
|
-
].join(" ");
|
|
182
|
-
try {
|
|
183
|
-
const child = spawn(
|
|
184
|
-
"powershell.exe",
|
|
185
|
-
["-NoProfile", "-WindowStyle", "Hidden", "-Command", ps],
|
|
186
|
-
{ detached: true, stdio: "ignore", windowsHide: true }
|
|
187
|
-
);
|
|
188
|
-
child.unref();
|
|
189
|
-
} catch {}
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (platform === "darwin") {
|
|
194
|
-
try {
|
|
195
|
-
const esc = (s) => String(s).replace(/"/g, '\\"');
|
|
196
|
-
const script = `display notification "${esc(message)}" with title "${esc(title)}"`;
|
|
197
|
-
const child = spawn("osascript", ["-e", script], { detached: true, stdio: "ignore" });
|
|
198
|
-
child.unref();
|
|
199
|
-
} catch {}
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Linux / other POSIX
|
|
204
|
-
try {
|
|
205
|
-
const child = spawn("notify-send", [title, message], { detached: true, stdio: "ignore" });
|
|
206
|
-
child.unref();
|
|
207
|
-
} catch {}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export function openBrowser(url) {
|
|
211
|
-
const platform = process.platform;
|
|
212
|
-
|
|
213
|
-
// Windows: prefer `rundll32.exe` (GUI subsystem — no cmd window flash) over
|
|
214
|
-
// `cmd /c start` which briefly flashes a black console window even with
|
|
215
|
-
// `windowsHide: true`. Fallback to cmd/start if rundll32 ever fails.
|
|
216
|
-
if (platform === "win32") {
|
|
217
|
-
try {
|
|
218
|
-
const child = spawn("rundll32.exe", ["url.dll,FileProtocolHandler", url], {
|
|
219
|
-
detached: true,
|
|
220
|
-
stdio: "ignore",
|
|
221
|
-
windowsHide: true,
|
|
222
|
-
});
|
|
223
|
-
child.unref();
|
|
224
|
-
return;
|
|
225
|
-
} catch {}
|
|
226
|
-
// Fallback: cmd /c start "" "url"
|
|
227
|
-
try {
|
|
228
|
-
const child = spawn("cmd.exe", ["/d", "/s", "/c", "start", "", url], {
|
|
229
|
-
detached: true,
|
|
230
|
-
stdio: "ignore",
|
|
231
|
-
windowsHide: true,
|
|
232
|
-
});
|
|
233
|
-
child.unref();
|
|
234
|
-
} catch {}
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (platform === "darwin") {
|
|
239
|
-
try {
|
|
240
|
-
const child = spawn("open", [url], { detached: true, stdio: "ignore" });
|
|
241
|
-
child.unref();
|
|
242
|
-
} catch {}
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Linux / other POSIX
|
|
247
|
-
try {
|
|
248
|
-
const child = spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
|
|
249
|
-
child.unref();
|
|
250
|
-
} catch {}
|
|
251
|
-
}
|