harnery 0.42.0 → 0.43.0
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/commander.d.ts +16 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +18 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +54 -4
- package/dist/commands/tunnel.d.ts +65 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +290 -101
- package/dist/commands/web-fetch.d.ts +3 -2
- package/dist/commands/web-fetch.d.ts.map +1 -1
- package/dist/commands/web-fetch.js +25 -19
- package/dist/commands/web.d.ts +7 -1
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +9 -3
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +11 -5
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +57 -13
- package/dist/core/agents/state/names.d.ts +43 -3
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +87 -3
- package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
- package/dist/core/hooks/cli.js +87 -18
- package/dist/core/hooks/fork-lineage.d.ts +40 -0
- package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
- package/dist/core/hooks/fork-lineage.js +62 -0
- package/dist/core/hooks/resolve/transcript.d.ts +16 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +84 -0
- package/dist/core/hooks/shell-rm.d.ts +2 -0
- package/dist/core/hooks/shell-rm.d.ts.map +1 -0
- package/dist/core/hooks/shell-rm.js +105 -0
- package/dist/core/hooks/shell-tokens.d.ts +12 -0
- package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
- package/dist/core/hooks/shell-tokens.js +97 -0
- package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
- package/dist/core/hooks/shell-waiter.js +3 -93
- package/dist/core/semantic/contract.d.ts +5 -5
- package/dist/core/storage/process-log.d.ts +12 -0
- package/dist/core/storage/process-log.d.ts.map +1 -1
- package/dist/core/storage/process-log.js +10 -2
- package/dist/lib/tunnel/allowlist.d.ts +44 -0
- package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
- package/dist/lib/tunnel/allowlist.js +204 -0
- package/dist/lib/tunnel/current-address.d.ts +41 -0
- package/dist/lib/tunnel/current-address.d.ts.map +1 -0
- package/dist/lib/tunnel/current-address.js +123 -0
- package/dist/lib/tunnel/gate.js +13 -6
- package/dist/lib/tunnel/processes.d.ts +39 -0
- package/dist/lib/tunnel/processes.d.ts.map +1 -0
- package/dist/lib/tunnel/processes.js +121 -0
- package/dist/lib/tunnel/state.d.ts +28 -3
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +98 -27
- package/package.json +2 -1
- package/src/commander.ts +32 -0
- package/src/commands/agents.ts +97 -11
- package/src/commands/tunnel.ts +394 -116
- package/src/commands/web-fetch.ts +30 -19
- package/src/commands/web.ts +9 -3
- package/src/core/agents/render/prompt-context.ts +25 -8
- package/src/core/agents/render/session-context.ts +83 -16
- package/src/core/agents/state/names.ts +117 -6
- package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
- package/src/core/hooks/cli.ts +106 -17
- package/src/core/hooks/fork-lineage.ts +82 -0
- package/src/core/hooks/resolve/transcript.ts +88 -0
- package/src/core/hooks/shell-rm.ts +103 -0
- package/src/core/hooks/shell-tokens.ts +96 -0
- package/src/core/hooks/shell-waiter.ts +4 -91
- package/src/core/storage/process-log.ts +17 -2
- package/src/lib/tunnel/allowlist.ts +208 -0
- package/src/lib/tunnel/current-address.ts +158 -0
- package/src/lib/tunnel/gate.ts +16 -8
- package/src/lib/tunnel/processes.ts +149 -0
- package/src/lib/tunnel/state.ts +123 -27
package/src/commands/tunnel.ts
CHANGED
|
@@ -14,10 +14,23 @@ import {
|
|
|
14
14
|
} from "../core/servers/index.ts";
|
|
15
15
|
import {
|
|
16
16
|
processLogDestination,
|
|
17
|
+
runProcessLogWorker,
|
|
17
18
|
runRotatingProcessSync,
|
|
18
19
|
spawnRotatingProcess,
|
|
19
20
|
} from "../core/storage/process-log.ts";
|
|
21
|
+
import { normalizeAllowEntry } from "../lib/tunnel/allowlist.ts";
|
|
22
|
+
import {
|
|
23
|
+
type DetectOptions,
|
|
24
|
+
detectPublicAddresses,
|
|
25
|
+
planCurrentAddresses,
|
|
26
|
+
} from "../lib/tunnel/current-address.ts";
|
|
20
27
|
import { ALLOW_PATHS_ENV, normalizeAllowPaths } from "../lib/tunnel/path-scope.ts";
|
|
28
|
+
import {
|
|
29
|
+
connectProbe,
|
|
30
|
+
findPidsByCommandLine,
|
|
31
|
+
listeningPorts,
|
|
32
|
+
portListening,
|
|
33
|
+
} from "../lib/tunnel/processes.ts";
|
|
21
34
|
import {
|
|
22
35
|
clearState,
|
|
23
36
|
DEFAULT_INSTANCE,
|
|
@@ -46,9 +59,11 @@ import {
|
|
|
46
59
|
* Every provider also passes through a path scope: `up` requires at least one
|
|
47
60
|
* `--allow-path <prefix>`, and the gate refuses any request outside it.
|
|
48
61
|
*
|
|
49
|
-
* State + config persisted under `.cache/tunnel
|
|
50
|
-
*
|
|
51
|
-
*
|
|
62
|
+
* State + config persisted under `.cache/tunnel/` (or `$HARNERY_TUNNEL_DIR`).
|
|
63
|
+
* cloudflared comes from `$HARNERY_CLOUDFLARED` / the config's `cloudflared_path`
|
|
64
|
+
* when set, else PATH, else auto-installs to ~/.local/bin/ on Linux (macOS and
|
|
65
|
+
* Windows hosts install it themselves); Tailscale requires an installed and
|
|
66
|
+
* authenticated `tailscale` CLI.
|
|
52
67
|
*/
|
|
53
68
|
|
|
54
69
|
const DEFAULT_TARGET = "127.0.0.1:8001";
|
|
@@ -220,19 +235,100 @@ function gateScriptPath(): string {
|
|
|
220
235
|
return resolve(import.meta.dirname, "..", "lib", "tunnel", "gate.ts");
|
|
221
236
|
}
|
|
222
237
|
|
|
238
|
+
/** The gate worker's `--name`/`--port` marker, also what the stray sweep matches. */
|
|
239
|
+
const GATE_SUBCOMMAND = ["tunnel", "gate"] as const;
|
|
240
|
+
|
|
241
|
+
export interface GateLaunch {
|
|
242
|
+
command: string;
|
|
243
|
+
/** Arguments before the gate's own `--name` and `--port`. */
|
|
244
|
+
arguments: string[];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface GateLaunchInputs {
|
|
248
|
+
/** Absolute path of `lib/tunnel/gate.ts` beside this file, whether or not it exists. */
|
|
249
|
+
gateScript: string;
|
|
250
|
+
gateScriptExists: boolean;
|
|
251
|
+
/** True when this process runs on Bun (so `execPath` is a Bun binary). */
|
|
252
|
+
underBun: boolean;
|
|
253
|
+
execPath: string;
|
|
254
|
+
/** The script this process was started with (`argv[1]`): the host CLI's entry or bundle. */
|
|
255
|
+
entryScript: string | undefined;
|
|
256
|
+
entryScriptExists: boolean;
|
|
257
|
+
bunOnPath: boolean;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* How to start the gate worker, which is a `Bun.serve` process (HTTP and
|
|
262
|
+
* WebSocket reverse proxy):
|
|
263
|
+
* 1. From a source or package checkout, `gate.ts` sits beside this module and
|
|
264
|
+
* runs directly.
|
|
265
|
+
* 2. Inside a single-file bundle there is no such file. The CLI then
|
|
266
|
+
* re-executes itself with the hidden `tunnel gate` task, which loads the
|
|
267
|
+
* gate in-process. This uses the running Bun binary, so Bun need not be
|
|
268
|
+
* on PATH.
|
|
269
|
+
* Null means neither works (a Node host with no Bun, or an unknown entry).
|
|
270
|
+
*/
|
|
271
|
+
export function resolveGateLaunch(i: GateLaunchInputs): GateLaunch | null {
|
|
272
|
+
if (i.gateScriptExists && (i.underBun || i.bunOnPath)) {
|
|
273
|
+
return {
|
|
274
|
+
command: i.underBun ? i.execPath : "bun",
|
|
275
|
+
arguments: ["run", i.gateScript],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (i.underBun && i.entryScript && i.entryScriptExists) {
|
|
279
|
+
return { command: i.execPath, arguments: [i.entryScript, ...GATE_SUBCOMMAND] };
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
223
284
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* failure is a clear message rather than an opaque ENOENT from the gate spawn.
|
|
285
|
+
* How to start the log wrapper that rotates each process's output. Beside a
|
|
286
|
+
* source or package checkout the default (this package's own wrapper file)
|
|
287
|
+
* works; inside a single-file bundle the CLI re-executes itself with the hidden
|
|
288
|
+
* `tunnel log-worker` task instead.
|
|
229
289
|
*/
|
|
230
|
-
function
|
|
231
|
-
|
|
290
|
+
export function resolveWorkerLaunch(i: GateLaunchInputs): GateLaunch | undefined {
|
|
291
|
+
if (i.gateScriptExists) return undefined;
|
|
292
|
+
if (i.underBun && i.entryScript && i.entryScriptExists) {
|
|
293
|
+
return { command: i.execPath, arguments: [i.entryScript, "tunnel", "log-worker"] };
|
|
294
|
+
}
|
|
295
|
+
return undefined;
|
|
232
296
|
}
|
|
233
297
|
|
|
298
|
+
function currentLaunchInputs(): GateLaunchInputs {
|
|
299
|
+
const gateScript = gateScriptPath();
|
|
300
|
+
const entryScript = process.argv[1] ? resolve(process.argv[1]) : undefined;
|
|
301
|
+
const underBun = typeof process.versions.bun === "string";
|
|
302
|
+
const gateScriptExists = existsSync(gateScript);
|
|
303
|
+
return {
|
|
304
|
+
gateScript,
|
|
305
|
+
gateScriptExists,
|
|
306
|
+
underBun,
|
|
307
|
+
execPath: process.execPath,
|
|
308
|
+
entryScript,
|
|
309
|
+
entryScriptExists: entryScript ? existsSync(entryScript) : false,
|
|
310
|
+
bunOnPath: !underBun && gateScriptExists && bunOnPath(),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function currentGateLaunch(): GateLaunch | null {
|
|
315
|
+
return resolveGateLaunch(currentLaunchInputs());
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function currentWorkerLaunch(): GateLaunch | undefined {
|
|
319
|
+
return resolveWorkerLaunch(currentLaunchInputs());
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function bunOnPath(): boolean {
|
|
323
|
+
return spawnSync("bun", ["--version"], { stdio: "ignore", windowsHide: true }).status === 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const requiresBunMessage = () =>
|
|
327
|
+
`${resolveBinName()} tunnel requires Bun: the gate worker is a Bun.serve process. ` +
|
|
328
|
+
"Install Bun (https://bun.sh) and re-run. (Every other command runs on Node.)";
|
|
329
|
+
|
|
234
330
|
function tailscaleAvailable(): boolean {
|
|
235
|
-
return spawnSync("tailscale", ["version"], { stdio: "ignore" }).status === 0;
|
|
331
|
+
return spawnSync("tailscale", ["version"], { stdio: "ignore", windowsHide: true }).status === 0;
|
|
236
332
|
}
|
|
237
333
|
|
|
238
334
|
function sleep(ms: number): Promise<void> {
|
|
@@ -240,17 +336,15 @@ function sleep(ms: number): Promise<void> {
|
|
|
240
336
|
}
|
|
241
337
|
|
|
242
338
|
/**
|
|
243
|
-
* Kill every process whose command line matches `pattern` (
|
|
244
|
-
* skipping our own PID and any already-killed. Returns the count
|
|
245
|
-
* as a fallback so orphaned gate/cloudflared processes get cleaned
|
|
246
|
-
* the state file was lost (which otherwise left them squatting on the
|
|
339
|
+
* Kill every process whose command line matches `pattern` (an extended regular
|
|
340
|
+
* expression), skipping our own PID and any already-killed. Returns the count
|
|
341
|
+
* killed. Used as a fallback so orphaned gate/cloudflared processes get cleaned
|
|
342
|
+
* even when the state file was lost (which otherwise left them squatting on the
|
|
343
|
+
* port).
|
|
247
344
|
*/
|
|
248
345
|
function killByPattern(pattern: string, alreadyKilled: Set<number>): number {
|
|
249
|
-
const r = spawnSync("pgrep", ["-f", pattern], { encoding: "utf-8" });
|
|
250
|
-
if (r.status !== 0 || typeof r.stdout !== "string") return 0;
|
|
251
346
|
let killed = 0;
|
|
252
|
-
for (const
|
|
253
|
-
const pid = Number(line.trim());
|
|
347
|
+
for (const pid of findPidsByCommandLine(pattern)) {
|
|
254
348
|
if (!pid || pid === process.pid || alreadyKilled.has(pid)) continue;
|
|
255
349
|
try {
|
|
256
350
|
process.kill(pid);
|
|
@@ -263,63 +357,49 @@ function killByPattern(pattern: string, alreadyKilled: Set<number>): number {
|
|
|
263
357
|
return killed;
|
|
264
358
|
}
|
|
265
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Command-line pattern for ONE instance's gate, identified by its port. Matches
|
|
362
|
+
* both launch forms: `bun run .../gate.ts --name N --port P` and a host CLI
|
|
363
|
+
* re-executing itself as `<cli> tunnel gate --name N --port P`. Port boundary is
|
|
364
|
+
* guarded with `( |$)` so port 9001 doesn't match 90011.
|
|
365
|
+
*/
|
|
366
|
+
function gatePattern(gatePort: number): string {
|
|
367
|
+
return `(gate\\.ts|tunnel gate) .*--port ${gatePort}( |$)`;
|
|
368
|
+
}
|
|
369
|
+
|
|
266
370
|
/**
|
|
267
371
|
* Sweep stray gate + cloudflared processes for ONE instance, identified by its
|
|
268
372
|
* gate port. Both signatures are port-scoped so tearing down one tunnel never
|
|
269
373
|
* touches another:
|
|
270
|
-
* - gate:
|
|
374
|
+
* - gate: see gatePattern (the port is on the gate's argv)
|
|
271
375
|
* - cloudflared: `--url http://localhost:<port>` (order-independent, so it
|
|
272
376
|
* matches regardless of the `--protocol http2` flag we also pass).
|
|
273
|
-
* Port boundary is guarded with `( |$)` so port 9001 doesn't match 90011.
|
|
274
377
|
*/
|
|
275
378
|
function sweepStrays(gatePort: number, alreadyKilled: Set<number>): number {
|
|
276
379
|
return (
|
|
277
|
-
killByPattern(
|
|
380
|
+
killByPattern(gatePattern(gatePort), alreadyKilled) +
|
|
278
381
|
killByPattern(`--url http://localhost:${gatePort}( |$)`, alreadyKilled)
|
|
279
382
|
);
|
|
280
383
|
}
|
|
281
384
|
|
|
282
|
-
/**
|
|
283
|
-
* Ports currently bound by a LISTEN socket (best-effort, platform-dependent).
|
|
284
|
-
*
|
|
285
|
-
* `ss` (iproute2) is Linux-only. Without the lsof fallback this returns an empty
|
|
286
|
-
* set on macOS/BSD. That does not fail loudly. It reports every port as free, so
|
|
287
|
-
* `allocateGatePort` can hand out an occupied port and the reload port-release
|
|
288
|
-
* check becomes inert.
|
|
289
|
-
*/
|
|
290
|
-
function listeningPorts(): Set<number> {
|
|
291
|
-
const ports = new Set<number>();
|
|
292
|
-
const ss = spawnSync("ss", ["-tlnH"], { encoding: "utf-8" });
|
|
293
|
-
if (ss.status === 0 && typeof ss.stdout === "string") {
|
|
294
|
-
for (const m of ss.stdout.matchAll(/:(\d+)\s/g)) ports.add(Number(m[1]));
|
|
295
|
-
return ports;
|
|
296
|
-
}
|
|
297
|
-
// Rows look like: `bun 123 user 4u IPv4 0x… 0t0 TCP 127.0.0.1:58055 (LISTEN)`.
|
|
298
|
-
// Status can be nonzero while stdout still holds usable rows (lsof reports a
|
|
299
|
-
// failure when any single process is unreadable), so judge it on the output.
|
|
300
|
-
const lsof = spawnSync("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN"], { encoding: "utf-8" });
|
|
301
|
-
if (typeof lsof.stdout === "string") {
|
|
302
|
-
for (const m of lsof.stdout.matchAll(/:(\d+) \(LISTEN\)$/gm)) ports.add(Number(m[1]));
|
|
303
|
-
}
|
|
304
|
-
return ports;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
385
|
/**
|
|
308
386
|
* Pick a gate port for a new instance. An explicit `--gate-port` is honored
|
|
309
387
|
* (and rejected if it's already taken); otherwise scan upward from 9001 for the
|
|
310
388
|
* first port that's neither held by a live instance nor currently listening.
|
|
311
389
|
*/
|
|
312
|
-
function allocateGatePort(preferred: number | undefined): number {
|
|
390
|
+
async function allocateGatePort(preferred: number | undefined): Promise<number> {
|
|
313
391
|
const used = new Set<number>(
|
|
314
392
|
listStates()
|
|
315
393
|
.filter((s) => isProcessAlive(s.gate_pid))
|
|
316
394
|
.map((s) => s.gate_port),
|
|
317
395
|
);
|
|
396
|
+
// No discovery tool (null) is not "every port free": probe candidates by connecting.
|
|
318
397
|
const listening = listeningPorts();
|
|
319
|
-
const taken = (p: number) =>
|
|
398
|
+
const taken = async (p: number) =>
|
|
399
|
+
used.has(p) || (listening ? listening.has(p) : await connectProbe(p));
|
|
320
400
|
|
|
321
401
|
if (preferred !== undefined) {
|
|
322
|
-
if (taken(preferred)) {
|
|
402
|
+
if (await taken(preferred)) {
|
|
323
403
|
emit.error({
|
|
324
404
|
code: "tunnel_port_taken",
|
|
325
405
|
message: `Gate port ${preferred} is already in use. Omit --gate-port to auto-allocate, or pick a free one.`,
|
|
@@ -329,7 +409,7 @@ function allocateGatePort(preferred: number | undefined): number {
|
|
|
329
409
|
return preferred;
|
|
330
410
|
}
|
|
331
411
|
for (let p = DEFAULT_GATE_PORT; p <= MAX_GATE_PORT; p++) {
|
|
332
|
-
if (!taken(p)) return p;
|
|
412
|
+
if (!(await taken(p))) return p;
|
|
333
413
|
}
|
|
334
414
|
emit.error({
|
|
335
415
|
code: "tunnel_no_free_port",
|
|
@@ -507,13 +587,14 @@ interface GateSpawnOpts {
|
|
|
507
587
|
* is passed as an env var, which the gate snapshots at module load and never
|
|
508
588
|
* re-reads — that snapshot is precisely why `reload` has to exist.
|
|
509
589
|
*/
|
|
510
|
-
function spawnGate(o: GateSpawnOpts): ChildProcess {
|
|
590
|
+
function spawnGate(o: GateSpawnOpts, launch: GateLaunch): ChildProcess {
|
|
511
591
|
// `--name`/`--port` on argv mirror the env vars; they're what makes the gate
|
|
512
|
-
// process distinguishable per-instance in
|
|
592
|
+
// process distinguishable per-instance in a command-line search (see sweepStrays).
|
|
513
593
|
const gateProc = spawnRotatingProcess({
|
|
514
594
|
path: o.gateLogPath,
|
|
515
|
-
command:
|
|
516
|
-
arguments: [
|
|
595
|
+
command: launch.command,
|
|
596
|
+
arguments: [...launch.arguments, "--name", o.name, "--port", String(o.gatePort)],
|
|
597
|
+
worker: currentWorkerLaunch(),
|
|
517
598
|
env: {
|
|
518
599
|
...process.env,
|
|
519
600
|
HARNERY_TUNNEL_ALLOW: o.allowedIps.join(","),
|
|
@@ -530,13 +611,9 @@ function spawnGate(o: GateSpawnOpts): ChildProcess {
|
|
|
530
611
|
}
|
|
531
612
|
|
|
532
613
|
async function up(opts: UpOpts): Promise<void> {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
message:
|
|
537
|
-
"harn tunnel requires Bun: the gate worker is a Bun.serve process. " +
|
|
538
|
-
"Install Bun (https://bun.sh) and re-run. (Every other harn command runs on Node.)",
|
|
539
|
-
});
|
|
614
|
+
const launch = currentGateLaunch();
|
|
615
|
+
if (!launch) {
|
|
616
|
+
emit.error({ code: "tunnel_requires_bun", message: requiresBunMessage() });
|
|
540
617
|
process.exit(1);
|
|
541
618
|
}
|
|
542
619
|
const name = resolveName(opts.name);
|
|
@@ -581,7 +658,7 @@ async function up(opts: UpOpts): Promise<void> {
|
|
|
581
658
|
|
|
582
659
|
// Allocate the gate port (after clearing dead state so its old port frees up
|
|
583
660
|
// for reuse). Explicit --gate-port is validated; otherwise auto-scan.
|
|
584
|
-
const gatePort = allocateGatePort(opts.gatePort ? Number(opts.gatePort) : undefined);
|
|
661
|
+
const gatePort = await allocateGatePort(opts.gatePort ? Number(opts.gatePort) : undefined);
|
|
585
662
|
|
|
586
663
|
// Self-heal: clear any orphaned gate/cloudflared on THIS instance's port from
|
|
587
664
|
// a prior crashed or state-cleared run so the gate port is free before we bind.
|
|
@@ -591,12 +668,23 @@ async function up(opts: UpOpts): Promise<void> {
|
|
|
591
668
|
if (provider === "cloudflare" && cfg.allowed_ips.length === 0) {
|
|
592
669
|
emit.error({
|
|
593
670
|
code: "tunnel_allowlist_empty",
|
|
594
|
-
message:
|
|
671
|
+
message: `Allowlist is empty; refusing to start. Add an IP first: ${resolveBinName()} tunnel allow add <ip> (or --current)`,
|
|
595
672
|
});
|
|
596
673
|
process.exit(1);
|
|
597
674
|
}
|
|
598
675
|
|
|
599
|
-
|
|
676
|
+
let cloudflaredBin: string | null = null;
|
|
677
|
+
if (provider === "cloudflare") {
|
|
678
|
+
try {
|
|
679
|
+
cloudflaredBin = ensureCloudflared();
|
|
680
|
+
} catch (error) {
|
|
681
|
+
emit.error({
|
|
682
|
+
code: "tunnel_cloudflared_missing",
|
|
683
|
+
message: error instanceof Error ? error.message : String(error),
|
|
684
|
+
});
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
600
688
|
if (provider === "tailscale" && !tailscaleAvailable()) {
|
|
601
689
|
emit.error({
|
|
602
690
|
code: "tunnel_tailscale_missing",
|
|
@@ -609,20 +697,31 @@ async function up(opts: UpOpts): Promise<void> {
|
|
|
609
697
|
const gateLogPath = logPaths.gate;
|
|
610
698
|
const providerLogPath = logPaths.provider;
|
|
611
699
|
|
|
612
|
-
const gateProc = spawnGate(
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
700
|
+
const gateProc = spawnGate(
|
|
701
|
+
{
|
|
702
|
+
name,
|
|
703
|
+
gatePort,
|
|
704
|
+
target,
|
|
705
|
+
vhost,
|
|
706
|
+
provider,
|
|
707
|
+
allowedIps: cfg.allowed_ips,
|
|
708
|
+
allowPaths,
|
|
709
|
+
gateLogPath,
|
|
710
|
+
},
|
|
711
|
+
launch,
|
|
712
|
+
);
|
|
622
713
|
|
|
623
714
|
await sleep(800);
|
|
624
715
|
|
|
625
|
-
|
|
716
|
+
// A host CLI that re-executes itself takes longer to load than a bare script,
|
|
717
|
+
// so wait for the gate to actually listen before the provider forwards to it.
|
|
718
|
+
if (!isProcessAlive(gateProc.pid!) || !(await waitForPortBound(gatePort, 15_000))) {
|
|
719
|
+
try {
|
|
720
|
+
process.kill(gateProc.pid!);
|
|
721
|
+
} catch {
|
|
722
|
+
/* already dead */
|
|
723
|
+
}
|
|
724
|
+
sweepStrays(gatePort, new Set());
|
|
626
725
|
emit.error({
|
|
627
726
|
code: "tunnel_gate_failed",
|
|
628
727
|
message: `Gate failed to start. Check log: ${gateLogPath}`,
|
|
@@ -644,6 +743,7 @@ async function up(opts: UpOpts): Promise<void> {
|
|
|
644
743
|
path: providerLogPath,
|
|
645
744
|
command: cloudflaredBin!,
|
|
646
745
|
arguments: ["tunnel", "--protocol", "http2", "--url", `http://localhost:${gatePort}`],
|
|
746
|
+
worker: currentWorkerLaunch(),
|
|
647
747
|
});
|
|
648
748
|
cfdProc.unref();
|
|
649
749
|
cloudflaredPid = cfdProc.pid!;
|
|
@@ -703,8 +803,9 @@ async function up(opts: UpOpts): Promise<void> {
|
|
|
703
803
|
writeState(state);
|
|
704
804
|
recordTunnelServer(state);
|
|
705
805
|
|
|
806
|
+
const bin = resolveBinName();
|
|
706
807
|
const stopHint =
|
|
707
|
-
name === DEFAULT_INSTANCE ?
|
|
808
|
+
name === DEFAULT_INSTANCE ? `${bin} tunnel down` : `${bin} tunnel down --name ${name}`;
|
|
708
809
|
emit.text(`\n Instance: ${name}\n`);
|
|
709
810
|
emit.text(` Provider: ${provider}${tailscaleMode ? ` (${tailscaleMode})` : ""}\n`);
|
|
710
811
|
emit.text(` URL: ${url}\n\n`);
|
|
@@ -723,11 +824,11 @@ async function up(opts: UpOpts): Promise<void> {
|
|
|
723
824
|
}
|
|
724
825
|
if (!registered) {
|
|
725
826
|
emit.text(
|
|
726
|
-
` ⚠ Edge connection didn't register within 30s (QUIC can wedge on a cold\n start). If the URL 404s or times out, bounce it: ${stopHint} &&
|
|
827
|
+
` ⚠ Edge connection didn't register within 30s (QUIC can wedge on a cold\n start). If the URL 404s or times out, bounce it: ${stopHint} && ${bin} tunnel up\n\n`,
|
|
727
828
|
);
|
|
728
829
|
}
|
|
729
830
|
emit.text(` Stop: ${stopHint}\n`);
|
|
730
|
-
emit.text(
|
|
831
|
+
emit.text(` Status: ${bin} tunnel status\n`);
|
|
731
832
|
}
|
|
732
833
|
|
|
733
834
|
/** The server registry lists tunnels beside every other local server. */
|
|
@@ -741,7 +842,13 @@ function recordTunnelServer(state: TunnelState): void {
|
|
|
741
842
|
}
|
|
742
843
|
|
|
743
844
|
/** Tear down a single instance by name. Returns the number of processes killed. */
|
|
744
|
-
function
|
|
845
|
+
function portHeldByOtherLiveInstance(port: number, name: string): boolean {
|
|
846
|
+
return listStates().some(
|
|
847
|
+
(s) => s.name !== name && s.gate_port === port && isProcessAlive(s.gate_pid),
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
export function downOne(name: string, killed: Set<number>): number {
|
|
745
852
|
const before = killed.size;
|
|
746
853
|
const state = readState(name);
|
|
747
854
|
if (state) {
|
|
@@ -759,8 +866,11 @@ function downOne(name: string, killed: Set<number>): number {
|
|
|
759
866
|
}
|
|
760
867
|
}
|
|
761
868
|
// Fallback: sweep orphans on this instance's gate port, even when state was
|
|
762
|
-
// lost; they'd otherwise squat on the port and break the next `up`.
|
|
763
|
-
|
|
869
|
+
// lost; they'd otherwise squat on the port and break the next `up`. Skip it
|
|
870
|
+
// when another live instance now owns that port: a stale record's port is
|
|
871
|
+
// reused after a reboot, and sweeping it would kill the live tunnel there.
|
|
872
|
+
const port = state?.gate_port ?? DEFAULT_GATE_PORT;
|
|
873
|
+
if (!portHeldByOtherLiveInstance(port, name)) sweepStrays(port, killed);
|
|
764
874
|
clearState(name);
|
|
765
875
|
unregisterServer(tunnelServerId(name), { coordRoot: registryRoot() });
|
|
766
876
|
return killed.size - before;
|
|
@@ -789,7 +899,7 @@ function down(opts: DownOpts): void {
|
|
|
789
899
|
const others = listStates();
|
|
790
900
|
if (others.length > 0) {
|
|
791
901
|
emit.text(
|
|
792
|
-
`No default tunnel running. Other tunnels up: ${others.map((s) => s.name).join(", ")}.\nUse
|
|
902
|
+
`No default tunnel running. Other tunnels up: ${others.map((s) => s.name).join(", ")}.\nUse \`${resolveBinName()} tunnel down --name <name>\` or \`${resolveBinName()} tunnel down --all\`.\n`,
|
|
793
903
|
);
|
|
794
904
|
return;
|
|
795
905
|
}
|
|
@@ -807,20 +917,20 @@ function down(opts: DownOpts): void {
|
|
|
807
917
|
async function waitForPortFree(port: number, timeoutMs: number): Promise<boolean> {
|
|
808
918
|
const deadline = Date.now() + timeoutMs;
|
|
809
919
|
while (Date.now() < deadline) {
|
|
810
|
-
if (!
|
|
920
|
+
if (!(await portListening(port))) return true;
|
|
811
921
|
await sleep(100);
|
|
812
922
|
}
|
|
813
|
-
return !
|
|
923
|
+
return !(await portListening(port));
|
|
814
924
|
}
|
|
815
925
|
|
|
816
926
|
/** Poll until `port` has a LISTEN socket (or the deadline passes). */
|
|
817
927
|
async function waitForPortBound(port: number, timeoutMs: number): Promise<boolean> {
|
|
818
928
|
const deadline = Date.now() + timeoutMs;
|
|
819
929
|
while (Date.now() < deadline) {
|
|
820
|
-
if (
|
|
930
|
+
if (await portListening(port)) return true;
|
|
821
931
|
await sleep(100);
|
|
822
932
|
}
|
|
823
|
-
return
|
|
933
|
+
return portListening(port);
|
|
824
934
|
}
|
|
825
935
|
|
|
826
936
|
/**
|
|
@@ -847,8 +957,12 @@ export async function reloadOne(state: TunnelState): Promise<{ ok: boolean; mess
|
|
|
847
957
|
`Reloading the gate can't bring it back — run \`${bin} tunnel up --name ${name}\`.`,
|
|
848
958
|
};
|
|
849
959
|
}
|
|
850
|
-
|
|
851
|
-
|
|
960
|
+
const launch = currentGateLaunch();
|
|
961
|
+
if (!launch) {
|
|
962
|
+
return {
|
|
963
|
+
ok: false,
|
|
964
|
+
message: `[${name}] Bun is not available, so the gate can't be respawned.`,
|
|
965
|
+
};
|
|
852
966
|
}
|
|
853
967
|
if (state.allow_paths.length === 0) {
|
|
854
968
|
return {
|
|
@@ -872,7 +986,7 @@ export async function reloadOne(state: TunnelState): Promise<{ ok: boolean; mess
|
|
|
872
986
|
/* race: already gone */
|
|
873
987
|
}
|
|
874
988
|
}
|
|
875
|
-
killByPattern(
|
|
989
|
+
killByPattern(gatePattern(gatePort), killed);
|
|
876
990
|
|
|
877
991
|
if (!(await waitForPortFree(gatePort, 5_000))) {
|
|
878
992
|
return {
|
|
@@ -884,23 +998,26 @@ export async function reloadOne(state: TunnelState): Promise<{ ok: boolean; mess
|
|
|
884
998
|
}
|
|
885
999
|
|
|
886
1000
|
const cfg = readConfig();
|
|
887
|
-
const gateProc = spawnGate(
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1001
|
+
const gateProc = spawnGate(
|
|
1002
|
+
{
|
|
1003
|
+
name,
|
|
1004
|
+
gatePort,
|
|
1005
|
+
target: state.target,
|
|
1006
|
+
vhost: state.vhost,
|
|
1007
|
+
provider: state.provider,
|
|
1008
|
+
allowedIps: cfg.allowed_ips,
|
|
1009
|
+
allowPaths: state.allow_paths,
|
|
1010
|
+
gateLogPath: tunnelLogDestinations(name, state.provider).gate,
|
|
1011
|
+
},
|
|
1012
|
+
launch,
|
|
1013
|
+
);
|
|
897
1014
|
|
|
898
1015
|
await sleep(800);
|
|
899
1016
|
const pid = gateProc.pid;
|
|
900
1017
|
if (
|
|
901
1018
|
typeof pid !== "number" ||
|
|
902
1019
|
!isProcessAlive(pid) ||
|
|
903
|
-
!(await waitForPortBound(gatePort,
|
|
1020
|
+
!(await waitForPortBound(gatePort, 15_000))
|
|
904
1021
|
) {
|
|
905
1022
|
return {
|
|
906
1023
|
ok: false,
|
|
@@ -1077,9 +1194,16 @@ function allowList(): void {
|
|
|
1077
1194
|
emit.text("(empty)\n");
|
|
1078
1195
|
return;
|
|
1079
1196
|
}
|
|
1080
|
-
|
|
1197
|
+
const automatic = cfg.auto_allowed ?? [];
|
|
1198
|
+
for (const ip of cfg.allowed_ips) {
|
|
1199
|
+
emit.text(`${ip}${sameEntryIn(automatic, ip) ? " (automatic)" : ""}\n`);
|
|
1200
|
+
}
|
|
1081
1201
|
}
|
|
1082
1202
|
|
|
1203
|
+
const canonicalEntry = (entry: string) => normalizeAllowEntry(entry) ?? entry;
|
|
1204
|
+
const sameEntryIn = (list: readonly string[], entry: string) =>
|
|
1205
|
+
list.some((e) => canonicalEntry(e) === canonicalEntry(entry));
|
|
1206
|
+
|
|
1083
1207
|
/**
|
|
1084
1208
|
* Trailer for `allow add`/`allow rm`. Each gate snapshots the allowlist at
|
|
1085
1209
|
* spawn, so a config edit alone changes nothing until the gates restart.
|
|
@@ -1097,31 +1221,156 @@ function allowChangedHint(): void {
|
|
|
1097
1221
|
);
|
|
1098
1222
|
}
|
|
1099
1223
|
|
|
1100
|
-
|
|
1224
|
+
interface AllowAddOpts {
|
|
1225
|
+
current?: boolean;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
async function allowAdd(entry: string | undefined, opts: AllowAddOpts = {}): Promise<void> {
|
|
1229
|
+
if (opts.current) {
|
|
1230
|
+
if (entry) {
|
|
1231
|
+
emit.error({
|
|
1232
|
+
code: "tunnel_allow_current_with_entry",
|
|
1233
|
+
message: "Pass either an address or --current, not both.",
|
|
1234
|
+
});
|
|
1235
|
+
process.exit(1);
|
|
1236
|
+
}
|
|
1237
|
+
const result = await refreshCurrentAddress({ emit, reload: true });
|
|
1238
|
+
if (!result.ok) process.exit(1);
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
if (!entry) {
|
|
1242
|
+
emit.error({
|
|
1243
|
+
code: "tunnel_allow_entry_missing",
|
|
1244
|
+
message: `Name an address or range to allow, or use --current: ${resolveBinName()} tunnel allow add <ip|cidr>`,
|
|
1245
|
+
});
|
|
1246
|
+
process.exit(1);
|
|
1247
|
+
}
|
|
1248
|
+
const canonical = normalizeAllowEntry(entry);
|
|
1249
|
+
if (!canonical) {
|
|
1250
|
+
emit.error({
|
|
1251
|
+
code: "tunnel_allow_entry_invalid",
|
|
1252
|
+
message:
|
|
1253
|
+
`"${entry}" is not an IP address or CIDR range. ` +
|
|
1254
|
+
"Examples: 203.0.113.8, 203.0.113.0/24, 2601:db8:1:2::/64.",
|
|
1255
|
+
});
|
|
1256
|
+
process.exit(1);
|
|
1257
|
+
}
|
|
1101
1258
|
const cfg = readConfig();
|
|
1102
|
-
if (cfg.allowed_ips
|
|
1103
|
-
|
|
1259
|
+
if (sameEntryIn(cfg.allowed_ips, canonical)) {
|
|
1260
|
+
// Adding by hand an entry the refresh owns adopts it: later refreshes keep it.
|
|
1261
|
+
if (cfg.auto_allowed && sameEntryIn(cfg.auto_allowed, canonical)) {
|
|
1262
|
+
cfg.auto_allowed = cfg.auto_allowed.filter((e) => canonicalEntry(e) !== canonical);
|
|
1263
|
+
writeConfig(cfg);
|
|
1264
|
+
emit.text(`${canonical} is now a manual entry; automatic refreshes will keep it.\n`);
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
emit.text(`${canonical} already in allowlist.\n`);
|
|
1104
1268
|
return;
|
|
1105
1269
|
}
|
|
1106
|
-
cfg.allowed_ips.push(
|
|
1270
|
+
cfg.allowed_ips.push(canonical);
|
|
1107
1271
|
writeConfig(cfg);
|
|
1108
|
-
emit.text(`Added ${
|
|
1272
|
+
emit.text(`Added ${canonical}.\n`);
|
|
1109
1273
|
allowChangedHint();
|
|
1110
1274
|
}
|
|
1111
1275
|
|
|
1112
|
-
function allowRm(
|
|
1276
|
+
function allowRm(entry: string): void {
|
|
1113
1277
|
const cfg = readConfig();
|
|
1114
|
-
const
|
|
1278
|
+
const wanted = canonicalEntry(entry);
|
|
1279
|
+
const idx = cfg.allowed_ips.findIndex((e) => e === entry || canonicalEntry(e) === wanted);
|
|
1115
1280
|
if (idx === -1) {
|
|
1116
|
-
emit.text(`${
|
|
1281
|
+
emit.text(`${entry} not in allowlist.\n`);
|
|
1117
1282
|
return;
|
|
1118
1283
|
}
|
|
1119
|
-
cfg.allowed_ips.splice(idx, 1);
|
|
1284
|
+
const [removed] = cfg.allowed_ips.splice(idx, 1);
|
|
1285
|
+
if (cfg.auto_allowed) {
|
|
1286
|
+
cfg.auto_allowed = cfg.auto_allowed.filter(
|
|
1287
|
+
(e) => canonicalEntry(e) !== canonicalEntry(removed as string),
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1120
1290
|
writeConfig(cfg);
|
|
1121
|
-
emit.text(`Removed ${
|
|
1291
|
+
emit.text(`Removed ${removed}.\n`);
|
|
1122
1292
|
allowChangedHint();
|
|
1123
1293
|
}
|
|
1124
1294
|
|
|
1295
|
+
export interface RefreshCurrentOptions {
|
|
1296
|
+
emit?: EmitContext;
|
|
1297
|
+
/** Reload running Cloudflare gates after an allowlist change, keeping their URLs. */
|
|
1298
|
+
reload: boolean;
|
|
1299
|
+
/** Speak only when the allowlist changed or no address could be detected. */
|
|
1300
|
+
quiet?: boolean;
|
|
1301
|
+
/** Lookup timeout and retries; a sync that must not stall offline passes small ones. */
|
|
1302
|
+
detectOptions?: DetectOptions;
|
|
1303
|
+
/** Replaces the lookup (tests). */
|
|
1304
|
+
detect?: typeof detectPublicAddresses;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
export interface RefreshCurrentResult {
|
|
1308
|
+
/** False only when a requested reload of a live gate failed. A failed lookup is a warning, not a failure. */
|
|
1309
|
+
ok: boolean;
|
|
1310
|
+
/** The allowlist gained or lost an entry. */
|
|
1311
|
+
changed: boolean;
|
|
1312
|
+
/** Entries newly allowed. */
|
|
1313
|
+
added: string[];
|
|
1314
|
+
removed: string[];
|
|
1315
|
+
warnings: string[];
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* `tunnel allow add --current`: allow this machine's own public addresses, as
|
|
1320
|
+
* Cloudflare sees them, and drop the ones allowed by the previous refresh.
|
|
1321
|
+
* Exported so a host CLI can run it before `tunnel up` and from its own sync.
|
|
1322
|
+
*/
|
|
1323
|
+
export async function refreshCurrentAddress(
|
|
1324
|
+
options: RefreshCurrentOptions,
|
|
1325
|
+
): Promise<RefreshCurrentResult> {
|
|
1326
|
+
if (options.emit) emit = options.emit;
|
|
1327
|
+
const detected = await (options.detect ?? detectPublicAddresses)(options.detectOptions);
|
|
1328
|
+
const cfg = readConfig();
|
|
1329
|
+
const plan = planCurrentAddresses(cfg, detected);
|
|
1330
|
+
const noAddress = !detected.v4 && !detected.v6;
|
|
1331
|
+
// Quiet runs (a host's sync) speak only when something changed or nothing
|
|
1332
|
+
// could be detected; "no IPv6 on this network" is not news every time.
|
|
1333
|
+
if (!options.quiet || noAddress || plan.changed) {
|
|
1334
|
+
for (const warning of plan.warnings) emit.text(` ⚠ ${warning}\n`);
|
|
1335
|
+
}
|
|
1336
|
+
const result: RefreshCurrentResult = {
|
|
1337
|
+
ok: true,
|
|
1338
|
+
changed: plan.changed,
|
|
1339
|
+
added: plan.added,
|
|
1340
|
+
removed: plan.removed,
|
|
1341
|
+
warnings: plan.warnings,
|
|
1342
|
+
};
|
|
1343
|
+
if (noAddress) return result;
|
|
1344
|
+
|
|
1345
|
+
writeConfig(plan.config);
|
|
1346
|
+
if (!options.quiet || plan.changed) {
|
|
1347
|
+
const found = [detected.v4, detected.v6].filter(Boolean).join(", ");
|
|
1348
|
+
emit.text(`This machine reaches Cloudflare as ${found}.\n`);
|
|
1349
|
+
emit.text(
|
|
1350
|
+
plan.changed
|
|
1351
|
+
? `Allowlist updated${plan.added.length ? `: added ${plan.added.join(", ")}` : ""}${plan.removed.length ? `; removed ${plan.removed.join(", ")}` : ""}.\n`
|
|
1352
|
+
: "Allowlist already covers this machine.\n",
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
if (!plan.changed || !options.reload) return result;
|
|
1356
|
+
|
|
1357
|
+
const live = listStates().filter((s) => s.provider === "cloudflare" && providerIsAlive(s));
|
|
1358
|
+
for (const s of live) {
|
|
1359
|
+
const r = await reloadOne(s);
|
|
1360
|
+
emit.text(`${r.ok ? "ok " : "FAIL"} ${r.message}\n`);
|
|
1361
|
+
if (!r.ok) result.ok = false;
|
|
1362
|
+
}
|
|
1363
|
+
return result;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/** Hidden task: run the gate worker in this process (the self-launch form of the gate). */
|
|
1367
|
+
async function runGateInProcess(): Promise<void> {
|
|
1368
|
+
await import("../lib/tunnel/gate.ts");
|
|
1369
|
+
// The gate's server keeps the process alive. Never resolve, so a host's
|
|
1370
|
+
// post-command exit or output finalization cannot end the worker.
|
|
1371
|
+
await new Promise<never>(() => {});
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1125
1374
|
let emit: EmitContext;
|
|
1126
1375
|
let context: HarneryProgramContext | undefined;
|
|
1127
1376
|
|
|
@@ -1206,9 +1455,38 @@ export function registerTunnelCommand(
|
|
|
1206
1455
|
|
|
1207
1456
|
const allow = cmd
|
|
1208
1457
|
.command("allow")
|
|
1209
|
-
.description(
|
|
1458
|
+
.description(
|
|
1459
|
+
"Manage the Cloudflare CF-Connecting-IP allowlist (addresses and CIDR ranges, IPv4 and IPv6)",
|
|
1460
|
+
)
|
|
1210
1461
|
.action(allowList);
|
|
1211
|
-
allow
|
|
1212
|
-
|
|
1213
|
-
|
|
1462
|
+
allow
|
|
1463
|
+
.command("add [entry]")
|
|
1464
|
+
.description(
|
|
1465
|
+
"Add an IP address or CIDR range (203.0.113.0/24, 2601:db8:1:2::/64), or --current for this machine",
|
|
1466
|
+
)
|
|
1467
|
+
.option(
|
|
1468
|
+
"--current",
|
|
1469
|
+
"allow this machine's public addresses as Cloudflare sees them (IPv4 /32 and IPv6 /64), " +
|
|
1470
|
+
"replace the ones the previous --current added, and reload running gates",
|
|
1471
|
+
)
|
|
1472
|
+
.action(allowAdd);
|
|
1473
|
+
allow
|
|
1474
|
+
.command("rm <entry>")
|
|
1475
|
+
.description("Remove an address or range from the allowlist")
|
|
1476
|
+
.action(allowRm);
|
|
1477
|
+
allow.command("list").description("List allowed entries (default action)").action(allowList);
|
|
1478
|
+
|
|
1479
|
+
cmd
|
|
1480
|
+
.command("log-worker <specification>", { hidden: true })
|
|
1481
|
+
.description("Internal: run the output-rotating wrapper for one tunnel process")
|
|
1482
|
+
.action(async (specification: string) => {
|
|
1483
|
+
await runProcessLogWorker(specification);
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
cmd
|
|
1487
|
+
.command("gate", { hidden: true })
|
|
1488
|
+
.description("Internal: run the gate worker in this process")
|
|
1489
|
+
.option("--name <name>", "instance name")
|
|
1490
|
+
.option("--port <port>", "port to listen on")
|
|
1491
|
+
.action(runGateInProcess);
|
|
1214
1492
|
}
|