9remote 2.0.1 → 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/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
- }
@@ -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
- }
@@ -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, "&amp;")
151
- .replace(/</g, "&lt;")
152
- .replace(/>/g, "&gt;")
153
- .replace(/'/g, "&apos;")
154
- .replace(/"/g, "&quot;");
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
- }