traforo 0.2.3 → 0.2.5
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/lockfile.d.ts +3 -0
- package/dist/lockfile.js +12 -5
- package/dist/run-tunnel.js +28 -8
- package/package.json +1 -1
package/dist/lockfile.d.ts
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* Stores one JSON file per active tunnel port in ~/.traforo/{port}.json.
|
|
5
5
|
* Used to detect port conflicts, show tunnel info in error messages,
|
|
6
6
|
* and let agents reuse existing tunnels instead of killing them.
|
|
7
|
+
*
|
|
8
|
+
* Override the lockfile directory with TRAFORO_HOME env var (useful for tests).
|
|
7
9
|
*/
|
|
10
|
+
export declare function getLockfileDir(): string;
|
|
8
11
|
export type LockfileData = {
|
|
9
12
|
tunnelId: string;
|
|
10
13
|
tunnelUrl: string;
|
package/dist/lockfile.js
CHANGED
|
@@ -4,17 +4,22 @@
|
|
|
4
4
|
* Stores one JSON file per active tunnel port in ~/.traforo/{port}.json.
|
|
5
5
|
* Used to detect port conflicts, show tunnel info in error messages,
|
|
6
6
|
* and let agents reuse existing tunnels instead of killing them.
|
|
7
|
+
*
|
|
8
|
+
* Override the lockfile directory with TRAFORO_HOME env var (useful for tests).
|
|
7
9
|
*/
|
|
8
10
|
import fs from 'node:fs';
|
|
9
11
|
import path from 'node:path';
|
|
10
12
|
import os from 'node:os';
|
|
11
|
-
|
|
13
|
+
export function getLockfileDir() {
|
|
14
|
+
return process.env.TRAFORO_HOME ?? path.join(os.homedir(), '.traforo');
|
|
15
|
+
}
|
|
12
16
|
function lockfilePath(port) {
|
|
13
|
-
return path.join(
|
|
17
|
+
return path.join(getLockfileDir(), `${port}.json`);
|
|
14
18
|
}
|
|
15
19
|
export function writeLockfile(port, data) {
|
|
16
20
|
try {
|
|
17
|
-
|
|
21
|
+
const dir = getLockfileDir();
|
|
22
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
18
23
|
fs.writeFileSync(lockfilePath(port), JSON.stringify(data, null, 2) + '\n');
|
|
19
24
|
}
|
|
20
25
|
catch {
|
|
@@ -60,7 +65,9 @@ export function isLockfileStale(lock) {
|
|
|
60
65
|
process.kill(lock.tunnelPid, 0);
|
|
61
66
|
return false;
|
|
62
67
|
}
|
|
63
|
-
catch {
|
|
64
|
-
|
|
68
|
+
catch (error) {
|
|
69
|
+
// ESRCH = no such process (dead). EPERM = process exists but we
|
|
70
|
+
// can't signal it (e.g. PID 1) — treat as alive, not stale.
|
|
71
|
+
return error.code === 'ESRCH';
|
|
65
72
|
}
|
|
66
73
|
}
|
package/dist/run-tunnel.js
CHANGED
|
@@ -6,6 +6,23 @@ import { TunnelClient } from './client.js';
|
|
|
6
6
|
import { writeLockfile, readLockfile, removeLockfile, isLockfileStale, } from './lockfile.js';
|
|
7
7
|
const execPromise = promisify(exec);
|
|
8
8
|
export const CLI_NAME = 'traforo';
|
|
9
|
+
/**
|
|
10
|
+
* Shell-quote an argument array so the suggested command is copy-pasteable.
|
|
11
|
+
* Wraps args in single quotes if they contain shell-special characters.
|
|
12
|
+
*/
|
|
13
|
+
function shellQuote(args) {
|
|
14
|
+
return args
|
|
15
|
+
.map((arg) => {
|
|
16
|
+
if (arg === '')
|
|
17
|
+
return "''";
|
|
18
|
+
// Safe chars that don't need quoting
|
|
19
|
+
if (/^[a-zA-Z0-9._\-/:=@]+$/.test(arg))
|
|
20
|
+
return arg;
|
|
21
|
+
// Wrap in single quotes, escaping any inner single quotes
|
|
22
|
+
return "'" + arg.replace(/'/g, "'\\''") + "'";
|
|
23
|
+
})
|
|
24
|
+
.join(' ');
|
|
25
|
+
}
|
|
9
26
|
const DEFAULT_TUNNEL_ID_BYTES = 10;
|
|
10
27
|
export function createRandomTunnelId({ port }) {
|
|
11
28
|
return `${crypto.randomBytes(DEFAULT_TUNNEL_ID_BYTES).toString('hex')}-${port}`;
|
|
@@ -168,13 +185,13 @@ export async function runTunnel(options) {
|
|
|
168
185
|
// Kill existing process on port if requested
|
|
169
186
|
if (options.kill) {
|
|
170
187
|
await killProcessOnPort(port);
|
|
171
|
-
|
|
172
|
-
// Verify the port actually freed up
|
|
188
|
+
// Verify the port actually freed up before removing the lockfile
|
|
173
189
|
if (await isPortInUse(port, localHost)) {
|
|
174
190
|
console.error(`Error: Port ${port} is still in use after --kill.`);
|
|
175
191
|
console.error(`The process may require elevated permissions to terminate.`);
|
|
176
192
|
process.exit(1);
|
|
177
193
|
}
|
|
194
|
+
removeLockfile(port); // no ownership check — --kill is intentional
|
|
178
195
|
}
|
|
179
196
|
// Pre-flight: detect port conflict before spawning the child process
|
|
180
197
|
if (options.command && options.command.length > 0 && !options.kill) {
|
|
@@ -184,6 +201,7 @@ export async function runTunnel(options) {
|
|
|
184
201
|
if (lock && !isLockfileStale(lock)) {
|
|
185
202
|
const currentCwd = process.cwd();
|
|
186
203
|
const currentCmd = options.command;
|
|
204
|
+
const restartCommand = `${CLI_NAME} -p ${port} -t ${lock.tunnelId} --kill -- ${shellQuote(currentCmd)}`;
|
|
187
205
|
const sameCwd = lock.cwd === currentCwd;
|
|
188
206
|
const sameCmd = lock.command &&
|
|
189
207
|
lock.command.length === currentCmd.length &&
|
|
@@ -193,12 +211,14 @@ export async function runTunnel(options) {
|
|
|
193
211
|
console.error(`Error: Port ${port} is already in use\n`);
|
|
194
212
|
console.error(` Tunnel: ${lock.tunnelUrl}`);
|
|
195
213
|
console.error(` ID: ${lock.tunnelId}`);
|
|
196
|
-
console.error(` Command: ${lock.command
|
|
214
|
+
console.error(` Command: ${lock.command ? shellQuote(lock.command) : 'unknown'}`);
|
|
197
215
|
console.error(` Dir: ${lock.cwd}`);
|
|
198
216
|
console.error(` PID: ${lock.tunnelPid}`);
|
|
199
217
|
console.error(` Started: ${lock.startedAt}\n`);
|
|
200
218
|
console.error(`The same command in the same directory is already tunneled.`);
|
|
201
219
|
console.error(`Reuse the tunnel URL above instead of creating a new one.`);
|
|
220
|
+
console.error(`If you want to restart it without changing the tunnel URL for existing consumers, run:`);
|
|
221
|
+
console.error(` ${restartCommand}`);
|
|
202
222
|
process.exit(1);
|
|
203
223
|
}
|
|
204
224
|
else {
|
|
@@ -206,13 +226,13 @@ export async function runTunnel(options) {
|
|
|
206
226
|
console.error(`Error: Port ${port} is already in use\n`);
|
|
207
227
|
console.error(` Tunnel: ${lock.tunnelUrl}`);
|
|
208
228
|
console.error(` ID: ${lock.tunnelId}`);
|
|
209
|
-
console.error(` Command: ${lock.command
|
|
229
|
+
console.error(` Command: ${lock.command ? shellQuote(lock.command) : 'unknown'}`);
|
|
210
230
|
console.error(` Dir: ${lock.cwd}`);
|
|
211
231
|
console.error(` PID: ${lock.tunnelPid}`);
|
|
212
232
|
console.error(` Started: ${lock.startedAt}\n`);
|
|
213
|
-
console.error(`Use --kill to terminate the existing process and start fresh,`);
|
|
233
|
+
console.error(`Use --kill to terminate the existing process and start fresh while keeping the same tunnel URL,`);
|
|
214
234
|
console.error(`or just reuse the tunnel URL above instead:\n`);
|
|
215
|
-
console.error(`
|
|
235
|
+
console.error(` ${restartCommand}`);
|
|
216
236
|
process.exit(1);
|
|
217
237
|
}
|
|
218
238
|
}
|
|
@@ -222,7 +242,7 @@ export async function runTunnel(options) {
|
|
|
222
242
|
removeLockfile(port);
|
|
223
243
|
console.error(`Error: Port ${port} is already in use by another process.\n`);
|
|
224
244
|
console.error(`Use --kill to terminate it before starting:`);
|
|
225
|
-
console.error(` traforo -p ${port} --kill -- ${options.command
|
|
245
|
+
console.error(` traforo -p ${port} --kill -- ${shellQuote(options.command)}`);
|
|
226
246
|
process.exit(1);
|
|
227
247
|
}
|
|
228
248
|
}
|
|
@@ -232,7 +252,7 @@ export async function runTunnel(options) {
|
|
|
232
252
|
if (options.command && options.command.length > 0) {
|
|
233
253
|
const cmd = options.command[0];
|
|
234
254
|
const args = options.command.slice(1);
|
|
235
|
-
console.log(`Starting: ${options.command
|
|
255
|
+
console.log(`Starting: ${shellQuote(options.command)}`);
|
|
236
256
|
console.log(`PORT=${port}\n`);
|
|
237
257
|
const spawnedChild = spawn(cmd, args, {
|
|
238
258
|
stdio: 'inherit',
|