9remote 0.1.13 → 0.1.15

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/index.js CHANGED
@@ -13,8 +13,7 @@ import { generateApiKeyWithMachine } from "./utils/apiKey.js";
13
13
  import { loadKey, saveKey, saveState, clearState } from "./utils/state.js";
14
14
  import { createTempKey } from "./utils/token.js";
15
15
  import { checkForUpdates } from "./utils/updateChecker.js";
16
- import { ensureNativeDeps } from "./utils/installer.js";
17
- import { ensureCloudflared, spawnCloudflared, killCloudflared } from "./utils/cloudflared.js";
16
+ import { ensureCloudflared, spawnCloudflared, killCloudflared, resetRestartCounter } from "./utils/cloudflared.js";
18
17
 
19
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
19
  const PROJECT_ROOT = path.resolve(__dirname, "..");
@@ -174,6 +173,7 @@ function setupExitHandler(serverManager, tunnelProcess, apiKey) {
174
173
  console.log(chalk.yellow("\n\nšŸ›‘ Stopping server..."));
175
174
  serverManager.shutdown();
176
175
  tunnelProcess.kill();
176
+ resetRestartCounter();
177
177
 
178
178
  // Cleanup tunnel on worker
179
179
  try {
@@ -214,14 +214,9 @@ async function createNamedTunnel(apiKey) {
214
214
  async function startServerAndTunnel(selectedKey) {
215
215
  console.log(chalk.cyan("\nšŸš€ Starting server..."));
216
216
 
217
- // Kill existing processes
217
+ // Kill existing cloudflared process
218
218
  try {
219
219
  killCloudflared();
220
- if (process.platform === "win32") {
221
- execSync("taskkill /F /IM node.exe /FI \"WINDOWTITLE eq server.js*\" 2>nul || exit 0", { stdio: "ignore" });
222
- } else {
223
- execSync("pkill -f 'node server.js' 2>/dev/null || true", { stdio: "ignore" });
224
- }
225
220
  } catch { }
226
221
 
227
222
  // Create session first
@@ -266,13 +261,22 @@ async function startServerAndTunnel(selectedKey) {
266
261
 
267
262
  const { token, hostname: tunnelUrl } = tunnelData;
268
263
 
269
- // Spawn cloudflared with token
264
+ // Spawn cloudflared with token and auto-restart callback
270
265
  console.log(chalk.cyan("āœ… Starting tunnel..."));
271
266
  let tunnelProcess;
272
- try {
273
- tunnelProcess = await spawnCloudflared(token);
274
- } catch (error) {
275
- console.log(chalk.red(`āŒ Failed to start cloudflared: ${error.message}`));
267
+
268
+ const startTunnel = async (tunnelToken) => {
269
+ try {
270
+ tunnelProcess = await spawnCloudflared(tunnelToken, startTunnel);
271
+ return tunnelProcess;
272
+ } catch (error) {
273
+ console.log(chalk.red(`āŒ Failed to start cloudflared: ${error.message}`));
274
+ return null;
275
+ }
276
+ };
277
+
278
+ tunnelProcess = await startTunnel(token);
279
+ if (!tunnelProcess) {
276
280
  serverManager.shutdown();
277
281
  return null;
278
282
  }
@@ -522,16 +526,6 @@ async function autoStartDev() {
522
526
  async function start() {
523
527
  checkForUpdates();
524
528
 
525
- // Ensure native dependencies are installed (first time only)
526
- const depsReady = await ensureNativeDeps();
527
- if (!depsReady) {
528
- console.log(chalk.red("āŒ Failed to install dependencies. Please try again."));
529
- process.exit(1);
530
- }
531
-
532
- // Wait a bit for symlinks to be fully created (filesystem sync)
533
- await new Promise(r => setTimeout(r, 1000));
534
-
535
529
  if (process.argv.includes("--auto")) {
536
530
  await autoStartDev();
537
531
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "9remote",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "description": "Remote terminal access from anywhere",
6
6
  "main": "index.js",
@@ -11,6 +11,15 @@ const BIN_NAME = IS_WINDOWS ? `${BINARY_NAME}.exe` : BINARY_NAME;
11
11
  const BIN_PATH = path.join(BIN_DIR, BIN_NAME);
12
12
  const PID_FILE = path.join(os.homedir(), ".9remote", "cloudflared.pid");
13
13
 
14
+ // Track intentional shutdown to suppress exit logs
15
+ let isIntentionalShutdown = false;
16
+
17
+ // Auto-restart configuration
18
+ const MAX_RESTART_ATTEMPTS = 3;
19
+ const RESTART_WINDOW_MS = 60000; // 1 minute
20
+ let restartTimes = [];
21
+ let restartCallback = null;
22
+
14
23
  const GITHUB_BASE_URL = "https://github.com/cloudflare/cloudflared/releases/latest/download";
15
24
 
16
25
  /**
@@ -152,11 +161,17 @@ const LOG_IGNORE = [
152
161
  /**
153
162
  * Spawn cloudflared tunnel
154
163
  * @param {string} tunnelToken
164
+ * @param {Function} onRestart - Callback when tunnel needs restart
155
165
  * @returns {ChildProcess}
156
166
  */
157
- export async function spawnCloudflared(tunnelToken) {
167
+ export async function spawnCloudflared(tunnelToken, onRestart = null) {
158
168
  const binaryPath = await ensureCloudflared();
159
169
 
170
+ // Store restart callback
171
+ if (onRestart) {
172
+ restartCallback = onRestart;
173
+ }
174
+
160
175
  const child = spawn(binaryPath, ["tunnel", "run", "--token", tunnelToken], {
161
176
  detached: false,
162
177
  stdio: ["ignore", "pipe", "pipe"]
@@ -172,6 +187,11 @@ export async function spawnCloudflared(tunnelToken) {
172
187
  return;
173
188
  }
174
189
 
190
+ // Skip errors during intentional shutdown
191
+ if (isIntentionalShutdown) {
192
+ return;
193
+ }
194
+
175
195
  // Show connection status briefly
176
196
  if (msg.includes("Registered tunnel connection")) {
177
197
  connectionCount++;
@@ -198,8 +218,27 @@ export async function spawnCloudflared(tunnelToken) {
198
218
  });
199
219
 
200
220
  child.on("exit", (code) => {
201
- if (code !== 0 && code !== null) {
221
+ // Only log unexpected exits
222
+ if (!isIntentionalShutdown && code !== 0 && code !== null) {
202
223
  console.log(`cloudflared exited with code ${code}`);
224
+
225
+ // Auto-restart logic
226
+ if (restartCallback) {
227
+ const now = Date.now();
228
+ restartTimes.push(now);
229
+
230
+ // Remove old restart times outside window
231
+ restartTimes = restartTimes.filter(t => t > now - RESTART_WINDOW_MS);
232
+
233
+ if (restartTimes.length <= MAX_RESTART_ATTEMPTS) {
234
+ console.log(`šŸ”„ Restarting tunnel... (attempt ${restartTimes.length}/${MAX_RESTART_ATTEMPTS})`);
235
+ setTimeout(() => {
236
+ restartCallback(tunnelToken);
237
+ }, 2000);
238
+ } else {
239
+ console.log(`āŒ Too many tunnel restarts (${MAX_RESTART_ATTEMPTS} in ${RESTART_WINDOW_MS / 1000}s). Giving up.`);
240
+ }
241
+ }
203
242
  }
204
243
  });
205
244
 
@@ -215,12 +254,20 @@ export async function spawnCloudflared(tunnelToken) {
215
254
  export function killCloudflared() {
216
255
  try {
217
256
  if (fs.existsSync(PID_FILE)) {
257
+ isIntentionalShutdown = true;
218
258
  const pid = parseInt(fs.readFileSync(PID_FILE, "utf8"));
219
259
  process.kill(pid);
220
260
  fs.unlinkSync(PID_FILE);
221
- console.log("āœ… cloudflared stopped");
222
261
  }
223
262
  } catch (error) {
224
263
  // Silently ignore errors
225
264
  }
226
265
  }
266
+
267
+ /**
268
+ * Reset restart counter
269
+ */
270
+ export function resetRestartCounter() {
271
+ restartTimes = [];
272
+ restartCallback = null;
273
+ }
@@ -1,137 +0,0 @@
1
- import { execSync } from "child_process";
2
- import ora from "ora";
3
- import chalk from "chalk";
4
- import fs from "fs";
5
- import path from "path";
6
- import { fileURLToPath } from "url";
7
- import os from "os";
8
-
9
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
- const PROJECT_ROOT = path.resolve(__dirname, "../..");
11
-
12
- /**
13
- * Check if running from global installation
14
- */
15
- function isGlobalInstall() {
16
- // Check if we're in global node_modules
17
- return PROJECT_ROOT.includes("/node_modules/9remote") ||
18
- PROJECT_ROOT.includes("\\node_modules\\9remote");
19
- }
20
-
21
- /**
22
- * Get dependencies directory
23
- * For global install: use ~/.9remote/node_modules
24
- * For local dev: use project node_modules
25
- */
26
- function getDepsDir() {
27
- if (isGlobalInstall()) {
28
- const homeDir = os.homedir();
29
- return path.join(homeDir, ".9remote");
30
- }
31
- return PROJECT_ROOT;
32
- }
33
-
34
- const DEPS_DIR = getDepsDir();
35
- const INSTALL_MARKER = path.join(DEPS_DIR, "node_modules", ".deps-installed");
36
-
37
- /**
38
- * Ensure all dependencies are installed
39
- * Only runs once on first launch
40
- */
41
- export async function ensureNativeDeps() {
42
- // Create deps directory if needed
43
- if (!fs.existsSync(DEPS_DIR)) {
44
- fs.mkdirSync(DEPS_DIR, { recursive: true });
45
- }
46
-
47
- // Check if already installed
48
- if (fs.existsSync(INSTALL_MARKER)) {
49
- // Add deps to NODE_PATH for global install
50
- if (isGlobalInstall()) {
51
- const depsNodeModules = path.join(DEPS_DIR, "node_modules");
52
- if (process.env.NODE_PATH) {
53
- process.env.NODE_PATH = `${depsNodeModules}${path.delimiter}${process.env.NODE_PATH}`;
54
- } else {
55
- process.env.NODE_PATH = depsNodeModules;
56
- }
57
- require("module").Module._initPaths();
58
- }
59
- return true;
60
- }
61
-
62
- const spinner = ora("Checking dependencies...").start();
63
-
64
- try {
65
- // Check if cloudflared is installed
66
- let allInstalled = true;
67
- try {
68
- await import("cloudflared");
69
- } catch {
70
- allInstalled = false;
71
- }
72
-
73
- if (allInstalled) {
74
- spinner.succeed("Dependencies ready");
75
-
76
- // Mark as installed
77
- fs.mkdirSync(path.dirname(INSTALL_MARKER), { recursive: true });
78
- fs.writeFileSync(INSTALL_MARKER, new Date().toISOString());
79
-
80
- return true;
81
- }
82
-
83
- // Install cloudflared only (other deps are in package.json)
84
- spinner.stop();
85
- console.log(chalk.cyan("šŸ“¦ Installing cloudflared (first time only, ~10 seconds)...\n"));
86
-
87
- const packageNames = ["cloudflared"];
88
-
89
- try {
90
- console.log(chalk.gray(" Installing cloudflared..."));
91
-
92
- // Create package.json if in global install mode
93
- if (isGlobalInstall()) {
94
- const pkgJsonPath = path.join(DEPS_DIR, "package.json");
95
- if (!fs.existsSync(pkgJsonPath)) {
96
- fs.writeFileSync(pkgJsonPath, JSON.stringify({
97
- name: "9remote-deps",
98
- version: "1.0.0",
99
- private: true
100
- }, null, 2));
101
- }
102
- }
103
-
104
- execSync(`npm install --no-save --prefer-offline ${packageNames.join(" ")}`, {
105
- cwd: DEPS_DIR,
106
- stdio: "inherit"
107
- });
108
- console.log(chalk.green("\nāœ” Cloudflared installed successfully"));
109
-
110
- // Add deps to NODE_PATH for global install
111
- if (isGlobalInstall()) {
112
- const depsNodeModules = path.join(DEPS_DIR, "node_modules");
113
- if (process.env.NODE_PATH) {
114
- process.env.NODE_PATH = `${depsNodeModules}${path.delimiter}${process.env.NODE_PATH}`;
115
- } else {
116
- process.env.NODE_PATH = depsNodeModules;
117
- }
118
- require("module").Module._initPaths();
119
- }
120
- } catch (error) {
121
- console.log(chalk.red("\nāœ– Failed to install dependencies"));
122
- console.error(error.message);
123
- console.log(chalk.yellow("\nšŸ’” Tip: Try running with sudo if permission denied"));
124
- return false;
125
- }
126
-
127
- // Mark as installed
128
- fs.mkdirSync(path.dirname(INSTALL_MARKER), { recursive: true });
129
- fs.writeFileSync(INSTALL_MARKER, new Date().toISOString());
130
-
131
- return true;
132
- } catch (error) {
133
- spinner.fail("Failed to install dependencies");
134
- console.error(error.message);
135
- return false;
136
- }
137
- }