traforo 0.2.3 → 0.2.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.
@@ -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
- const LOCKFILE_DIR = path.join(os.homedir(), '.traforo');
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(LOCKFILE_DIR, `${port}.json`);
17
+ return path.join(getLockfileDir(), `${port}.json`);
14
18
  }
15
19
  export function writeLockfile(port, data) {
16
20
  try {
17
- fs.mkdirSync(LOCKFILE_DIR, { recursive: true });
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
- return true;
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
  }
@@ -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
- removeLockfile(port); // no ownership check --kill is intentional
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) {
@@ -193,7 +210,7 @@ export async function runTunnel(options) {
193
210
  console.error(`Error: Port ${port} is already in use\n`);
194
211
  console.error(` Tunnel: ${lock.tunnelUrl}`);
195
212
  console.error(` ID: ${lock.tunnelId}`);
196
- console.error(` Command: ${lock.command?.join(' ') ?? 'unknown'}`);
213
+ console.error(` Command: ${lock.command ? shellQuote(lock.command) : 'unknown'}`);
197
214
  console.error(` Dir: ${lock.cwd}`);
198
215
  console.error(` PID: ${lock.tunnelPid}`);
199
216
  console.error(` Started: ${lock.startedAt}\n`);
@@ -206,13 +223,13 @@ export async function runTunnel(options) {
206
223
  console.error(`Error: Port ${port} is already in use\n`);
207
224
  console.error(` Tunnel: ${lock.tunnelUrl}`);
208
225
  console.error(` ID: ${lock.tunnelId}`);
209
- console.error(` Command: ${lock.command?.join(' ') ?? 'unknown'}`);
226
+ console.error(` Command: ${lock.command ? shellQuote(lock.command) : 'unknown'}`);
210
227
  console.error(` Dir: ${lock.cwd}`);
211
228
  console.error(` PID: ${lock.tunnelPid}`);
212
229
  console.error(` Started: ${lock.startedAt}\n`);
213
230
  console.error(`Use --kill to terminate the existing process and start fresh,`);
214
231
  console.error(`or just reuse the tunnel URL above instead:\n`);
215
- console.error(` traforo -p ${port} --kill -- ${options.command.join(' ')}`);
232
+ console.error(` traforo -p ${port} --kill -- ${shellQuote(options.command)}`);
216
233
  process.exit(1);
217
234
  }
218
235
  }
@@ -222,7 +239,7 @@ export async function runTunnel(options) {
222
239
  removeLockfile(port);
223
240
  console.error(`Error: Port ${port} is already in use by another process.\n`);
224
241
  console.error(`Use --kill to terminate it before starting:`);
225
- console.error(` traforo -p ${port} --kill -- ${options.command.join(' ')}`);
242
+ console.error(` traforo -p ${port} --kill -- ${shellQuote(options.command)}`);
226
243
  process.exit(1);
227
244
  }
228
245
  }
@@ -232,7 +249,7 @@ export async function runTunnel(options) {
232
249
  if (options.command && options.command.length > 0) {
233
250
  const cmd = options.command[0];
234
251
  const args = options.command.slice(1);
235
- console.log(`Starting: ${options.command.join(' ')}`);
252
+ console.log(`Starting: ${shellQuote(options.command)}`);
236
253
  console.log(`PORT=${port}\n`);
237
254
  const spawnedChild = spawn(cmd, args, {
238
255
  stdio: 'inherit',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "traforo",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "HTTP tunnel via Cloudflare Durable Objects and WebSockets. Edge caching and password protection.",
5
5
  "type": "module",
6
6
  "license": "MIT",