nstantpage-agent 0.3.2 → 0.3.4

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.
@@ -19,6 +19,7 @@ import chalk from 'chalk';
19
19
  import path from 'path';
20
20
  import fs from 'fs';
21
21
  import os from 'os';
22
+ import { execSync } from 'child_process';
22
23
  import { getConfig } from '../config.js';
23
24
  import { TunnelClient } from '../tunnel.js';
24
25
  import { LocalServer } from '../localServer.js';
@@ -97,6 +98,54 @@ async function fetchProjectFiles(backendUrl, projectId, projectDir, token) {
97
98
  console.log(chalk.green(` ✓ ${written} files downloaded (version ${data.version})`));
98
99
  return { fileCount: written, isNew: true };
99
100
  }
101
+ /**
102
+ * Kill any process listening on the given port.
103
+ */
104
+ function killPort(port) {
105
+ try {
106
+ // Works on macOS and Linux
107
+ const pids = execSync(`lsof -ti :${port} 2>/dev/null`, { encoding: 'utf-8' }).trim();
108
+ if (pids) {
109
+ for (const pid of pids.split('\n')) {
110
+ const p = parseInt(pid.trim(), 10);
111
+ if (p && p !== process.pid) {
112
+ try {
113
+ process.kill(p, 'SIGTERM');
114
+ }
115
+ catch { }
116
+ }
117
+ }
118
+ }
119
+ }
120
+ catch {
121
+ // lsof not available or no process found — that's fine
122
+ try {
123
+ // Fallback for systems without lsof
124
+ execSync(`fuser -k ${port}/tcp 2>/dev/null`, { encoding: 'utf-8' });
125
+ }
126
+ catch { }
127
+ }
128
+ }
129
+ /**
130
+ * Clean up any previous agent instance (kill old PID + free ports).
131
+ */
132
+ function cleanupPreviousAgent(conf, apiPort, devPort) {
133
+ // 1. Try to kill the previously stored agent PID
134
+ const oldPid = conf.get('agentPid');
135
+ if (oldPid && oldPid !== process.pid) {
136
+ try {
137
+ process.kill(oldPid, 'SIGTERM');
138
+ console.log(chalk.gray(` Stopped previous agent (PID ${oldPid})`));
139
+ }
140
+ catch {
141
+ // Already dead
142
+ }
143
+ conf.delete('agentPid');
144
+ }
145
+ // 2. Free the ports in case orphaned processes are still holding them
146
+ killPort(apiPort);
147
+ killPort(devPort);
148
+ }
100
149
  export async function startCommand(directory, options) {
101
150
  const conf = getConfig();
102
151
  // If --token was passed, persist it
@@ -134,6 +183,10 @@ export async function startCommand(directory, options) {
134
183
  // Save project ID
135
184
  conf.set('projectId', projectId);
136
185
  const backendUrl = resolveBackendUrl(options);
186
+ // Kill any leftover agent / free ports from a previous run
187
+ cleanupPreviousAgent(conf, apiPort, devPort);
188
+ // Small delay to let ports release
189
+ await new Promise(r => setTimeout(r, 300));
137
190
  console.log(chalk.blue(`\n🚀 nstantpage agent v${VERSION}\n`));
138
191
  console.log(chalk.gray(` Project ID: ${projectId}`));
139
192
  console.log(chalk.gray(` Directory: ${projectDir}`));
package/dist/tunnel.d.ts CHANGED
@@ -45,6 +45,8 @@ export declare class TunnelClient {
45
45
  /**
46
46
  * Start reconnecting in the background (non-blocking).
47
47
  * Used when initial connect() fails so the dev server can keep running.
48
+ * The close handler from the failed connect() already schedules a reconnect,
49
+ * so we just ensure shouldReconnect is true and don't duplicate.
48
50
  */
49
51
  startBackgroundReconnect(): void;
50
52
  disconnect(): void;
package/dist/tunnel.js CHANGED
@@ -106,11 +106,17 @@ export class TunnelClient {
106
106
  /**
107
107
  * Start reconnecting in the background (non-blocking).
108
108
  * Used when initial connect() fails so the dev server can keep running.
109
+ * The close handler from the failed connect() already schedules a reconnect,
110
+ * so we just ensure shouldReconnect is true and don't duplicate.
109
111
  */
110
112
  startBackgroundReconnect() {
111
113
  this.shouldReconnect = true;
112
- this.reconnectAttempts = 0;
113
- this.scheduleReconnect();
114
+ // Only kick off a reconnect if there isn't one already pending
115
+ // (the close handler from the failed connect() already scheduled one)
116
+ if (!this.reconnectTimer) {
117
+ this.reconnectAttempts = 0;
118
+ this.scheduleReconnect();
119
+ }
114
120
  }
115
121
  disconnect() {
116
122
  this.shouldReconnect = false;
@@ -235,6 +241,8 @@ export class TunnelClient {
235
241
  scheduleReconnect() {
236
242
  if (!this.shouldReconnect)
237
243
  return;
244
+ if (this.reconnectTimer)
245
+ return; // Already have a pending reconnect
238
246
  if (this.reconnectAttempts >= this.maxReconnectAttempts) {
239
247
  console.error(' [Tunnel] Max reconnect attempts reached. Giving up.');
240
248
  return;
@@ -243,6 +251,7 @@ export class TunnelClient {
243
251
  this.reconnectAttempts++;
244
252
  console.log(` [Tunnel] Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts})...`);
245
253
  this.reconnectTimer = setTimeout(async () => {
254
+ this.reconnectTimer = null; // Clear so next scheduleReconnect can fire
246
255
  try {
247
256
  await this.connect();
248
257
  console.log(' [Tunnel] Reconnected!');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nstantpage-agent",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Local development agent for nstantpage.com — run your projects locally, preview in the cloud. Replaces cloud containers for faster builds.",
5
5
  "type": "module",
6
6
  "bin": {