harnery 0.41.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 +21 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +63 -5
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +113 -12
- package/dist/commands/browse.d.ts +164 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +263 -51
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +52 -0
- package/dist/commands/disk.d.ts +6 -0
- package/dist/commands/disk.d.ts.map +1 -0
- package/dist/commands/disk.js +80 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +48 -1
- package/dist/commands/files.d.ts +16 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +54 -2
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +43 -9
- package/dist/commands/rm.d.ts +4 -0
- package/dist/commands/rm.d.ts.map +1 -0
- package/dist/commands/rm.js +87 -0
- package/dist/commands/servers.d.ts +7 -0
- package/dist/commands/servers.d.ts.map +1 -0
- package/dist/commands/servers.js +394 -0
- package/dist/commands/tunnel.d.ts +65 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +372 -103
- 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/adapters/profiles.d.ts +9 -0
- package/dist/core/adapters/profiles.d.ts.map +1 -1
- package/dist/core/adapters/profiles.js +22 -1
- package/dist/core/adapters/types.d.ts +9 -0
- package/dist/core/adapters/types.d.ts.map +1 -1
- package/dist/core/agents/cli.js +13 -6
- 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 +59 -13
- package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +60 -0
- 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/artifacts/delivery-card.js +9 -2
- package/dist/core/artifacts/index.d.ts +140 -4
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +522 -49
- package/dist/core/config.d.ts +68 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +128 -3
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +2 -2
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +16 -0
- package/dist/core/events/v3/projection.js +9 -1
- 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/adapter/events.d.ts +17 -0
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +5 -0
- package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
- package/dist/core/hooks/adapter/wiring.d.ts +37 -7
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +62 -4
- package/dist/core/hooks/cli.js +131 -23
- 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/run-markers.d.ts +37 -0
- package/dist/core/hooks/run-markers.d.ts.map +1 -0
- package/dist/core/hooks/run-markers.js +109 -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/servers/index.d.ts +211 -0
- package/dist/core/servers/index.d.ts.map +1 -0
- package/dist/core/servers/index.js +560 -0
- package/dist/core/servers/net.d.ts +58 -0
- package/dist/core/servers/net.d.ts.map +1 -0
- package/dist/core/servers/net.js +270 -0
- package/dist/core/servers/tunnels.d.ts +14 -0
- package/dist/core/servers/tunnels.d.ts.map +1 -0
- package/dist/core/servers/tunnels.js +33 -0
- package/dist/core/storage/builtins.js +1 -0
- 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/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +30 -1
- package/dist/core/workflow/sandbox-projection.d.ts +13 -2
- package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
- package/dist/core/workflow/sandbox-projection.js +23 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +6 -1
- package/dist/core/workflow/spawn-codex.d.ts +2 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +20 -1
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +6 -1
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +4 -1
- package/dist/core/workflow/types.d.ts +47 -2
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/worker-access.d.ts +92 -0
- package/dist/core/workflow/worker-access.d.ts.map +1 -0
- package/dist/core/workflow/worker-access.js +450 -0
- package/dist/lib/browser/batch-steps.d.ts +18 -0
- package/dist/lib/browser/batch-steps.d.ts.map +1 -0
- package/dist/lib/browser/batch-steps.js +82 -0
- package/dist/lib/browser/collect/collector.d.ts +99 -0
- package/dist/lib/browser/collect/collector.d.ts.map +1 -0
- package/dist/lib/browser/collect/collector.js +202 -0
- package/dist/lib/browser/collect/fields.d.ts +31 -0
- package/dist/lib/browser/collect/fields.d.ts.map +1 -0
- package/dist/lib/browser/collect/fields.js +75 -0
- package/dist/lib/browser/collect/index.d.ts +6 -0
- package/dist/lib/browser/collect/index.d.ts.map +1 -0
- package/dist/lib/browser/collect/index.js +5 -0
- package/dist/lib/browser/collect/page-driver.d.ts +14 -0
- package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
- package/dist/lib/browser/collect/page-driver.js +306 -0
- package/dist/lib/browser/collect/presets.d.ts +52 -0
- package/dist/lib/browser/collect/presets.d.ts.map +1 -0
- package/dist/lib/browser/collect/presets.js +176 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
- package/dist/lib/browser/collect/scroll-plan.js +118 -0
- package/dist/lib/browser/index.d.ts +1 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.d.ts +2 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +13 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +1 -0
- package/dist/lib/browser/windows-chrome.d.ts +59 -0
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
- package/dist/lib/browser/windows-chrome.js +179 -0
- package/dist/lib/claude-desktop-share.d.ts +136 -0
- package/dist/lib/claude-desktop-share.d.ts.map +1 -0
- package/dist/lib/claude-desktop-share.js +394 -0
- package/dist/lib/disk-usage.d.ts +50 -0
- package/dist/lib/disk-usage.d.ts.map +1 -0
- package/dist/lib/disk-usage.js +303 -0
- package/dist/lib/docs-lint.d.ts +6 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +8 -3
- package/dist/lib/guarded-remove.d.ts +34 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -0
- package/dist/lib/guarded-remove.js +310 -0
- package/dist/lib/instructions/templates.js +8 -8
- 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/error-page.d.ts +1 -1
- package/dist/lib/tunnel/error-page.d.ts.map +1 -1
- package/dist/lib/tunnel/error-page.js +19 -8
- package/dist/lib/tunnel/gate.js +33 -7
- package/dist/lib/tunnel/path-scope.d.ts +22 -0
- package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
- package/dist/lib/tunnel/path-scope.js +62 -0
- 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 +33 -3
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +115 -30
- package/package.json +7 -1
- package/schemas/config.schema.json +91 -0
- package/src/commander.ts +51 -0
- package/src/commands/agents.ts +111 -11
- package/src/commands/artifacts.ts +171 -19
- package/src/commands/browse.ts +400 -57
- package/src/commands/claude-desktop.ts +61 -0
- package/src/commands/disk.ts +114 -0
- package/src/commands/doctor.ts +52 -1
- package/src/commands/files.ts +93 -2
- package/src/commands/init.ts +55 -5
- package/src/commands/rm.ts +115 -0
- package/src/commands/servers.ts +506 -0
- package/src/commands/tunnel.ts +494 -116
- package/src/commands/web-fetch.ts +30 -19
- package/src/commands/web.ts +9 -3
- package/src/core/adapters/profiles.ts +22 -1
- package/src/core/adapters/types.ts +10 -0
- package/src/core/agents/cli.ts +14 -6
- package/src/core/agents/render/prompt-context.ts +25 -8
- package/src/core/agents/render/session-context.ts +84 -16
- package/src/core/agents/state/live-coordination-view.ts +73 -0
- package/src/core/agents/state/names.ts +117 -6
- package/src/core/artifacts/delivery-card.ts +10 -2
- package/src/core/artifacts/index.ts +697 -44
- package/src/core/config.ts +183 -4
- package/src/core/diagnostics/bundle.ts +2 -1
- package/src/core/events/v3/producers/recorder.ts +16 -0
- package/src/core/events/v3/projection.ts +11 -1
- package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
- package/src/core/hooks/adapter/events.ts +19 -0
- package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
- package/src/core/hooks/adapter/wiring.ts +116 -8
- package/src/core/hooks/cli.ts +152 -22
- package/src/core/hooks/fork-lineage.ts +82 -0
- package/src/core/hooks/resolve/transcript.ts +88 -0
- package/src/core/hooks/run-markers.ts +132 -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/servers/index.ts +712 -0
- package/src/core/servers/net.ts +283 -0
- package/src/core/servers/tunnels.ts +39 -0
- package/src/core/storage/builtins.ts +1 -0
- package/src/core/storage/process-log.ts +17 -2
- package/src/core/workflow/engine.ts +38 -1
- package/src/core/workflow/sandbox-projection.ts +38 -2
- package/src/core/workflow/spawn-claude.ts +5 -1
- package/src/core/workflow/spawn-codex.ts +18 -1
- package/src/core/workflow/spawn-cursor.ts +5 -1
- package/src/core/workflow/spawn-opencode.ts +4 -1
- package/src/core/workflow/types.ts +62 -2
- package/src/core/workflow/worker-access.ts +525 -0
- package/src/lib/browser/batch-steps.ts +79 -0
- package/src/lib/browser/collect/collector.ts +306 -0
- package/src/lib/browser/collect/fields.ts +87 -0
- package/src/lib/browser/collect/index.ts +47 -0
- package/src/lib/browser/collect/page-driver.ts +327 -0
- package/src/lib/browser/collect/presets.ts +237 -0
- package/src/lib/browser/collect/scroll-plan.ts +179 -0
- package/src/lib/browser/index.ts +1 -0
- package/src/lib/browser/page-review-judge.ts +16 -0
- package/src/lib/browser/qa-run-contracts.ts +11 -0
- package/src/lib/browser/qa-run.ts +1 -0
- package/src/lib/browser/windows-chrome.ts +227 -0
- package/src/lib/claude-desktop-share.ts +511 -0
- package/src/lib/disk-usage.ts +347 -0
- package/src/lib/docs-lint.ts +8 -3
- package/src/lib/guarded-remove.ts +376 -0
- package/src/lib/instructions/templates.ts +8 -8
- package/src/lib/tunnel/allowlist.ts +208 -0
- package/src/lib/tunnel/current-address.ts +158 -0
- package/src/lib/tunnel/error-page.ts +25 -9
- package/src/lib/tunnel/gate.ts +38 -9
- package/src/lib/tunnel/path-scope.ts +73 -0
- package/src/lib/tunnel/processes.ts +149 -0
- package/src/lib/tunnel/state.ts +143 -30
package/src/commands/tunnel.ts
CHANGED
|
@@ -4,12 +4,33 @@ import { resolve } from "node:path";
|
|
|
4
4
|
import type { Command } from "commander";
|
|
5
5
|
import type { EmitContext, HarneryProgramContext } from "../commander.ts";
|
|
6
6
|
|
|
7
|
+
import { resolveCoordRoot } from "../core/agents/coord-client.ts";
|
|
7
8
|
import { resolveBinName } from "../core/config.ts";
|
|
9
|
+
import {
|
|
10
|
+
tryRegisterServer,
|
|
11
|
+
tunnelServerId,
|
|
12
|
+
tunnelServerInput,
|
|
13
|
+
unregisterServer,
|
|
14
|
+
} from "../core/servers/index.ts";
|
|
8
15
|
import {
|
|
9
16
|
processLogDestination,
|
|
17
|
+
runProcessLogWorker,
|
|
10
18
|
runRotatingProcessSync,
|
|
11
19
|
spawnRotatingProcess,
|
|
12
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";
|
|
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";
|
|
13
34
|
import {
|
|
14
35
|
clearState,
|
|
15
36
|
DEFAULT_INSTANCE,
|
|
@@ -35,10 +56,14 @@ import {
|
|
|
35
56
|
* upstream. Cloudflare quick tunnels add an IP allowlist at the gate via the
|
|
36
57
|
* Cloudflare-set `CF-Connecting-IP` header; Tailscale Serve/Funnel exposes the
|
|
37
58
|
* same gate through tailscaled, with Tailscale owning the access boundary.
|
|
59
|
+
* Every provider also passes through a path scope: `up` requires at least one
|
|
60
|
+
* `--allow-path <prefix>`, and the gate refuses any request outside it.
|
|
38
61
|
*
|
|
39
|
-
* State + config persisted under `.cache/tunnel
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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.
|
|
42
67
|
*/
|
|
43
68
|
|
|
44
69
|
const DEFAULT_TARGET = "127.0.0.1:8001";
|
|
@@ -55,6 +80,7 @@ interface UpOpts {
|
|
|
55
80
|
visibility?: string;
|
|
56
81
|
path?: string;
|
|
57
82
|
httpsPort?: string;
|
|
83
|
+
allowPath?: string[];
|
|
58
84
|
}
|
|
59
85
|
|
|
60
86
|
interface DownOpts {
|
|
@@ -165,23 +191,144 @@ function resolveHttpsPort(raw: string | undefined): number {
|
|
|
165
191
|
return port;
|
|
166
192
|
}
|
|
167
193
|
|
|
194
|
+
/**
|
|
195
|
+
* The path scope for `up`. At least one prefix is required: a tunnel forwards
|
|
196
|
+
* to a whole local app, and publishing all of it has to be a stated choice
|
|
197
|
+
* (`--allow-path /`), never the result of leaving a flag off.
|
|
198
|
+
*/
|
|
199
|
+
function resolveAllowPaths(raw: string[] | undefined): string[] {
|
|
200
|
+
let paths: string[];
|
|
201
|
+
try {
|
|
202
|
+
paths = normalizeAllowPaths(raw ?? []);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
emit.error({
|
|
205
|
+
code: "tunnel_path_scope_invalid",
|
|
206
|
+
message: error instanceof Error ? error.message : String(error),
|
|
207
|
+
});
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
if (paths.length === 0) {
|
|
211
|
+
emit.error({
|
|
212
|
+
code: "tunnel_path_scope_empty",
|
|
213
|
+
message:
|
|
214
|
+
"No path scope; refusing to start. Name what the tunnel shares with --allow-path <prefix> " +
|
|
215
|
+
"(repeatable), or pass --allow-path / to publish the whole upstream.",
|
|
216
|
+
});
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
return paths;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function samePaths(a: readonly string[], b: readonly string[]): boolean {
|
|
223
|
+
return a.length === b.length && [...a].sort().join("\n") === [...b].sort().join("\n");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function formatPaths(paths: readonly string[]): string {
|
|
227
|
+
return paths.length === 0 ? "(none: every path refused)" : paths.join(", ");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function collectPath(value: string, previous: string[]): string[] {
|
|
231
|
+
return [...previous, value];
|
|
232
|
+
}
|
|
233
|
+
|
|
168
234
|
function gateScriptPath(): string {
|
|
169
235
|
return resolve(import.meta.dirname, "..", "lib", "tunnel", "gate.ts");
|
|
170
236
|
}
|
|
171
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
|
+
|
|
172
284
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* 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.
|
|
178
289
|
*/
|
|
179
|
-
function
|
|
180
|
-
|
|
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;
|
|
296
|
+
}
|
|
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());
|
|
181
320
|
}
|
|
182
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
|
+
|
|
183
330
|
function tailscaleAvailable(): boolean {
|
|
184
|
-
return spawnSync("tailscale", ["version"], { stdio: "ignore" }).status === 0;
|
|
331
|
+
return spawnSync("tailscale", ["version"], { stdio: "ignore", windowsHide: true }).status === 0;
|
|
185
332
|
}
|
|
186
333
|
|
|
187
334
|
function sleep(ms: number): Promise<void> {
|
|
@@ -189,17 +336,15 @@ function sleep(ms: number): Promise<void> {
|
|
|
189
336
|
}
|
|
190
337
|
|
|
191
338
|
/**
|
|
192
|
-
* Kill every process whose command line matches `pattern` (
|
|
193
|
-
* skipping our own PID and any already-killed. Returns the count
|
|
194
|
-
* as a fallback so orphaned gate/cloudflared processes get cleaned
|
|
195
|
-
* 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).
|
|
196
344
|
*/
|
|
197
345
|
function killByPattern(pattern: string, alreadyKilled: Set<number>): number {
|
|
198
|
-
const r = spawnSync("pgrep", ["-f", pattern], { encoding: "utf-8" });
|
|
199
|
-
if (r.status !== 0 || typeof r.stdout !== "string") return 0;
|
|
200
346
|
let killed = 0;
|
|
201
|
-
for (const
|
|
202
|
-
const pid = Number(line.trim());
|
|
347
|
+
for (const pid of findPidsByCommandLine(pattern)) {
|
|
203
348
|
if (!pid || pid === process.pid || alreadyKilled.has(pid)) continue;
|
|
204
349
|
try {
|
|
205
350
|
process.kill(pid);
|
|
@@ -212,63 +357,49 @@ function killByPattern(pattern: string, alreadyKilled: Set<number>): number {
|
|
|
212
357
|
return killed;
|
|
213
358
|
}
|
|
214
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
|
+
|
|
215
370
|
/**
|
|
216
371
|
* Sweep stray gate + cloudflared processes for ONE instance, identified by its
|
|
217
372
|
* gate port. Both signatures are port-scoped so tearing down one tunnel never
|
|
218
373
|
* touches another:
|
|
219
|
-
* - gate:
|
|
374
|
+
* - gate: see gatePattern (the port is on the gate's argv)
|
|
220
375
|
* - cloudflared: `--url http://localhost:<port>` (order-independent, so it
|
|
221
376
|
* matches regardless of the `--protocol http2` flag we also pass).
|
|
222
|
-
* Port boundary is guarded with `( |$)` so port 9001 doesn't match 90011.
|
|
223
377
|
*/
|
|
224
378
|
function sweepStrays(gatePort: number, alreadyKilled: Set<number>): number {
|
|
225
379
|
return (
|
|
226
|
-
killByPattern(
|
|
380
|
+
killByPattern(gatePattern(gatePort), alreadyKilled) +
|
|
227
381
|
killByPattern(`--url http://localhost:${gatePort}( |$)`, alreadyKilled)
|
|
228
382
|
);
|
|
229
383
|
}
|
|
230
384
|
|
|
231
|
-
/**
|
|
232
|
-
* Ports currently bound by a LISTEN socket (best-effort, platform-dependent).
|
|
233
|
-
*
|
|
234
|
-
* `ss` (iproute2) is Linux-only. Without the lsof fallback this returns an empty
|
|
235
|
-
* set on macOS/BSD. That does not fail loudly. It reports every port as free, so
|
|
236
|
-
* `allocateGatePort` can hand out an occupied port and the reload port-release
|
|
237
|
-
* check becomes inert.
|
|
238
|
-
*/
|
|
239
|
-
function listeningPorts(): Set<number> {
|
|
240
|
-
const ports = new Set<number>();
|
|
241
|
-
const ss = spawnSync("ss", ["-tlnH"], { encoding: "utf-8" });
|
|
242
|
-
if (ss.status === 0 && typeof ss.stdout === "string") {
|
|
243
|
-
for (const m of ss.stdout.matchAll(/:(\d+)\s/g)) ports.add(Number(m[1]));
|
|
244
|
-
return ports;
|
|
245
|
-
}
|
|
246
|
-
// Rows look like: `bun 123 user 4u IPv4 0x… 0t0 TCP 127.0.0.1:58055 (LISTEN)`.
|
|
247
|
-
// Status can be nonzero while stdout still holds usable rows (lsof reports a
|
|
248
|
-
// failure when any single process is unreadable), so judge it on the output.
|
|
249
|
-
const lsof = spawnSync("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN"], { encoding: "utf-8" });
|
|
250
|
-
if (typeof lsof.stdout === "string") {
|
|
251
|
-
for (const m of lsof.stdout.matchAll(/:(\d+) \(LISTEN\)$/gm)) ports.add(Number(m[1]));
|
|
252
|
-
}
|
|
253
|
-
return ports;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
385
|
/**
|
|
257
386
|
* Pick a gate port for a new instance. An explicit `--gate-port` is honored
|
|
258
387
|
* (and rejected if it's already taken); otherwise scan upward from 9001 for the
|
|
259
388
|
* first port that's neither held by a live instance nor currently listening.
|
|
260
389
|
*/
|
|
261
|
-
function allocateGatePort(preferred: number | undefined): number {
|
|
390
|
+
async function allocateGatePort(preferred: number | undefined): Promise<number> {
|
|
262
391
|
const used = new Set<number>(
|
|
263
392
|
listStates()
|
|
264
393
|
.filter((s) => isProcessAlive(s.gate_pid))
|
|
265
394
|
.map((s) => s.gate_port),
|
|
266
395
|
);
|
|
396
|
+
// No discovery tool (null) is not "every port free": probe candidates by connecting.
|
|
267
397
|
const listening = listeningPorts();
|
|
268
|
-
const taken = (p: number) =>
|
|
398
|
+
const taken = async (p: number) =>
|
|
399
|
+
used.has(p) || (listening ? listening.has(p) : await connectProbe(p));
|
|
269
400
|
|
|
270
401
|
if (preferred !== undefined) {
|
|
271
|
-
if (taken(preferred)) {
|
|
402
|
+
if (await taken(preferred)) {
|
|
272
403
|
emit.error({
|
|
273
404
|
code: "tunnel_port_taken",
|
|
274
405
|
message: `Gate port ${preferred} is already in use. Omit --gate-port to auto-allocate, or pick a free one.`,
|
|
@@ -278,7 +409,7 @@ function allocateGatePort(preferred: number | undefined): number {
|
|
|
278
409
|
return preferred;
|
|
279
410
|
}
|
|
280
411
|
for (let p = DEFAULT_GATE_PORT; p <= MAX_GATE_PORT; p++) {
|
|
281
|
-
if (!taken(p)) return p;
|
|
412
|
+
if (!(await taken(p))) return p;
|
|
282
413
|
}
|
|
283
414
|
emit.error({
|
|
284
415
|
code: "tunnel_no_free_port",
|
|
@@ -444,6 +575,7 @@ interface GateSpawnOpts {
|
|
|
444
575
|
vhost: string;
|
|
445
576
|
provider: TunnelProvider;
|
|
446
577
|
allowedIps: string[];
|
|
578
|
+
allowPaths: string[];
|
|
447
579
|
gateLogPath: string;
|
|
448
580
|
}
|
|
449
581
|
|
|
@@ -455,13 +587,14 @@ interface GateSpawnOpts {
|
|
|
455
587
|
* is passed as an env var, which the gate snapshots at module load and never
|
|
456
588
|
* re-reads — that snapshot is precisely why `reload` has to exist.
|
|
457
589
|
*/
|
|
458
|
-
function spawnGate(o: GateSpawnOpts): ChildProcess {
|
|
590
|
+
function spawnGate(o: GateSpawnOpts, launch: GateLaunch): ChildProcess {
|
|
459
591
|
// `--name`/`--port` on argv mirror the env vars; they're what makes the gate
|
|
460
|
-
// process distinguishable per-instance in
|
|
592
|
+
// process distinguishable per-instance in a command-line search (see sweepStrays).
|
|
461
593
|
const gateProc = spawnRotatingProcess({
|
|
462
594
|
path: o.gateLogPath,
|
|
463
|
-
command:
|
|
464
|
-
arguments: [
|
|
595
|
+
command: launch.command,
|
|
596
|
+
arguments: [...launch.arguments, "--name", o.name, "--port", String(o.gatePort)],
|
|
597
|
+
worker: currentWorkerLaunch(),
|
|
465
598
|
env: {
|
|
466
599
|
...process.env,
|
|
467
600
|
HARNERY_TUNNEL_ALLOW: o.allowedIps.join(","),
|
|
@@ -470,6 +603,7 @@ function spawnGate(o: GateSpawnOpts): ChildProcess {
|
|
|
470
603
|
HARNERY_TUNNEL_TARGET: o.target,
|
|
471
604
|
HARNERY_TUNNEL_VHOST: o.vhost,
|
|
472
605
|
HARNERY_TUNNEL_PORT: String(o.gatePort),
|
|
606
|
+
[ALLOW_PATHS_ENV]: o.allowPaths.join(","),
|
|
473
607
|
},
|
|
474
608
|
});
|
|
475
609
|
gateProc.unref();
|
|
@@ -477,13 +611,9 @@ function spawnGate(o: GateSpawnOpts): ChildProcess {
|
|
|
477
611
|
}
|
|
478
612
|
|
|
479
613
|
async function up(opts: UpOpts): Promise<void> {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
message:
|
|
484
|
-
"harn tunnel requires Bun: the gate worker is a Bun.serve process. " +
|
|
485
|
-
"Install Bun (https://bun.sh) and re-run. (Every other harn command runs on Node.)",
|
|
486
|
-
});
|
|
614
|
+
const launch = currentGateLaunch();
|
|
615
|
+
if (!launch) {
|
|
616
|
+
emit.error({ code: "tunnel_requires_bun", message: requiresBunMessage() });
|
|
487
617
|
process.exit(1);
|
|
488
618
|
}
|
|
489
619
|
const name = resolveName(opts.name);
|
|
@@ -498,6 +628,7 @@ async function up(opts: UpOpts): Promise<void> {
|
|
|
498
628
|
provider === "tailscale" ? resolveTailscalePath(opts.path, name) : undefined;
|
|
499
629
|
const tailscaleHttpsPort =
|
|
500
630
|
provider === "tailscale" ? resolveHttpsPort(opts.httpsPort) : undefined;
|
|
631
|
+
const allowPaths = resolveAllowPaths(opts.allowPath);
|
|
501
632
|
|
|
502
633
|
const existing = readState(name);
|
|
503
634
|
if (existing && tunnelIsAlive(existing)) {
|
|
@@ -508,16 +639,26 @@ async function up(opts: UpOpts): Promise<void> {
|
|
|
508
639
|
});
|
|
509
640
|
process.exit(1);
|
|
510
641
|
}
|
|
642
|
+
if (!samePaths(existing.allow_paths, allowPaths)) {
|
|
643
|
+
emit.error({
|
|
644
|
+
code: "tunnel_instance_in_use",
|
|
645
|
+
message:
|
|
646
|
+
`Tunnel [${name}] is already up sharing ${formatPaths(existing.allow_paths)}. ` +
|
|
647
|
+
`Stop it before starting it with ${formatPaths(allowPaths)}.`,
|
|
648
|
+
});
|
|
649
|
+
process.exit(1);
|
|
650
|
+
}
|
|
511
651
|
emit.text(`Already up [${name}]: ${existing.url}\n`);
|
|
512
652
|
emit.text(` Provider: ${existing.provider}\n`);
|
|
513
653
|
emit.text(` Forwarding: ${existing.target} (Host: ${existing.vhost})\n`);
|
|
654
|
+
emit.text(` Paths: ${formatPaths(existing.allow_paths)}\n`);
|
|
514
655
|
return;
|
|
515
656
|
}
|
|
516
657
|
if (existing) downOne(name, new Set());
|
|
517
658
|
|
|
518
659
|
// Allocate the gate port (after clearing dead state so its old port frees up
|
|
519
660
|
// for reuse). Explicit --gate-port is validated; otherwise auto-scan.
|
|
520
|
-
const gatePort = allocateGatePort(opts.gatePort ? Number(opts.gatePort) : undefined);
|
|
661
|
+
const gatePort = await allocateGatePort(opts.gatePort ? Number(opts.gatePort) : undefined);
|
|
521
662
|
|
|
522
663
|
// Self-heal: clear any orphaned gate/cloudflared on THIS instance's port from
|
|
523
664
|
// a prior crashed or state-cleared run so the gate port is free before we bind.
|
|
@@ -527,12 +668,23 @@ async function up(opts: UpOpts): Promise<void> {
|
|
|
527
668
|
if (provider === "cloudflare" && cfg.allowed_ips.length === 0) {
|
|
528
669
|
emit.error({
|
|
529
670
|
code: "tunnel_allowlist_empty",
|
|
530
|
-
message:
|
|
671
|
+
message: `Allowlist is empty; refusing to start. Add an IP first: ${resolveBinName()} tunnel allow add <ip> (or --current)`,
|
|
531
672
|
});
|
|
532
673
|
process.exit(1);
|
|
533
674
|
}
|
|
534
675
|
|
|
535
|
-
|
|
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
|
+
}
|
|
536
688
|
if (provider === "tailscale" && !tailscaleAvailable()) {
|
|
537
689
|
emit.error({
|
|
538
690
|
code: "tunnel_tailscale_missing",
|
|
@@ -545,19 +697,31 @@ async function up(opts: UpOpts): Promise<void> {
|
|
|
545
697
|
const gateLogPath = logPaths.gate;
|
|
546
698
|
const providerLogPath = logPaths.provider;
|
|
547
699
|
|
|
548
|
-
const gateProc = spawnGate(
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
+
);
|
|
557
713
|
|
|
558
714
|
await sleep(800);
|
|
559
715
|
|
|
560
|
-
|
|
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());
|
|
561
725
|
emit.error({
|
|
562
726
|
code: "tunnel_gate_failed",
|
|
563
727
|
message: `Gate failed to start. Check log: ${gateLogPath}`,
|
|
@@ -579,6 +743,7 @@ async function up(opts: UpOpts): Promise<void> {
|
|
|
579
743
|
path: providerLogPath,
|
|
580
744
|
command: cloudflaredBin!,
|
|
581
745
|
arguments: ["tunnel", "--protocol", "http2", "--url", `http://localhost:${gatePort}`],
|
|
746
|
+
worker: currentWorkerLaunch(),
|
|
582
747
|
});
|
|
583
748
|
cfdProc.unref();
|
|
584
749
|
cloudflaredPid = cfdProc.pid!;
|
|
@@ -630,18 +795,22 @@ async function up(opts: UpOpts): Promise<void> {
|
|
|
630
795
|
target,
|
|
631
796
|
vhost,
|
|
632
797
|
gate_port: gatePort,
|
|
798
|
+
allow_paths: allowPaths,
|
|
633
799
|
tailscale_mode: tailscaleMode,
|
|
634
800
|
tailscale_path: tailscalePath,
|
|
635
801
|
tailscale_https_port: tailscaleHttpsPort,
|
|
636
802
|
};
|
|
637
803
|
writeState(state);
|
|
804
|
+
recordTunnelServer(state);
|
|
638
805
|
|
|
806
|
+
const bin = resolveBinName();
|
|
639
807
|
const stopHint =
|
|
640
|
-
name === DEFAULT_INSTANCE ?
|
|
808
|
+
name === DEFAULT_INSTANCE ? `${bin} tunnel down` : `${bin} tunnel down --name ${name}`;
|
|
641
809
|
emit.text(`\n Instance: ${name}\n`);
|
|
642
810
|
emit.text(` Provider: ${provider}${tailscaleMode ? ` (${tailscaleMode})` : ""}\n`);
|
|
643
811
|
emit.text(` URL: ${url}\n\n`);
|
|
644
812
|
emit.text(` Forwarding: ${target} (Host: ${vhost})\n`);
|
|
813
|
+
emit.text(` Paths: ${formatPaths(allowPaths)}\n`);
|
|
645
814
|
emit.text(` Gate port: ${gatePort}\n`);
|
|
646
815
|
if (provider === "cloudflare") {
|
|
647
816
|
emit.text(` Allowed IPs: ${cfg.allowed_ips.join(", ")}\n\n`);
|
|
@@ -655,15 +824,31 @@ async function up(opts: UpOpts): Promise<void> {
|
|
|
655
824
|
}
|
|
656
825
|
if (!registered) {
|
|
657
826
|
emit.text(
|
|
658
|
-
` ⚠ 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`,
|
|
659
828
|
);
|
|
660
829
|
}
|
|
661
830
|
emit.text(` Stop: ${stopHint}\n`);
|
|
662
|
-
emit.text(
|
|
831
|
+
emit.text(` Status: ${bin} tunnel status\n`);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/** The server registry lists tunnels beside every other local server. */
|
|
835
|
+
function registryRoot(): string {
|
|
836
|
+
return resolveCoordRoot() ?? process.cwd();
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function recordTunnelServer(state: TunnelState): void {
|
|
840
|
+
const coordRoot = registryRoot();
|
|
841
|
+
tryRegisterServer(tunnelServerInput(state, coordRoot), { coordRoot });
|
|
663
842
|
}
|
|
664
843
|
|
|
665
844
|
/** Tear down a single instance by name. Returns the number of processes killed. */
|
|
666
|
-
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 {
|
|
667
852
|
const before = killed.size;
|
|
668
853
|
const state = readState(name);
|
|
669
854
|
if (state) {
|
|
@@ -681,9 +866,13 @@ function downOne(name: string, killed: Set<number>): number {
|
|
|
681
866
|
}
|
|
682
867
|
}
|
|
683
868
|
// Fallback: sweep orphans on this instance's gate port, even when state was
|
|
684
|
-
// lost; they'd otherwise squat on the port and break the next `up`.
|
|
685
|
-
|
|
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);
|
|
686
874
|
clearState(name);
|
|
875
|
+
unregisterServer(tunnelServerId(name), { coordRoot: registryRoot() });
|
|
687
876
|
return killed.size - before;
|
|
688
877
|
}
|
|
689
878
|
|
|
@@ -710,7 +899,7 @@ function down(opts: DownOpts): void {
|
|
|
710
899
|
const others = listStates();
|
|
711
900
|
if (others.length > 0) {
|
|
712
901
|
emit.text(
|
|
713
|
-
`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`,
|
|
714
903
|
);
|
|
715
904
|
return;
|
|
716
905
|
}
|
|
@@ -728,20 +917,20 @@ function down(opts: DownOpts): void {
|
|
|
728
917
|
async function waitForPortFree(port: number, timeoutMs: number): Promise<boolean> {
|
|
729
918
|
const deadline = Date.now() + timeoutMs;
|
|
730
919
|
while (Date.now() < deadline) {
|
|
731
|
-
if (!
|
|
920
|
+
if (!(await portListening(port))) return true;
|
|
732
921
|
await sleep(100);
|
|
733
922
|
}
|
|
734
|
-
return !
|
|
923
|
+
return !(await portListening(port));
|
|
735
924
|
}
|
|
736
925
|
|
|
737
926
|
/** Poll until `port` has a LISTEN socket (or the deadline passes). */
|
|
738
927
|
async function waitForPortBound(port: number, timeoutMs: number): Promise<boolean> {
|
|
739
928
|
const deadline = Date.now() + timeoutMs;
|
|
740
929
|
while (Date.now() < deadline) {
|
|
741
|
-
if (
|
|
930
|
+
if (await portListening(port)) return true;
|
|
742
931
|
await sleep(100);
|
|
743
932
|
}
|
|
744
|
-
return
|
|
933
|
+
return portListening(port);
|
|
745
934
|
}
|
|
746
935
|
|
|
747
936
|
/**
|
|
@@ -768,8 +957,21 @@ export async function reloadOne(state: TunnelState): Promise<{ ok: boolean; mess
|
|
|
768
957
|
`Reloading the gate can't bring it back — run \`${bin} tunnel up --name ${name}\`.`,
|
|
769
958
|
};
|
|
770
959
|
}
|
|
771
|
-
|
|
772
|
-
|
|
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
|
+
};
|
|
966
|
+
}
|
|
967
|
+
if (state.allow_paths.length === 0) {
|
|
968
|
+
return {
|
|
969
|
+
ok: false,
|
|
970
|
+
message:
|
|
971
|
+
`[${name}] has no recorded path scope, so a reloaded gate would refuse every request. ` +
|
|
972
|
+
`Restart it with \`${bin} tunnel down --name ${name}\` and ` +
|
|
973
|
+
`\`${bin} tunnel up --name ${name} --allow-path <prefix>\`.`,
|
|
974
|
+
};
|
|
773
975
|
}
|
|
774
976
|
|
|
775
977
|
// Kill ONLY the gate. sweepStrays() is deliberately avoided here: it also
|
|
@@ -784,7 +986,7 @@ export async function reloadOne(state: TunnelState): Promise<{ ok: boolean; mess
|
|
|
784
986
|
/* race: already gone */
|
|
785
987
|
}
|
|
786
988
|
}
|
|
787
|
-
killByPattern(
|
|
989
|
+
killByPattern(gatePattern(gatePort), killed);
|
|
788
990
|
|
|
789
991
|
if (!(await waitForPortFree(gatePort, 5_000))) {
|
|
790
992
|
return {
|
|
@@ -796,22 +998,26 @@ export async function reloadOne(state: TunnelState): Promise<{ ok: boolean; mess
|
|
|
796
998
|
}
|
|
797
999
|
|
|
798
1000
|
const cfg = readConfig();
|
|
799
|
-
const gateProc = spawnGate(
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
+
);
|
|
808
1014
|
|
|
809
1015
|
await sleep(800);
|
|
810
1016
|
const pid = gateProc.pid;
|
|
811
1017
|
if (
|
|
812
1018
|
typeof pid !== "number" ||
|
|
813
1019
|
!isProcessAlive(pid) ||
|
|
814
|
-
!(await waitForPortBound(gatePort,
|
|
1020
|
+
!(await waitForPortBound(gatePort, 15_000))
|
|
815
1021
|
) {
|
|
816
1022
|
return {
|
|
817
1023
|
ok: false,
|
|
@@ -822,6 +1028,7 @@ export async function reloadOne(state: TunnelState): Promise<{ ok: boolean; mess
|
|
|
822
1028
|
}
|
|
823
1029
|
|
|
824
1030
|
writeState({ ...state, gate_pid: pid });
|
|
1031
|
+
recordTunnelServer({ ...state, gate_pid: pid });
|
|
825
1032
|
const allowNote =
|
|
826
1033
|
state.provider === "cloudflare" ? ` Allowlist: ${cfg.allowed_ips.length} IP(s).` : "";
|
|
827
1034
|
return { ok: true, message: `[${name}] gate reloaded, URL unchanged: ${state.url}.${allowNote}` };
|
|
@@ -898,6 +1105,7 @@ function statusDetail(state: TunnelState): void {
|
|
|
898
1105
|
);
|
|
899
1106
|
emit.text(` URL: ${state.url}\n`);
|
|
900
1107
|
emit.text(` Forwarding: ${state.target} (Host: ${state.vhost})\n`);
|
|
1108
|
+
emit.text(` Paths: ${formatPaths(state.allow_paths)}\n`);
|
|
901
1109
|
emit.text(` Gate port: ${state.gate_port}\n`);
|
|
902
1110
|
if (state.provider === "cloudflare") {
|
|
903
1111
|
emit.text(` Allowed IPs: ${cfg.allowed_ips.join(", ")}\n`);
|
|
@@ -941,6 +1149,7 @@ function status(opts: StatusOpts): void {
|
|
|
941
1149
|
state: instanceState(s),
|
|
942
1150
|
url: s.url,
|
|
943
1151
|
fwd: `${s.target} (${s.vhost})`,
|
|
1152
|
+
paths: formatPaths(s.allow_paths),
|
|
944
1153
|
port: String(s.gate_port),
|
|
945
1154
|
up: fmtUptime(s.started_at),
|
|
946
1155
|
}));
|
|
@@ -950,15 +1159,16 @@ function status(opts: StatusOpts): void {
|
|
|
950
1159
|
state: 5,
|
|
951
1160
|
url: Math.max(3, ...rows.map((r) => r.url.length)),
|
|
952
1161
|
fwd: Math.max(10, ...rows.map((r) => r.fwd.length)),
|
|
1162
|
+
paths: Math.max(5, ...rows.map((r) => r.paths.length)),
|
|
953
1163
|
port: 4,
|
|
954
1164
|
};
|
|
955
1165
|
const pad = (s: string, n: number) => s.padEnd(n);
|
|
956
1166
|
emit.text(
|
|
957
|
-
`${pad("NAME", w.name)} ${pad("PROVIDER", w.provider)} ${pad("STATE", w.state)} ${pad("URL", w.url)} ${pad("FORWARDING", w.fwd)} ${pad("PORT", w.port)} UPTIME\n`,
|
|
1167
|
+
`${pad("NAME", w.name)} ${pad("PROVIDER", w.provider)} ${pad("STATE", w.state)} ${pad("URL", w.url)} ${pad("FORWARDING", w.fwd)} ${pad("PATHS", w.paths)} ${pad("PORT", w.port)} UPTIME\n`,
|
|
958
1168
|
);
|
|
959
1169
|
for (const r of rows) {
|
|
960
1170
|
emit.text(
|
|
961
|
-
`${pad(r.name, w.name)} ${pad(r.provider, w.provider)} ${pad(r.state, w.state)} ${pad(r.url, w.url)} ${pad(r.fwd, w.fwd)} ${pad(r.port, w.port)} ${r.up}\n`,
|
|
1171
|
+
`${pad(r.name, w.name)} ${pad(r.provider, w.provider)} ${pad(r.state, w.state)} ${pad(r.url, w.url)} ${pad(r.fwd, w.fwd)} ${pad(r.paths, w.paths)} ${pad(r.port, w.port)} ${r.up}\n`,
|
|
962
1172
|
);
|
|
963
1173
|
}
|
|
964
1174
|
}
|
|
@@ -984,9 +1194,16 @@ function allowList(): void {
|
|
|
984
1194
|
emit.text("(empty)\n");
|
|
985
1195
|
return;
|
|
986
1196
|
}
|
|
987
|
-
|
|
1197
|
+
const automatic = cfg.auto_allowed ?? [];
|
|
1198
|
+
for (const ip of cfg.allowed_ips) {
|
|
1199
|
+
emit.text(`${ip}${sameEntryIn(automatic, ip) ? " (automatic)" : ""}\n`);
|
|
1200
|
+
}
|
|
988
1201
|
}
|
|
989
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
|
+
|
|
990
1207
|
/**
|
|
991
1208
|
* Trailer for `allow add`/`allow rm`. Each gate snapshots the allowlist at
|
|
992
1209
|
* spawn, so a config edit alone changes nothing until the gates restart.
|
|
@@ -1004,31 +1221,156 @@ function allowChangedHint(): void {
|
|
|
1004
1221
|
);
|
|
1005
1222
|
}
|
|
1006
1223
|
|
|
1007
|
-
|
|
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
|
+
}
|
|
1008
1258
|
const cfg = readConfig();
|
|
1009
|
-
if (cfg.allowed_ips
|
|
1010
|
-
|
|
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`);
|
|
1011
1268
|
return;
|
|
1012
1269
|
}
|
|
1013
|
-
cfg.allowed_ips.push(
|
|
1270
|
+
cfg.allowed_ips.push(canonical);
|
|
1014
1271
|
writeConfig(cfg);
|
|
1015
|
-
emit.text(`Added ${
|
|
1272
|
+
emit.text(`Added ${canonical}.\n`);
|
|
1016
1273
|
allowChangedHint();
|
|
1017
1274
|
}
|
|
1018
1275
|
|
|
1019
|
-
function allowRm(
|
|
1276
|
+
function allowRm(entry: string): void {
|
|
1020
1277
|
const cfg = readConfig();
|
|
1021
|
-
const
|
|
1278
|
+
const wanted = canonicalEntry(entry);
|
|
1279
|
+
const idx = cfg.allowed_ips.findIndex((e) => e === entry || canonicalEntry(e) === wanted);
|
|
1022
1280
|
if (idx === -1) {
|
|
1023
|
-
emit.text(`${
|
|
1281
|
+
emit.text(`${entry} not in allowlist.\n`);
|
|
1024
1282
|
return;
|
|
1025
1283
|
}
|
|
1026
|
-
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
|
+
}
|
|
1027
1290
|
writeConfig(cfg);
|
|
1028
|
-
emit.text(`Removed ${
|
|
1291
|
+
emit.text(`Removed ${removed}.\n`);
|
|
1029
1292
|
allowChangedHint();
|
|
1030
1293
|
}
|
|
1031
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
|
+
|
|
1032
1374
|
let emit: EmitContext;
|
|
1033
1375
|
let context: HarneryProgramContext | undefined;
|
|
1034
1376
|
|
|
@@ -1071,6 +1413,13 @@ export function registerTunnelCommand(
|
|
|
1071
1413
|
"Tailscale only: URL path mount (default: / for default, /<name> for named instances)",
|
|
1072
1414
|
)
|
|
1073
1415
|
.option("--https-port <port>", "Tailscale only: HTTPS listen port (default: 443)", "443")
|
|
1416
|
+
.option(
|
|
1417
|
+
"--allow-path <prefix>",
|
|
1418
|
+
"URL path prefix the tunnel shares (repeatable, required). Every other path is refused; " +
|
|
1419
|
+
"pass / to publish the whole upstream.",
|
|
1420
|
+
collectPath,
|
|
1421
|
+
[],
|
|
1422
|
+
)
|
|
1074
1423
|
.action(up);
|
|
1075
1424
|
|
|
1076
1425
|
cmd
|
|
@@ -1106,9 +1455,38 @@ export function registerTunnelCommand(
|
|
|
1106
1455
|
|
|
1107
1456
|
const allow = cmd
|
|
1108
1457
|
.command("allow")
|
|
1109
|
-
.description(
|
|
1458
|
+
.description(
|
|
1459
|
+
"Manage the Cloudflare CF-Connecting-IP allowlist (addresses and CIDR ranges, IPv4 and IPv6)",
|
|
1460
|
+
)
|
|
1110
1461
|
.action(allowList);
|
|
1111
|
-
allow
|
|
1112
|
-
|
|
1113
|
-
|
|
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);
|
|
1114
1492
|
}
|