localpreview 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -2
- package/dist/cli-ui.d.ts +22 -0
- package/dist/cli-ui.d.ts.map +1 -0
- package/dist/cli-ui.js +304 -0
- package/dist/command.d.ts.map +1 -1
- package/dist/command.js +199 -38
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -3
- package/dist/control-plane.d.ts +11 -1
- package/dist/control-plane.d.ts.map +1 -1
- package/dist/control-plane.js +56 -1
- package/dist/index.js +2 -1
- package/dist/local-proxy.d.ts.map +1 -1
- package/dist/local-proxy.js +4 -3
- package/dist/relay-client.d.ts.map +1 -1
- package/dist/relay-client.js +3 -2
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/package.json +17 -18
package/README.md
CHANGED
|
@@ -8,8 +8,9 @@ server to a public URL.
|
|
|
8
8
|
```sh
|
|
9
9
|
localpreview connect 3000
|
|
10
10
|
localpreview connect https://localhost:3000
|
|
11
|
-
localpreview connect 3000 --name
|
|
11
|
+
localpreview connect 3000 --name my-app
|
|
12
12
|
localpreview connect 3000 -l
|
|
13
|
+
localpreview connect 5173 --capture localhost:4000
|
|
13
14
|
```
|
|
14
15
|
|
|
15
16
|
`localpreview <target>` is still accepted as a deprecated compatibility alias and
|
|
@@ -21,6 +22,38 @@ Targets are parsed by `@localpreview/protocol`:
|
|
|
21
22
|
- URLs must use `http` or `https`.
|
|
22
23
|
- URL targets must include an explicit port.
|
|
23
24
|
|
|
25
|
+
Run `localpreview -h` for public help. When `LOCALPREVIEW_ADMIN_TOKEN` is set in
|
|
26
|
+
the environment, global help also lists admin commands.
|
|
27
|
+
|
|
28
|
+
## Admin commands
|
|
29
|
+
|
|
30
|
+
Subdomain cleanup is an admin-only operation. Export `LOCALPREVIEW_ADMIN_TOKEN`
|
|
31
|
+
before running `clean`:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
export LOCALPREVIEW_ADMIN_TOKEN=your-token
|
|
35
|
+
localpreview clean my-app --force
|
|
36
|
+
localpreview clean --all --force -l
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Admin authorization is read from the environment only.
|
|
40
|
+
|
|
41
|
+
`localpreview clean -h` and `localpreview list -h` show full admin help only when
|
|
42
|
+
`LOCALPREVIEW_ADMIN_TOKEN` is set. Without it, you get public help plus a note
|
|
43
|
+
that admin commands require the variable.
|
|
44
|
+
|
|
45
|
+
Inspect active tunnels, Redis inventory drift, and sandbox leftovers without
|
|
46
|
+
printing tokens or preview URLs:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
localpreview list
|
|
50
|
+
localpreview list --limit 20 --skip 20 -l
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The list view prints a compact summary, page details, and a practical table with
|
|
54
|
+
subdomain, short tunnel/sandbox ids, age, relay health, sandbox status, and the
|
|
55
|
+
control-plane note for each row.
|
|
56
|
+
|
|
24
57
|
## Runtime Flow
|
|
25
58
|
|
|
26
59
|
1. The CLI validates the target and optional `--name`.
|
|
@@ -42,6 +75,7 @@ browser -> control-plane -> relay -> CLI WebSocket -> local target
|
|
|
42
75
|
The package is structured as small Effect-backed services:
|
|
43
76
|
|
|
44
77
|
- `src/command.ts`: command parsing, legacy alias handling, and top-level flow.
|
|
78
|
+
- `src/cli-ui.ts`: help text, terminal styling, and log formatting.
|
|
45
79
|
- `src/config.ts`: runtime defaults and environment-derived limits.
|
|
46
80
|
- `src/control-plane.ts`: tunnel create/delete HTTP adapter.
|
|
47
81
|
- `src/relay-client.ts`: WebSocket lifecycle, signals, and relay event handling.
|
|
@@ -55,7 +89,8 @@ Effect.
|
|
|
55
89
|
## Defaults and Limits
|
|
56
90
|
|
|
57
91
|
- Control-plane URL precedence:
|
|
58
|
-
`-l
|
|
92
|
+
`-l` / `--local` (shorthand for `--control-plane http://localhost:3000`) >
|
|
93
|
+
`https://localpreview.dev`
|
|
59
94
|
- Request body limit: `10 MB`
|
|
60
95
|
- Response body limit: `50 MB`
|
|
61
96
|
- Local request timeout: `30 seconds`
|
|
@@ -66,6 +101,7 @@ Effect.
|
|
|
66
101
|
Optional environment overrides:
|
|
67
102
|
|
|
68
103
|
```sh
|
|
104
|
+
LOCALPREVIEW_ADMIN_TOKEN=... # required for `clean` and `list`
|
|
69
105
|
LOCALPREVIEW_REQUEST_BODY_LIMIT_BYTES=10485760
|
|
70
106
|
LOCALPREVIEW_RESPONSE_BODY_LIMIT_BYTES=52428800
|
|
71
107
|
LOCALPREVIEW_REQUEST_TIMEOUT_MS=30000
|
|
@@ -110,5 +146,6 @@ For a local end-to-end run, start the relay and control-plane first:
|
|
|
110
146
|
```sh
|
|
111
147
|
pnpm dev:relay
|
|
112
148
|
pnpm dev:web
|
|
149
|
+
export LOCALPREVIEW_ADMIN_TOKEN=your-local-token
|
|
113
150
|
pnpm localpreview connect 3000 -l
|
|
114
151
|
```
|
package/dist/cli-ui.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ListTunnelsResponse } from "@localpreview/protocol";
|
|
2
|
+
export declare const ADMIN_TOKEN_ENV = "LOCALPREVIEW_ADMIN_TOKEN";
|
|
3
|
+
export declare const hasAdminTokenInEnv: (env?: NodeJS.ProcessEnv) => boolean;
|
|
4
|
+
export declare const formatWarning: (message: string) => string;
|
|
5
|
+
export declare const formatCliError: (message: string) => string;
|
|
6
|
+
export declare const formatStatus: (message: string) => string;
|
|
7
|
+
export declare const formatRelayReconnect: (message: string) => string;
|
|
8
|
+
export declare const formatRequestOutbound: (method: string, path: string) => string;
|
|
9
|
+
export declare const formatRequestInbound: (status: number, path: string, durationMs: number) => string;
|
|
10
|
+
export declare const formatRequestError: (path: string, message: string) => string;
|
|
11
|
+
export declare const removedAdminTokenFlagMessage: () => string;
|
|
12
|
+
export declare const adminTokenRequiredMessage: () => string;
|
|
13
|
+
export type HelpRenderOptions = {
|
|
14
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
15
|
+
};
|
|
16
|
+
export declare const renderGlobalHelp: (options?: HelpRenderOptions) => string;
|
|
17
|
+
export declare const renderConnectHelp: () => string;
|
|
18
|
+
export declare const renderCleanHelp: (options?: HelpRenderOptions) => string;
|
|
19
|
+
export declare const renderListHelp: (options?: HelpRenderOptions) => string;
|
|
20
|
+
export declare const formatListTunnelsOutput: (response: ListTunnelsResponse) => string;
|
|
21
|
+
export declare const formatCompactAge: (activeForMs: number | undefined) => string;
|
|
22
|
+
//# sourceMappingURL=cli-ui.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-ui.d.ts","sourceRoot":"","sources":["../src/cli-ui.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAkB,MAAM,wBAAwB,CAAC;AAIlF,eAAO,MAAM,eAAe,6BAA6B,CAAC;AAE1D,eAAO,MAAM,kBAAkB,GAAI,MAAK,MAAM,CAAC,UAAwB,KAAG,OAGzE,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,SAAS,MAAM,KAAG,MAQ/C,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,KAAG,MAQhD,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,SAAS,MAAM,KAAG,MAM9C,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,SAAS,MAAM,KAAG,MACT,CAAC;AAE/C,eAAO,MAAM,qBAAqB,GAAI,QAAQ,MAAM,EAAE,MAAM,MAAM,KAAG,MASpE,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,MAAM,EACd,MAAM,MAAM,EACZ,YAAY,MAAM,KACjB,MAWF,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,EAAE,SAAS,MAAM,KAAG,MASlE,CAAC;AAKF,eAAO,MAAM,4BAA4B,QAAO,MAA0C,CAAC;AAE3F,eAAO,MAAM,yBAAyB,QAAO,MACkD,CAAC;AAEhG,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CAClC,CAAC;AAiDF,eAAO,MAAM,gBAAgB,GAAI,UAAS,iBAAsB,KAAG,MASlE,CAAC;AAEF,eAAO,MAAM,iBAAiB,QAAO,MAuBjC,CAAC;AAEL,eAAO,MAAM,eAAe,GAAI,UAAS,iBAAsB,KAAG,MAmCjE,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,UAAS,iBAAsB,KAAG,MAiChE,CAAC;AAaF,eAAO,MAAM,uBAAuB,GAAI,UAAU,mBAAmB,KAAG,MA0DvE,CAAC;AAwCF,eAAO,MAAM,gBAAgB,GAAI,aAAa,MAAM,GAAG,SAAS,KAAG,MA0BlE,CAAC"}
|
package/dist/cli-ui.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { LOCAL_CONTROL_PLANE_URL } from "./config.js";
|
|
3
|
+
import { CLI_PACKAGE_VERSION } from "./version.js";
|
|
4
|
+
export const ADMIN_TOKEN_ENV = "LOCALPREVIEW_ADMIN_TOKEN";
|
|
5
|
+
export const hasAdminTokenInEnv = (env = process.env) => {
|
|
6
|
+
const token = env[ADMIN_TOKEN_ENV];
|
|
7
|
+
return token !== undefined && token.length > 0;
|
|
8
|
+
};
|
|
9
|
+
export const formatWarning = (message) => {
|
|
10
|
+
const prefix = "Warning:";
|
|
11
|
+
if (!pc.isColorSupported) {
|
|
12
|
+
return `${prefix} ${message}`;
|
|
13
|
+
}
|
|
14
|
+
return `${pc.yellow(prefix)} ${message}`;
|
|
15
|
+
};
|
|
16
|
+
export const formatCliError = (message) => {
|
|
17
|
+
const prefix = "Error:";
|
|
18
|
+
if (!pc.isColorSupported) {
|
|
19
|
+
return `${prefix} ${message}`;
|
|
20
|
+
}
|
|
21
|
+
return `${pc.red(prefix)} ${message}`;
|
|
22
|
+
};
|
|
23
|
+
export const formatStatus = (message) => {
|
|
24
|
+
if (!pc.isColorSupported) {
|
|
25
|
+
return message;
|
|
26
|
+
}
|
|
27
|
+
return pc.cyan(message);
|
|
28
|
+
};
|
|
29
|
+
export const formatRelayReconnect = (message) => formatWarning(`${message}; reconnecting...`);
|
|
30
|
+
export const formatRequestOutbound = (method, path) => {
|
|
31
|
+
const arrow = "→";
|
|
32
|
+
const line = `${arrow} ${method} ${path}`;
|
|
33
|
+
if (!pc.isColorSupported) {
|
|
34
|
+
return line;
|
|
35
|
+
}
|
|
36
|
+
return `${pc.dim(arrow)} ${pc.bold(method)} ${path}`;
|
|
37
|
+
};
|
|
38
|
+
export const formatRequestInbound = (status, path, durationMs) => {
|
|
39
|
+
const arrow = "←";
|
|
40
|
+
const line = `${arrow} ${status} ${path} ${durationMs}ms`;
|
|
41
|
+
if (!pc.isColorSupported) {
|
|
42
|
+
return line;
|
|
43
|
+
}
|
|
44
|
+
const statusText = status >= 500 ? pc.red(String(status)) : status >= 400 ? pc.yellow(String(status)) : pc.green(String(status));
|
|
45
|
+
return `${pc.dim(arrow)} ${statusText} ${path} ${pc.dim(`${durationMs}ms`)}`;
|
|
46
|
+
};
|
|
47
|
+
export const formatRequestError = (path, message) => {
|
|
48
|
+
const arrow = "←";
|
|
49
|
+
const line = `${arrow} error ${path} ${message}`;
|
|
50
|
+
if (!pc.isColorSupported) {
|
|
51
|
+
return line;
|
|
52
|
+
}
|
|
53
|
+
return `${pc.dim(arrow)} ${pc.red("error")} ${path} ${pc.red(message)}`;
|
|
54
|
+
};
|
|
55
|
+
const REMOVED_ADMIN_TOKEN_FLAG_MESSAGE = "The --admin-token flag was removed. Set the LOCALPREVIEW_ADMIN_TOKEN environment variable instead.";
|
|
56
|
+
export const removedAdminTokenFlagMessage = () => REMOVED_ADMIN_TOKEN_FLAG_MESSAGE;
|
|
57
|
+
export const adminTokenRequiredMessage = () => `Admin commands require ${ADMIN_TOKEN_ENV}. Export it in your shell, then rerun the command.`;
|
|
58
|
+
const heading = (text) => (pc.isColorSupported ? pc.bold(text) : text);
|
|
59
|
+
const commandName = (text) => (pc.isColorSupported ? pc.cyan(text) : text);
|
|
60
|
+
const flagName = (text) => (pc.isColorSupported ? pc.yellow(text) : text);
|
|
61
|
+
const dim = (text) => (pc.isColorSupported ? pc.dim(text) : text);
|
|
62
|
+
const localFlagHelpLine = () => ` ${flagName("-l")}, ${flagName("--local")} Shorthand for ${flagName("--control-plane")} ${dim(LOCAL_CONTROL_PLANE_URL)}`;
|
|
63
|
+
const joinLines = (lines) => lines.join("\n");
|
|
64
|
+
const publicUsageLines = () => [
|
|
65
|
+
heading("localpreview — expose local dev servers through a public preview URL"),
|
|
66
|
+
"",
|
|
67
|
+
heading("Usage"),
|
|
68
|
+
` ${commandName("localpreview")} ${commandName("connect")} <port|target-url> [options]`,
|
|
69
|
+
` ${commandName("localpreview")} ${flagName("--version")}`,
|
|
70
|
+
` ${commandName("localpreview")} ${flagName("-h")} | ${flagName("--help")}`,
|
|
71
|
+
"",
|
|
72
|
+
heading("Commands"),
|
|
73
|
+
` ${commandName("connect")} Start a tunnel to your local target`,
|
|
74
|
+
"",
|
|
75
|
+
heading("Global options"),
|
|
76
|
+
` ${flagName("-h")}, ${flagName("--help")} Show help`,
|
|
77
|
+
` ${flagName("--version")} Show version (${CLI_PACKAGE_VERSION})`,
|
|
78
|
+
];
|
|
79
|
+
const publicExamplesLines = () => [
|
|
80
|
+
"",
|
|
81
|
+
heading("Examples"),
|
|
82
|
+
` ${dim("localpreview connect 3000")}`,
|
|
83
|
+
` ${dim("localpreview connect 5173 --capture localhost:4000")}`,
|
|
84
|
+
` ${dim("localpreview connect https://localhost:3000 --name my-app")}`,
|
|
85
|
+
` ${dim("localpreview connect 3000 -l")}`,
|
|
86
|
+
` ${dim("localpreview connect 3000 --control-plane https://staging.localpreview.dev")}`,
|
|
87
|
+
];
|
|
88
|
+
const adminSectionLines = () => [
|
|
89
|
+
"",
|
|
90
|
+
heading("Admin commands"),
|
|
91
|
+
` ${dim(`Requires ${ADMIN_TOKEN_ENV} in the environment.`)}`,
|
|
92
|
+
` ${commandName("clean")} Remove subdomains from the control plane`,
|
|
93
|
+
` ${commandName("list")} List active tunnels and inventory orphans`,
|
|
94
|
+
];
|
|
95
|
+
export const renderGlobalHelp = (options = {}) => {
|
|
96
|
+
const env = options.env ?? process.env;
|
|
97
|
+
const lines = [...publicUsageLines(), ...publicExamplesLines()];
|
|
98
|
+
if (hasAdminTokenInEnv(env)) {
|
|
99
|
+
lines.push(...adminSectionLines());
|
|
100
|
+
}
|
|
101
|
+
return joinLines(lines);
|
|
102
|
+
};
|
|
103
|
+
export const renderConnectHelp = () => joinLines([
|
|
104
|
+
heading("localpreview connect — start a tunnel to a local target"),
|
|
105
|
+
"",
|
|
106
|
+
heading("Usage"),
|
|
107
|
+
` ${commandName("localpreview")} ${commandName("connect")} <port|target-url> [options]`,
|
|
108
|
+
` ${commandName("localpreview")} <port|target-url> [options] ${dim("(deprecated alias)")}`,
|
|
109
|
+
"",
|
|
110
|
+
heading("Arguments"),
|
|
111
|
+
` <port|target-url> Local port (e.g. 3000) or full http(s) URL with explicit port`,
|
|
112
|
+
"",
|
|
113
|
+
heading("Options"),
|
|
114
|
+
` ${flagName("--name")} <subdomain> Request a public subdomain`,
|
|
115
|
+
` ${flagName("--capture")} <host:port> Expose an extra loopback backend through the preview URL`,
|
|
116
|
+
localFlagHelpLine(),
|
|
117
|
+
` ${flagName("--control-plane")} <url> Remote control plane base URL (HTTPS required off localhost)`,
|
|
118
|
+
` ${flagName("-h")}, ${flagName("--help")} Show this help`,
|
|
119
|
+
"",
|
|
120
|
+
heading("Examples"),
|
|
121
|
+
` ${dim("localpreview connect 3000")}`,
|
|
122
|
+
` ${dim("localpreview connect 5173 --capture localhost:4000")}`,
|
|
123
|
+
` ${dim("localpreview connect https://localhost:3000 --name my-app")}`,
|
|
124
|
+
` ${dim("localpreview connect 3000 -l")}`,
|
|
125
|
+
]);
|
|
126
|
+
export const renderCleanHelp = (options = {}) => {
|
|
127
|
+
const env = options.env ?? process.env;
|
|
128
|
+
if (!hasAdminTokenInEnv(env)) {
|
|
129
|
+
return joinLines([
|
|
130
|
+
renderGlobalHelp(options),
|
|
131
|
+
"",
|
|
132
|
+
adminTokenRequiredMessage(),
|
|
133
|
+
]);
|
|
134
|
+
}
|
|
135
|
+
return joinLines([
|
|
136
|
+
heading("localpreview clean — admin subdomain cleanup"),
|
|
137
|
+
"",
|
|
138
|
+
heading("Usage"),
|
|
139
|
+
` ${commandName("localpreview")} ${commandName("clean")} <subdomain> [options]`,
|
|
140
|
+
` ${commandName("localpreview")} ${commandName("clean")} ${flagName("--all")} ${flagName("--force")} [options]`,
|
|
141
|
+
"",
|
|
142
|
+
heading("Arguments"),
|
|
143
|
+
` <subdomain> Subdomain to remove from the control plane`,
|
|
144
|
+
"",
|
|
145
|
+
heading("Options"),
|
|
146
|
+
` ${flagName("--force")} Force cleanup when relays are still connected`,
|
|
147
|
+
` ${flagName("--all")} Bulk cleanup (requires ${flagName("--force")})`,
|
|
148
|
+
localFlagHelpLine(),
|
|
149
|
+
` ${flagName("--control-plane")} <url> Control plane base URL`,
|
|
150
|
+
` ${flagName("-h")}, ${flagName("--help")} Show this help`,
|
|
151
|
+
"",
|
|
152
|
+
dim(`Authorization uses ${ADMIN_TOKEN_ENV} from the environment.`),
|
|
153
|
+
"",
|
|
154
|
+
heading("Examples"),
|
|
155
|
+
` ${dim("localpreview clean my-app --force")}`,
|
|
156
|
+
` ${dim("localpreview clean --all --force -l")}`,
|
|
157
|
+
` ${dim("localpreview clean demo --control-plane https://staging.localpreview.dev --force")}`,
|
|
158
|
+
]);
|
|
159
|
+
};
|
|
160
|
+
export const renderListHelp = (options = {}) => {
|
|
161
|
+
const env = options.env ?? process.env;
|
|
162
|
+
if (!hasAdminTokenInEnv(env)) {
|
|
163
|
+
return joinLines([
|
|
164
|
+
renderGlobalHelp(options),
|
|
165
|
+
"",
|
|
166
|
+
adminTokenRequiredMessage(),
|
|
167
|
+
]);
|
|
168
|
+
}
|
|
169
|
+
return joinLines([
|
|
170
|
+
heading("localpreview list — inspect tunnel inventory"),
|
|
171
|
+
"",
|
|
172
|
+
heading("Usage"),
|
|
173
|
+
` ${commandName("localpreview")} ${commandName("list")} [options]`,
|
|
174
|
+
"",
|
|
175
|
+
dim("Shows active tunnels, Redis inventory drift, and sandbox leftovers without revealing tokens or preview URLs."),
|
|
176
|
+
"",
|
|
177
|
+
heading("Options"),
|
|
178
|
+
` ${flagName("--limit")} <n> Page size (default 100)`,
|
|
179
|
+
` ${flagName("--skip")} <n> Offset into the sorted inventory (default 0)`,
|
|
180
|
+
localFlagHelpLine(),
|
|
181
|
+
` ${flagName("--control-plane")} <url> Control plane base URL`,
|
|
182
|
+
` ${flagName("-h")}, ${flagName("--help")} Show this help`,
|
|
183
|
+
"",
|
|
184
|
+
dim(`Authorization uses ${ADMIN_TOKEN_ENV} from the environment.`),
|
|
185
|
+
"",
|
|
186
|
+
heading("Examples"),
|
|
187
|
+
` ${dim("localpreview list")}`,
|
|
188
|
+
` ${dim("localpreview list --limit 20 --skip 20 -l")}`,
|
|
189
|
+
` ${dim("localpreview list --control-plane <control-plane-url>")}`,
|
|
190
|
+
]);
|
|
191
|
+
};
|
|
192
|
+
const LIST_COLUMNS = [
|
|
193
|
+
"kind",
|
|
194
|
+
"subdomain",
|
|
195
|
+
"tunnel",
|
|
196
|
+
"age",
|
|
197
|
+
"relay",
|
|
198
|
+
"sandbox",
|
|
199
|
+
"sandbox id",
|
|
200
|
+
"note",
|
|
201
|
+
];
|
|
202
|
+
export const formatListTunnelsOutput = (response) => {
|
|
203
|
+
const lines = [];
|
|
204
|
+
lines.push(heading("Tunnel inventory"));
|
|
205
|
+
lines.push("");
|
|
206
|
+
for (const warning of response.warnings ?? []) {
|
|
207
|
+
lines.push(formatWarning(warning));
|
|
208
|
+
}
|
|
209
|
+
if ((response.warnings?.length ?? 0) > 0) {
|
|
210
|
+
lines.push("");
|
|
211
|
+
}
|
|
212
|
+
lines.push(heading("Summary"));
|
|
213
|
+
lines.push(` tracked: ${response.counts.tracked} redis orphans: ${response.counts["redis-orphan"]} sandbox orphans: ${response.counts["sandbox-orphan"]}`);
|
|
214
|
+
lines.push("");
|
|
215
|
+
lines.push(heading("Page"));
|
|
216
|
+
lines.push(` showing: ${response.items.length} of ${response.total} skip: ${response.skip} limit: ${response.limit}`);
|
|
217
|
+
const nextSkip = response.skip + response.limit;
|
|
218
|
+
if (nextSkip < response.total) {
|
|
219
|
+
lines.push(` next: localpreview list --skip ${nextSkip} --limit ${response.limit}`);
|
|
220
|
+
}
|
|
221
|
+
if (response.items.length === 0) {
|
|
222
|
+
lines.push("");
|
|
223
|
+
lines.push(dim("No active tunnels or inventory orphans on this page."));
|
|
224
|
+
return joinLines(lines);
|
|
225
|
+
}
|
|
226
|
+
const rows = response.items.map(formatListTunnelRow);
|
|
227
|
+
const widths = LIST_COLUMNS.map((column, index) => Math.max(column.length, ...rows.map((row) => row[index]?.length ?? 0)));
|
|
228
|
+
lines.push("");
|
|
229
|
+
lines.push(heading("Items"));
|
|
230
|
+
lines.push(LIST_COLUMNS.map((column, index) => column.padEnd(widths[index] ?? column.length)).join(" "));
|
|
231
|
+
lines.push(widths.map((width) => "-".repeat(width)).join(" "));
|
|
232
|
+
for (const row of rows) {
|
|
233
|
+
lines.push(row
|
|
234
|
+
.map((cell, index) => cell.padEnd(widths[index] ?? cell.length))
|
|
235
|
+
.join(" "));
|
|
236
|
+
}
|
|
237
|
+
if (response.items.some((item) => item.relayHealthWarning !== undefined && item.relayHealthWarning.length > 0)) {
|
|
238
|
+
lines.push("");
|
|
239
|
+
lines.push(dim("Relay values ending in ! include a health warning from the control plane."));
|
|
240
|
+
}
|
|
241
|
+
return joinLines(lines);
|
|
242
|
+
};
|
|
243
|
+
const formatListTunnelRow = (item) => [
|
|
244
|
+
formatListItemKind(item.kind),
|
|
245
|
+
item.subdomain ?? "-",
|
|
246
|
+
formatShortId(item.tunnelId),
|
|
247
|
+
formatCompactAge(item.activeForMs),
|
|
248
|
+
formatRelayHealth(item),
|
|
249
|
+
item.sandboxStatus ?? "-",
|
|
250
|
+
formatShortId(item.sandboxId),
|
|
251
|
+
item.reason ?? item.orphanReason ?? "-",
|
|
252
|
+
];
|
|
253
|
+
const formatListItemKind = (kind) => {
|
|
254
|
+
switch (kind) {
|
|
255
|
+
case "tracked-tunnel":
|
|
256
|
+
return "tracked";
|
|
257
|
+
case "redis-orphan":
|
|
258
|
+
return "redis-orphan";
|
|
259
|
+
case "redis-index-orphan":
|
|
260
|
+
return "redis-index";
|
|
261
|
+
case "sandbox-orphan":
|
|
262
|
+
return "sandbox-orphan";
|
|
263
|
+
default: {
|
|
264
|
+
const unreachable = kind;
|
|
265
|
+
throw new Error(`Unhandled tunnel list item kind: ${String(unreachable)}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
const formatRelayHealth = (item) => {
|
|
270
|
+
const base = item.relayHealth;
|
|
271
|
+
if (item.relayHealthWarning !== undefined && item.relayHealthWarning.length > 0) {
|
|
272
|
+
return `${base}!`;
|
|
273
|
+
}
|
|
274
|
+
return base;
|
|
275
|
+
};
|
|
276
|
+
export const formatCompactAge = (activeForMs) => {
|
|
277
|
+
if (activeForMs === undefined) {
|
|
278
|
+
return "unknown";
|
|
279
|
+
}
|
|
280
|
+
const totalMinutes = Math.floor(activeForMs / 60_000);
|
|
281
|
+
if (totalMinutes < 1) {
|
|
282
|
+
return `${Math.max(1, Math.floor(activeForMs / 1_000))}s`;
|
|
283
|
+
}
|
|
284
|
+
if (totalMinutes < 60) {
|
|
285
|
+
return `${totalMinutes}m`;
|
|
286
|
+
}
|
|
287
|
+
const totalHours = Math.floor(totalMinutes / 60);
|
|
288
|
+
const minutes = totalMinutes % 60;
|
|
289
|
+
if (totalHours < 24) {
|
|
290
|
+
return minutes === 0 ? `${totalHours}h` : `${totalHours}h ${minutes}m`;
|
|
291
|
+
}
|
|
292
|
+
const days = Math.floor(totalHours / 24);
|
|
293
|
+
const hours = totalHours % 24;
|
|
294
|
+
return hours === 0 ? `${days}d` : `${days}d ${hours}h`;
|
|
295
|
+
};
|
|
296
|
+
const formatShortId = (value) => {
|
|
297
|
+
if (value === undefined || value.length === 0) {
|
|
298
|
+
return "-";
|
|
299
|
+
}
|
|
300
|
+
if (value.length <= 12) {
|
|
301
|
+
return value;
|
|
302
|
+
}
|
|
303
|
+
return `${value.slice(0, 8)}…`;
|
|
304
|
+
};
|
package/dist/command.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AAQA,OAAO,EAAW,MAAM,EAAiB,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AAQA,OAAO,EAAW,MAAM,EAAiB,MAAM,QAAQ,CAAC;AAkBxD,OAAO,EAAE,aAAa,EAAgB,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAwChF,eAAO,MAAM,MAAM,GACjB,MAAM,aAAa,CAAC,MAAM,CAAC,KAC1B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,eAAe,GAAG,aAAa,CAmBrD,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,KAAG,aAAa,CAAC,MAAM,CAC1C,CAAC"}
|
package/dist/command.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { formatCaptureOrigin, parseCaptureHostPort, parseTarget, validateRequestedSubdomain, } from "@localpreview/protocol";
|
|
2
2
|
import { Console, Effect, Layer, Option } from "effect";
|
|
3
|
+
import { adminTokenRequiredMessage, formatStatus, formatWarning, formatListTunnelsOutput, renderCleanHelp, renderConnectHelp, renderGlobalHelp, renderListHelp, removedAdminTokenFlagMessage, } from "./cli-ui.js";
|
|
3
4
|
import { CliConfig, CliConfigLive, LOCAL_CONTROL_PLANE_URL, normalizeControlPlaneUrl } from "./config.js";
|
|
4
5
|
import { ControlPlaneClient, ControlPlaneClientLive, } from "./control-plane.js";
|
|
5
6
|
import { CliUsageError, errorMessage } from "./errors.js";
|
|
6
7
|
import { LocalProxyLive } from "./local-proxy.js";
|
|
7
8
|
import { RelayClient, RelayClientLive } from "./relay-client.js";
|
|
9
|
+
import { CLI_PACKAGE_VERSION } from "./version.js";
|
|
8
10
|
export const runCli = (argv) => {
|
|
9
11
|
const normalized = normalizeCliArgs(argv);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
const helpKind = resolveHelpKind(normalized);
|
|
13
|
+
if (helpKind !== null) {
|
|
14
|
+
return printHelp(helpKind);
|
|
12
15
|
}
|
|
13
16
|
if (isVersionInvocation(normalized)) {
|
|
14
|
-
return Console.log(
|
|
17
|
+
return Console.log(CLI_PACKAGE_VERSION);
|
|
15
18
|
}
|
|
16
19
|
const parsed = parseCommand(normalized);
|
|
17
20
|
if (!parsed.ok) {
|
|
@@ -20,7 +23,29 @@ export const runCli = (argv) => {
|
|
|
20
23
|
return runSubcommand(parsed.command);
|
|
21
24
|
};
|
|
22
25
|
export const normalizeCliArgs = (argv) => argv[0] === "--" ? argv.slice(1) : argv;
|
|
23
|
-
const
|
|
26
|
+
const hasHelpFlag = (argv) => argv.includes("--help") || argv.includes("-h");
|
|
27
|
+
const resolveHelpKind = (argv) => {
|
|
28
|
+
if (argv.length === 0) {
|
|
29
|
+
return "global";
|
|
30
|
+
}
|
|
31
|
+
if (!hasHelpFlag(argv)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const first = argv[0];
|
|
35
|
+
if (first === "clean") {
|
|
36
|
+
return "clean";
|
|
37
|
+
}
|
|
38
|
+
if (first === "list") {
|
|
39
|
+
return "list";
|
|
40
|
+
}
|
|
41
|
+
if (first === "connect") {
|
|
42
|
+
return "connect";
|
|
43
|
+
}
|
|
44
|
+
if (first !== undefined && !first.startsWith("-")) {
|
|
45
|
+
return "connect";
|
|
46
|
+
}
|
|
47
|
+
return "global";
|
|
48
|
+
};
|
|
24
49
|
const isVersionInvocation = (argv) => argv.includes("--version");
|
|
25
50
|
const isLegacyConnectInvocation = (argv) => {
|
|
26
51
|
const first = argv[0];
|
|
@@ -29,21 +54,16 @@ const isLegacyConnectInvocation = (argv) => {
|
|
|
29
54
|
}
|
|
30
55
|
return first !== "connect" && !first.startsWith("-");
|
|
31
56
|
};
|
|
32
|
-
const printHelp = () =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
" localpreview connect 3000 --control-plane https://staging.localpreview.dev",
|
|
43
|
-
" localpreview clean proyecto --force",
|
|
44
|
-
" localpreview clean --all --force",
|
|
45
|
-
" localpreview clean demo --control-plane https://staging.localpreview.dev --admin-token ...",
|
|
46
|
-
].join("\n"));
|
|
57
|
+
const printHelp = (kind) => {
|
|
58
|
+
const text = kind === "global"
|
|
59
|
+
? renderGlobalHelp()
|
|
60
|
+
: kind === "connect"
|
|
61
|
+
? renderConnectHelp()
|
|
62
|
+
: kind === "clean"
|
|
63
|
+
? renderCleanHelp()
|
|
64
|
+
: renderListHelp();
|
|
65
|
+
return Console.log(text);
|
|
66
|
+
};
|
|
47
67
|
const parseCommand = (argv) => {
|
|
48
68
|
if (argv[0] === "clean") {
|
|
49
69
|
const parsed = parseCleanArgs(argv.slice(1));
|
|
@@ -58,6 +78,19 @@ const parseCommand = (argv) => {
|
|
|
58
78
|
ok: true,
|
|
59
79
|
};
|
|
60
80
|
}
|
|
81
|
+
if (argv[0] === "list") {
|
|
82
|
+
const parsed = parseListArgs(argv.slice(1));
|
|
83
|
+
if (!parsed.ok) {
|
|
84
|
+
return parsed;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
command: {
|
|
88
|
+
config: parsed.config,
|
|
89
|
+
kind: "list",
|
|
90
|
+
},
|
|
91
|
+
ok: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
61
94
|
const legacy = isLegacyConnectInvocation(argv);
|
|
62
95
|
const args = legacy ? argv : argv[0] === "connect" ? argv.slice(1) : argv;
|
|
63
96
|
const parsed = parseConnectArgs(args);
|
|
@@ -152,7 +185,6 @@ const parseConnectArgs = (argv) => {
|
|
|
152
185
|
};
|
|
153
186
|
const parseCleanArgs = (argv) => {
|
|
154
187
|
const rest = [...argv];
|
|
155
|
-
let adminToken;
|
|
156
188
|
let all = false;
|
|
157
189
|
let controlPlane;
|
|
158
190
|
let force = false;
|
|
@@ -165,13 +197,10 @@ const parseCleanArgs = (argv) => {
|
|
|
165
197
|
continue;
|
|
166
198
|
}
|
|
167
199
|
if (arg === "--admin-token") {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
adminToken = value.value;
|
|
173
|
-
index += 1;
|
|
174
|
-
continue;
|
|
200
|
+
return {
|
|
201
|
+
message: removedAdminTokenFlagMessage(),
|
|
202
|
+
ok: false,
|
|
203
|
+
};
|
|
175
204
|
}
|
|
176
205
|
if (arg === "--all") {
|
|
177
206
|
all = true;
|
|
@@ -222,14 +251,13 @@ const parseCleanArgs = (argv) => {
|
|
|
222
251
|
}
|
|
223
252
|
if (!all && subdomain === undefined) {
|
|
224
253
|
return {
|
|
225
|
-
message: "Usage: localpreview clean <subdomain> [--force] [-l|--local] [--control-plane url]
|
|
254
|
+
message: "Usage: localpreview clean <subdomain> [--force] [-l|--local] [--control-plane url]\n localpreview clean --all --force [-l|--local] [--control-plane url]",
|
|
226
255
|
ok: false,
|
|
227
256
|
};
|
|
228
257
|
}
|
|
229
258
|
return {
|
|
230
259
|
config: {
|
|
231
260
|
all,
|
|
232
|
-
adminToken: toOption(adminToken),
|
|
233
261
|
controlPlane: toOption(controlPlane),
|
|
234
262
|
force,
|
|
235
263
|
subdomain: toOption(subdomain),
|
|
@@ -237,6 +265,119 @@ const parseCleanArgs = (argv) => {
|
|
|
237
265
|
ok: true,
|
|
238
266
|
};
|
|
239
267
|
};
|
|
268
|
+
const parseListArgs = (argv) => {
|
|
269
|
+
const rest = [...argv];
|
|
270
|
+
let controlPlane;
|
|
271
|
+
let limit = 100;
|
|
272
|
+
let skip = 0;
|
|
273
|
+
let usedLocalControlPlane = false;
|
|
274
|
+
let usedControlPlaneFlag = false;
|
|
275
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
276
|
+
const arg = rest[index];
|
|
277
|
+
if (arg === undefined) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (arg === "--admin-token") {
|
|
281
|
+
return {
|
|
282
|
+
message: removedAdminTokenFlagMessage(),
|
|
283
|
+
ok: false,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (arg === "--limit") {
|
|
287
|
+
const value = readRequiredOptionValue(rest, index, "--limit");
|
|
288
|
+
if (!value.ok) {
|
|
289
|
+
return value;
|
|
290
|
+
}
|
|
291
|
+
const parsedLimit = parsePositiveIntegerOption(value.value, "--limit");
|
|
292
|
+
if (!parsedLimit.ok) {
|
|
293
|
+
return parsedLimit;
|
|
294
|
+
}
|
|
295
|
+
limit = parsedLimit.value;
|
|
296
|
+
index += 1;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (arg === "--skip") {
|
|
300
|
+
const value = readRequiredOptionValue(rest, index, "--skip");
|
|
301
|
+
if (!value.ok) {
|
|
302
|
+
return value;
|
|
303
|
+
}
|
|
304
|
+
const parsedSkip = parseNonNegativeIntegerOption(value.value, "--skip");
|
|
305
|
+
if (!parsedSkip.ok) {
|
|
306
|
+
return parsedSkip;
|
|
307
|
+
}
|
|
308
|
+
skip = parsedSkip.value;
|
|
309
|
+
index += 1;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const controlPlaneFlag = readControlPlaneFlag(rest, index, arg, {
|
|
313
|
+
usedControlPlaneFlag,
|
|
314
|
+
usedLocalControlPlane,
|
|
315
|
+
});
|
|
316
|
+
if (!controlPlaneFlag.ok) {
|
|
317
|
+
return controlPlaneFlag;
|
|
318
|
+
}
|
|
319
|
+
if (controlPlaneFlag.handled) {
|
|
320
|
+
if (controlPlaneFlag.usedLocalControlPlane) {
|
|
321
|
+
usedLocalControlPlane = true;
|
|
322
|
+
}
|
|
323
|
+
if (controlPlaneFlag.usedControlPlaneFlag) {
|
|
324
|
+
usedControlPlaneFlag = true;
|
|
325
|
+
}
|
|
326
|
+
controlPlane = controlPlaneFlag.controlPlane ?? controlPlane;
|
|
327
|
+
index = controlPlaneFlag.nextIndex;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (arg !== undefined && !arg.startsWith("-")) {
|
|
331
|
+
return {
|
|
332
|
+
message: `Unexpected argument: ${arg}`,
|
|
333
|
+
ok: false,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
message: `Unknown option: ${arg}`,
|
|
338
|
+
ok: false,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
config: {
|
|
343
|
+
controlPlane: toOption(controlPlane),
|
|
344
|
+
limit,
|
|
345
|
+
skip,
|
|
346
|
+
},
|
|
347
|
+
ok: true,
|
|
348
|
+
};
|
|
349
|
+
};
|
|
350
|
+
const parsePositiveIntegerOption = (value, option) => {
|
|
351
|
+
if (!/^\d+$/.test(value)) {
|
|
352
|
+
return {
|
|
353
|
+
message: `${option} must be a positive integer.`,
|
|
354
|
+
ok: false,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const parsed = Number.parseInt(value, 10);
|
|
358
|
+
if (parsed <= 0) {
|
|
359
|
+
return {
|
|
360
|
+
message: `${option} must be a positive integer.`,
|
|
361
|
+
ok: false,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
ok: true,
|
|
366
|
+
value: parsed,
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
const parseNonNegativeIntegerOption = (value, option) => {
|
|
370
|
+
if (!/^\d+$/.test(value)) {
|
|
371
|
+
return {
|
|
372
|
+
message: `${option} must be a non-negative integer.`,
|
|
373
|
+
ok: false,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
ok: true,
|
|
378
|
+
value: Number.parseInt(value, 10),
|
|
379
|
+
};
|
|
380
|
+
};
|
|
240
381
|
const readControlPlaneFlag = (argv, index, arg, state) => {
|
|
241
382
|
if (arg === "-l" || arg === "--local") {
|
|
242
383
|
if (state.usedControlPlaneFlag) {
|
|
@@ -302,7 +443,9 @@ const runSubcommand = (command) => {
|
|
|
302
443
|
const controlPlane = Option.getOrUndefined(command.config.controlPlane);
|
|
303
444
|
const effect = command.kind === "connect"
|
|
304
445
|
? runConnect(command.config, command.legacy)
|
|
305
|
-
:
|
|
446
|
+
: command.kind === "clean"
|
|
447
|
+
? runClean(command.config)
|
|
448
|
+
: runList(command.config);
|
|
306
449
|
return effect.pipe(Effect.provide(makeCommandLayer(makeConfigInput(controlPlane))));
|
|
307
450
|
};
|
|
308
451
|
const makeConfigInput = (controlPlaneUrl) => controlPlaneUrl === undefined ? {} : { controlPlaneUrl };
|
|
@@ -314,7 +457,7 @@ const makeCommandLayer = (input) => {
|
|
|
314
457
|
};
|
|
315
458
|
const runConnect = (config, legacy) => Effect.scoped(Effect.gen(function* () {
|
|
316
459
|
if (legacy) {
|
|
317
|
-
yield* Console.error("
|
|
460
|
+
yield* Console.error(formatWarning("`localpreview <target>` is deprecated. Use `localpreview connect <target>`."));
|
|
318
461
|
}
|
|
319
462
|
const target = parseTarget(config.target);
|
|
320
463
|
if (!target.ok) {
|
|
@@ -325,11 +468,11 @@ const runConnect = (config, legacy) => Effect.scoped(Effect.gen(function* () {
|
|
|
325
468
|
const relay = yield* RelayClient;
|
|
326
469
|
const cliConfig = yield* CliConfig;
|
|
327
470
|
const tunnel = yield* Effect.acquireRelease(controlPlane.createTunnel(cliConfig.controlPlaneUrl, requestedSubdomain === undefined ? {} : { requestedSubdomain }), (tunnel) => closeTunnelBestEffort(controlPlane, cliConfig.controlPlaneUrl, tunnel));
|
|
328
|
-
yield* Console.log(`Tunnel ready: ${tunnel.publicUrl}`);
|
|
329
|
-
yield* Console.log(`Forwarding to ${target.target.protocol}://${target.target.hostname}:${target.target.port}`);
|
|
471
|
+
yield* Console.log(formatStatus(`Tunnel ready: ${tunnel.publicUrl}`));
|
|
472
|
+
yield* Console.log(formatStatus(`Forwarding to ${target.target.protocol}://${target.target.hostname}:${target.target.port}`));
|
|
330
473
|
if (config.captures.length > 0) {
|
|
331
474
|
const origins = config.captures.map((capture) => formatCaptureOrigin(capture)).join(", ");
|
|
332
|
-
yield* Console.error(`
|
|
475
|
+
yield* Console.error(formatWarning(`Captured local backends (${origins}) are exposed through this preview URL. Anyone with the link can reach them.`));
|
|
333
476
|
}
|
|
334
477
|
yield* relay.connectAndServe(tunnel, target.target, config.captures);
|
|
335
478
|
}));
|
|
@@ -354,10 +497,11 @@ const runClean = (config) => Effect.gen(function* () {
|
|
|
354
497
|
}
|
|
355
498
|
const controlPlane = yield* ControlPlaneClient;
|
|
356
499
|
const cliConfig = yield* CliConfig;
|
|
357
|
-
const adminToken =
|
|
500
|
+
const adminToken = cliConfig.adminToken;
|
|
358
501
|
if (adminToken === undefined || adminToken.length === 0) {
|
|
502
|
+
yield* Console.log(renderGlobalHelp());
|
|
359
503
|
return yield* Effect.fail(new CliUsageError({
|
|
360
|
-
message:
|
|
504
|
+
message: adminTokenRequiredMessage(),
|
|
361
505
|
}));
|
|
362
506
|
}
|
|
363
507
|
if (config.all) {
|
|
@@ -380,7 +524,7 @@ const runClean = (config) => Effect.gen(function* () {
|
|
|
380
524
|
}
|
|
381
525
|
if (subdomain === undefined) {
|
|
382
526
|
return yield* Effect.fail(new CliUsageError({
|
|
383
|
-
message: "Usage: localpreview clean <subdomain> [--force] [-l|--local] [--control-plane url]
|
|
527
|
+
message: "Usage: localpreview clean <subdomain> [--force] [-l|--local] [--control-plane url]",
|
|
384
528
|
}));
|
|
385
529
|
}
|
|
386
530
|
const validation = validateRequestedSubdomain(subdomain);
|
|
@@ -403,6 +547,23 @@ const runClean = (config) => Effect.gen(function* () {
|
|
|
403
547
|
: ` Tunnel ${result.tunnelId} was ${result.relayStopped ? "stopped" : "removed from the store"}.`;
|
|
404
548
|
yield* Console.log(`Subdomain "${result.subdomain}" cleaned.${suffix}`);
|
|
405
549
|
});
|
|
550
|
+
const runList = (config) => Effect.gen(function* () {
|
|
551
|
+
const controlPlane = yield* ControlPlaneClient;
|
|
552
|
+
const cliConfig = yield* CliConfig;
|
|
553
|
+
const adminToken = cliConfig.adminToken;
|
|
554
|
+
if (adminToken === undefined || adminToken.length === 0) {
|
|
555
|
+
yield* Console.log(renderGlobalHelp());
|
|
556
|
+
return yield* Effect.fail(new CliUsageError({
|
|
557
|
+
message: adminTokenRequiredMessage(),
|
|
558
|
+
}));
|
|
559
|
+
}
|
|
560
|
+
const result = yield* controlPlane.listTunnels(cliConfig.controlPlaneUrl, {
|
|
561
|
+
adminToken,
|
|
562
|
+
limit: config.limit,
|
|
563
|
+
skip: config.skip,
|
|
564
|
+
});
|
|
565
|
+
yield* Console.log(formatListTunnelsOutput(result));
|
|
566
|
+
});
|
|
406
567
|
const closeTunnelBestEffort = (controlPlane, controlPlaneUrl, tunnel) => controlPlane
|
|
407
568
|
.closeTunnel(controlPlaneUrl, tunnel)
|
|
408
|
-
.pipe(Effect.catch((error) => Console.error(
|
|
569
|
+
.pipe(Effect.catch((error) => Console.error(formatWarning(errorMessage(error)))));
|
package/dist/config.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context, Effect, Layer } from "effect";
|
|
2
2
|
export type CliConfigShape = {
|
|
3
|
-
readonly
|
|
3
|
+
readonly adminToken: string | undefined;
|
|
4
4
|
readonly controlPlaneUrl: string;
|
|
5
5
|
readonly maxInFlightRequests: number;
|
|
6
6
|
readonly relayConnectTimeoutMs: number;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEhD,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEhD,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;CACzC,CAAC;;AAEF,qBAAa,SAAU,SAAQ,cAAyD;CAAG;AAE3F,eAAO,MAAM,wBAAwB,6BAA6B,CAAC;AACnE,eAAO,MAAM,uBAAuB,0BAA0B,CAAC;AAE/D,MAAM,MAAM,8BAA8B,GACtC;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB,GACD;IACE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;CACpB,CAAC;AAEN,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,GAAI,OAAO,MAAM,KAAG,8BA4DxD,CAAC;AA2BF,eAAO,MAAM,aAAa,GAAI,OAAO;IACnC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CAClC,KAAG,cAkBH,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,OAAO;IACnC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CAClC,yCAAmD,CAAC;AAWrD,eAAO,MAAM,aAAa,GAAI,OAAO;IACnC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CAClC,KAAG,MAAM,CAAC,MAAM,CAAC,cAAc,CAA4C,CAAC"}
|
package/dist/config.js
CHANGED
|
@@ -78,10 +78,9 @@ const isIpv4LoopbackAddress = (hostname) => {
|
|
|
78
78
|
export const makeCliConfig = (input) => {
|
|
79
79
|
const env = input.env ?? process.env;
|
|
80
80
|
const controlPlaneUrl = input.controlPlaneUrl ?? PUBLIC_CONTROL_PLANE_URL;
|
|
81
|
-
const
|
|
82
|
-
(controlPlaneUrl === LOCAL_CONTROL_PLANE_URL ? "local-dev-cleanup-token" : undefined);
|
|
81
|
+
const adminToken = env.LOCALPREVIEW_ADMIN_TOKEN;
|
|
83
82
|
return {
|
|
84
|
-
|
|
83
|
+
adminToken,
|
|
85
84
|
controlPlaneUrl,
|
|
86
85
|
maxInFlightRequests: readNumber(env.LOCALPREVIEW_MAX_IN_FLIGHT_REQUESTS, 100),
|
|
87
86
|
relayConnectTimeoutMs: readNumber(env.LOCALPREVIEW_RELAY_CONNECT_TIMEOUT_MS, 10_000),
|
package/dist/control-plane.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CreateTunnelResponse } from "@localpreview/protocol";
|
|
1
|
+
import { type CreateTunnelResponse, type ListTunnelsResponse } from "@localpreview/protocol";
|
|
2
2
|
import { Context, Effect, Layer } from "effect";
|
|
3
3
|
import { ControlPlaneError } from "./errors.js";
|
|
4
4
|
export type ControlPlaneClientShape = {
|
|
@@ -15,6 +15,11 @@ export type ControlPlaneClientShape = {
|
|
|
15
15
|
readonly createTunnel: (controlPlaneUrl: string, body: {
|
|
16
16
|
readonly requestedSubdomain?: string;
|
|
17
17
|
}) => Effect.Effect<CreateTunnelResponse, ControlPlaneError>;
|
|
18
|
+
readonly listTunnels: (controlPlaneUrl: string, input: {
|
|
19
|
+
readonly adminToken: string;
|
|
20
|
+
readonly limit: number;
|
|
21
|
+
readonly skip: number;
|
|
22
|
+
}) => Effect.Effect<ListTunnelsResponse, ControlPlaneError>;
|
|
18
23
|
};
|
|
19
24
|
export type CleanSubdomainResponse = {
|
|
20
25
|
readonly cleaned: boolean;
|
|
@@ -55,5 +60,10 @@ export declare const cleanAllSubdomains: (controlPlaneUrl: string, input: {
|
|
|
55
60
|
readonly adminToken: string;
|
|
56
61
|
readonly force: boolean;
|
|
57
62
|
}) => Promise<CleanAllSubdomainsResponse>;
|
|
63
|
+
export declare const listTunnels: (controlPlaneUrl: string, input: {
|
|
64
|
+
readonly adminToken: string;
|
|
65
|
+
readonly limit: number;
|
|
66
|
+
readonly skip: number;
|
|
67
|
+
}) => Promise<ListTunnelsResponse>;
|
|
58
68
|
export {};
|
|
59
69
|
//# sourceMappingURL=control-plane.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control-plane.d.ts","sourceRoot":"","sources":["../src/control-plane.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,oBAAoB,
|
|
1
|
+
{"version":3,"file":"control-plane.d.ts","sourceRoot":"","sources":["../src/control-plane.ts"],"names":[],"mappings":"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"}
|
package/dist/control-plane.js
CHANGED
|
@@ -11,11 +11,13 @@ export const ControlPlaneClientLive = Layer.succeed(ControlPlaneClient)({
|
|
|
11
11
|
while: (error) => error.retryable === true,
|
|
12
12
|
})),
|
|
13
13
|
createTunnel: (controlPlaneUrl, body) => createTunnelEffect(controlPlaneUrl, body),
|
|
14
|
+
listTunnels: (controlPlaneUrl, input) => listTunnelsEffect(controlPlaneUrl, input),
|
|
14
15
|
});
|
|
15
16
|
export const createTunnel = (controlPlaneUrl, body) => Effect.runPromise(createTunnelEffect(controlPlaneUrl, body));
|
|
16
17
|
export const closeTunnel = (controlPlaneUrl, tunnel) => Effect.runPromise(closeTunnelEffect(controlPlaneUrl, tunnel));
|
|
17
18
|
export const cleanSubdomain = (controlPlaneUrl, input) => Effect.runPromise(cleanSubdomainEffect(controlPlaneUrl, input));
|
|
18
19
|
export const cleanAllSubdomains = (controlPlaneUrl, input) => Effect.runPromise(cleanAllSubdomainsEffect(controlPlaneUrl, input));
|
|
20
|
+
export const listTunnels = (controlPlaneUrl, input) => Effect.runPromise(listTunnelsEffect(controlPlaneUrl, input));
|
|
19
21
|
const createTunnelEffect = (controlPlaneUrl, body) => Effect.promise(async () => {
|
|
20
22
|
let response;
|
|
21
23
|
try {
|
|
@@ -31,7 +33,7 @@ const createTunnelEffect = (controlPlaneUrl, body) => Effect.promise(async () =>
|
|
|
31
33
|
throw new ControlPlaneError({
|
|
32
34
|
message: [
|
|
33
35
|
`Could not reach localpreview control-plane at ${controlPlaneUrl}.`,
|
|
34
|
-
'Use "-l" or "--local"
|
|
36
|
+
'Use "-l" or "--local" as shorthand for --control-plane http://localhost:3000.',
|
|
35
37
|
].join("\n"),
|
|
36
38
|
retryable: false,
|
|
37
39
|
});
|
|
@@ -136,6 +138,59 @@ const cleanAllSubdomainsEffect = (controlPlaneUrl, input) => Effect.promise(asyn
|
|
|
136
138
|
}
|
|
137
139
|
return json;
|
|
138
140
|
});
|
|
141
|
+
const listTunnelsEffect = (controlPlaneUrl, input) => Effect.promise(async () => {
|
|
142
|
+
let response;
|
|
143
|
+
const url = new URL("/api/tunnels", controlPlaneUrl);
|
|
144
|
+
url.searchParams.set("limit", String(input.limit));
|
|
145
|
+
url.searchParams.set("skip", String(input.skip));
|
|
146
|
+
try {
|
|
147
|
+
response = await fetch(url, {
|
|
148
|
+
headers: {
|
|
149
|
+
[LOCALPREVIEW_ADMIN_TOKEN_HEADER]: input.adminToken,
|
|
150
|
+
},
|
|
151
|
+
method: "GET",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
throw new ControlPlaneError({
|
|
156
|
+
message: `Could not reach localpreview control-plane at ${controlPlaneUrl}.`,
|
|
157
|
+
retryable: false,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
const json = await readJson(response, "Tunnel listing");
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
if (response.status === 404) {
|
|
163
|
+
throw new ControlPlaneError({
|
|
164
|
+
message: [
|
|
165
|
+
"The control-plane does not support tunnel listing yet.",
|
|
166
|
+
"Deploy the updated control-plane, then rerun `localpreview list`.",
|
|
167
|
+
].join(" "),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
throw new ControlPlaneError({
|
|
171
|
+
message: json.error?.message ?? `Tunnel listing failed with HTTP ${response.status}.`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return validateListTunnelsResponse(json);
|
|
175
|
+
});
|
|
176
|
+
const validateListTunnelsResponse = (json) => {
|
|
177
|
+
if (typeof json.total !== "number" ||
|
|
178
|
+
typeof json.skip !== "number" ||
|
|
179
|
+
typeof json.limit !== "number" ||
|
|
180
|
+
!Array.isArray(json.items) ||
|
|
181
|
+
json.counts === undefined ||
|
|
182
|
+
typeof json.counts.tracked !== "number" ||
|
|
183
|
+
typeof json.counts["redis-orphan"] !== "number" ||
|
|
184
|
+
typeof json.counts["sandbox-orphan"] !== "number") {
|
|
185
|
+
throw new ControlPlaneError({
|
|
186
|
+
message: [
|
|
187
|
+
"Tunnel listing returned an invalid response.",
|
|
188
|
+
"This usually means the control-plane is running an older LocalPreview protocol.",
|
|
189
|
+
].join(" "),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return json;
|
|
193
|
+
};
|
|
139
194
|
const requiredCreateTunnelStringFields = [
|
|
140
195
|
"tunnelId",
|
|
141
196
|
"subdomain",
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
|
|
3
3
|
import { Console, Effect } from "effect";
|
|
4
4
|
import { runCli } from "./command.js";
|
|
5
|
+
import { formatCliError } from "./cli-ui.js";
|
|
5
6
|
import { errorMessage } from "./errors.js";
|
|
6
7
|
const argv = process.argv.slice(2);
|
|
7
|
-
const program = runCli(argv).pipe(Effect.catch((error) => Console.error(errorMessage(error)).pipe(Effect.andThen(Effect.sync(() => {
|
|
8
|
+
const program = runCli(argv).pipe(Effect.catch((error) => Console.error(formatCliError(errorMessage(error))).pipe(Effect.andThen(Effect.sync(() => {
|
|
8
9
|
process.exitCode =
|
|
9
10
|
typeof error === "object" &&
|
|
10
11
|
error !== null &&
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-proxy.d.ts","sourceRoot":"","sources":["../src/local-proxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,WAAW,EAEhB,KAAK,YAAY,EAClB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAW,OAAO,EAAE,MAAM,EAAS,KAAK,EAAO,MAAM,QAAQ,CAAC;AACrE,OAAO,KAAK,SAAS,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"local-proxy.d.ts","sourceRoot":"","sources":["../src/local-proxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,WAAW,EAEhB,KAAK,YAAY,EAClB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAW,OAAO,EAAE,MAAM,EAAS,KAAK,EAAO,MAAM,QAAQ,CAAC;AACrE,OAAO,KAAK,SAAS,MAAM,IAAI,CAAC;AAchC,OAAO,EAAE,SAAS,EAAuB,MAAM,aAAa,CAAC;AAC7D,OAAO,EAEL,kBAAkB,EAGnB,MAAM,aAAa,CAAC;AAWrB,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;IAChD,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,aAAa,EAAE,CACtB,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,YAAY,EACrB,KAAK,EAAE,MAAM,KACV,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC;CAC9C,CAAC;;AAEF,qBAAa,UAAW,SAAQ,eAA4D;CAAG;AAE/F,eAAO,MAAM,cAAc,2CAU1B,CAAC;AAgVF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,MAAM,EAAE,YAAY,MAAM,KAAG,MAqC5E,CAAC;AA0BF,eAAO,MAAM,0BAA0B,GAAI,SAAS,OAAO,KAAG,WAqB7D,CAAC"}
|
package/dist/local-proxy.js
CHANGED
|
@@ -2,6 +2,7 @@ import { decodeServerRelayMessage, encodeRelayMessage, filterEndToEndHeaderPairs
|
|
|
2
2
|
import { Console, Context, Effect, Fiber, Layer, Ref } from "effect";
|
|
3
3
|
import { buildCaptureCookiePathPrefix, resolveRoute, } from "./capture-route.js";
|
|
4
4
|
import { buildCaptureShimScript, buildFrontendOrigin, injectCaptureShim, isHtmlResponse, stripContentSecurityPolicy, } from "./capture-shim.js";
|
|
5
|
+
import { formatRequestError, formatRequestInbound, formatRequestOutbound } from "./cli-ui.js";
|
|
5
6
|
import { CliConfig } from "./config.js";
|
|
6
7
|
import { LocalRequestError, RelayProtocolError, RequestLimitError, errorMessage, } from "./errors.js";
|
|
7
8
|
export class LocalProxy extends Context.Service()("LocalProxy") {
|
|
@@ -51,7 +52,7 @@ const handleRequestStart = (config, requests, socket, message) => Effect.gen(fun
|
|
|
51
52
|
totalBytes: 0,
|
|
52
53
|
});
|
|
53
54
|
yield* Ref.set(requests, next);
|
|
54
|
-
yield* Console.log(
|
|
55
|
+
yield* Console.log(formatRequestOutbound(message.method, logPath(message.path)));
|
|
55
56
|
});
|
|
56
57
|
const handleRequestChunk = (config, requests, socket, message) => Effect.gen(function* () {
|
|
57
58
|
const current = yield* Ref.get(requests);
|
|
@@ -94,7 +95,7 @@ const handleRequestEnd = (config, requests, socket, session, message) => Effect.
|
|
|
94
95
|
message: error.message,
|
|
95
96
|
requestId: message.requestId,
|
|
96
97
|
type: "response-error",
|
|
97
|
-
}).pipe(Effect.andThen(Console.log(
|
|
98
|
+
}).pipe(Effect.andThen(Console.log(formatRequestError(logPath(request.path), error.message))))), Effect.ensuring(Ref.update(requests, (map) => {
|
|
98
99
|
const next = new Map(map);
|
|
99
100
|
next.delete(message.requestId);
|
|
100
101
|
return next;
|
|
@@ -202,7 +203,7 @@ const proxyRequest = (config, socket, session, requestId, request) => Effect.gen
|
|
|
202
203
|
requestId,
|
|
203
204
|
type: "response-end",
|
|
204
205
|
});
|
|
205
|
-
yield* Console.log(
|
|
206
|
+
yield* Console.log(formatRequestInbound(response.status, logPath(request.path), Date.now() - startedAt));
|
|
206
207
|
});
|
|
207
208
|
const rewriteCaptureResponseHeaders = (headers, capturePath) => {
|
|
208
209
|
const pathPrefix = buildCaptureCookiePathPrefix(capturePath);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relay-client.d.ts","sourceRoot":"","sources":["../src/relay-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAChG,OAAO,EAAW,OAAO,EAAY,MAAM,EAAE,KAAK,EAAY,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"relay-client.d.ts","sourceRoot":"","sources":["../src/relay-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAChG,OAAO,EAAW,OAAO,EAAY,MAAM,EAAE,KAAK,EAAY,MAAM,QAAQ,CAAC;AAG7E,OAAO,EAAE,SAAS,EAAuB,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAgB,MAAM,aAAa,CAAC;AACrF,OAAO,EAAE,UAAU,EAAwB,MAAM,kBAAkB,CAAC;AAEpE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,eAAe,EAAE,CACxB,MAAM,EAAE,oBAAoB,EAC5B,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,KACnC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,oBAAoB,GAAG,kBAAkB,CAAC,CAAC;CACrE,CAAC;;AAEF,qBAAa,WAAY,SAAQ,gBAA+D;CAAG;AAEnG,eAAO,MAAM,eAAe,yDAU3B,CAAC;AA6LF,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,OAAwB,CAAC"}
|
package/dist/relay-client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Console, Context, Deferred, Effect, Layer, Schedule } from "effect";
|
|
2
2
|
import WebSocket from "ws";
|
|
3
|
+
import { formatRelayReconnect, formatStatus } from "./cli-ui.js";
|
|
3
4
|
import { CliConfig } from "./config.js";
|
|
4
5
|
import { RelayConnectionError, RelayProtocolError, errorMessage } from "./errors.js";
|
|
5
6
|
import { LocalProxy } from "./local-proxy.js";
|
|
@@ -15,7 +16,7 @@ export const RelayClientLive = Layer.effect(RelayClient)(Effect.gen(function* ()
|
|
|
15
16
|
const connectAndServe = (config, localProxy, tunnel, target, captures) => serveWithReconnect(config, localProxy, tunnel, target, captures);
|
|
16
17
|
const serveWithReconnect = (config, localProxy, tunnel, target, captures) => serveOnce(config, localProxy, tunnel, target, captures).pipe(Effect.catch((error) => {
|
|
17
18
|
if (error instanceof RelayConnectionError && error.retryable === true) {
|
|
18
|
-
return Console.error(
|
|
19
|
+
return Console.error(formatRelayReconnect(error.message)).pipe(Effect.andThen(Effect.sleep("1 second")), Effect.andThen(serveWithReconnect(config, localProxy, tunnel, target, captures)));
|
|
19
20
|
}
|
|
20
21
|
return Effect.fail(error);
|
|
21
22
|
}));
|
|
@@ -23,7 +24,7 @@ const serveOnce = (config, localProxy, tunnel, target, captures) => Effect.gen(f
|
|
|
23
24
|
const socket = yield* openSocket(tunnel).pipe(Effect.retry({
|
|
24
25
|
schedule: Schedule.recurs(Math.ceil(config.relayConnectTimeoutMs / 250)),
|
|
25
26
|
}));
|
|
26
|
-
yield* Console.log("Connected to relay.");
|
|
27
|
+
yield* Console.log(formatStatus("Connected to relay."));
|
|
27
28
|
const session = { captures, target };
|
|
28
29
|
const done = yield* Deferred.make();
|
|
29
30
|
const handleSignal = () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,mBAAmB,QAAsB,CAAC"}
|
package/dist/version.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "localpreview",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"bin": {
|
|
5
5
|
"localpreview": "./dist/index.js"
|
|
6
6
|
},
|
|
@@ -10,27 +10,26 @@
|
|
|
10
10
|
"README.md"
|
|
11
11
|
],
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@effect/platform-node": "beta",
|
|
15
|
+
"effect": "beta",
|
|
16
|
+
"picocolors": "^1.1.1",
|
|
17
|
+
"ws": "latest",
|
|
18
|
+
"@localpreview/protocol": "0.2.2"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@effect/vitest": "beta",
|
|
22
|
+
"@types/node": "latest",
|
|
23
|
+
"@types/ws": "latest",
|
|
24
|
+
"tsx": "latest",
|
|
25
|
+
"typescript": "latest",
|
|
26
|
+
"vitest": "latest"
|
|
27
|
+
},
|
|
13
28
|
"scripts": {
|
|
14
29
|
"build": "tsc -p tsconfig.build.json",
|
|
15
30
|
"dev": "tsx src/index.ts",
|
|
16
31
|
"lint": "oxlint .",
|
|
17
|
-
"prepack": "pnpm build",
|
|
18
|
-
"prepublishOnly": "pnpm test && pnpm typecheck && pnpm build",
|
|
19
32
|
"test": "vitest run --passWithNoTests",
|
|
20
33
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
21
|
-
},
|
|
22
|
-
"dependencies": {
|
|
23
|
-
"@effect/platform-node": "beta",
|
|
24
|
-
"@localpreview/protocol": "^0.2.1",
|
|
25
|
-
"effect": "beta",
|
|
26
|
-
"ws": "latest"
|
|
27
|
-
},
|
|
28
|
-
"devDependencies": {
|
|
29
|
-
"@effect/vitest": "catalog:",
|
|
30
|
-
"@types/node": "catalog:",
|
|
31
|
-
"@types/ws": "catalog:",
|
|
32
|
-
"tsx": "catalog:",
|
|
33
|
-
"typescript": "catalog:",
|
|
34
|
-
"vitest": "catalog:"
|
|
35
34
|
}
|
|
36
|
-
}
|
|
35
|
+
}
|