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 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 proyecto
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|--local` > `https://localpreview.dev`
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
  ```
@@ -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
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AAQA,OAAO,EAAW,MAAM,EAAiB,MAAM,QAAQ,CAAC;AAOxD,OAAO,EAAE,aAAa,EAAgB,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AA8BhF,eAAO,MAAM,MAAM,GACjB,MAAM,aAAa,CAAC,MAAM,CAAC,KAC1B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,eAAe,GAAG,aAAa,CAkBrD,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,KAAG,aAAa,CAAC,MAAM,CAC1C,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
- if (isHelpInvocation(normalized)) {
11
- return printHelp();
12
+ const helpKind = resolveHelpKind(normalized);
13
+ if (helpKind !== null) {
14
+ return printHelp(helpKind);
12
15
  }
13
16
  if (isVersionInvocation(normalized)) {
14
- return Console.log("0.0.0");
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 isHelpInvocation = (argv) => argv.length === 0 || argv.includes("--help") || argv.includes("-h");
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 = () => Console.log([
33
- "Usage: localpreview connect <port|target-url> [--name subdomain] [--capture host:port] [-l|--local] [--control-plane url]",
34
- " localpreview clean <subdomain> [--force] [-l|--local] [--control-plane url] [--admin-token token]",
35
- " localpreview clean --all --force [-l|--local] [--control-plane url] [--admin-token token]",
36
- "",
37
- "Examples:",
38
- " localpreview connect 3000",
39
- " localpreview connect 5173 --capture localhost:4000",
40
- " localpreview connect https://localhost:3000 --name proyecto",
41
- " localpreview connect 3000 -l",
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
- const value = readRequiredOptionValue(rest, index, "--admin-token");
169
- if (!value.ok) {
170
- return value;
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] [--admin-token token]\n localpreview clean --all --force [-l|--local] [--control-plane url] [--admin-token token]",
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
- : runClean(command.config);
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("Warning: `localpreview <target>` is deprecated. Use `localpreview connect <target>`.");
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(`Warning: captured local backends (${origins}) are exposed through this preview URL. Anyone with the link can reach them.`);
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 = Option.getOrUndefined(config.adminToken) ?? cliConfig.cleanupAdminToken;
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: "Subdomain cleanup requires an admin token. Set LOCALPREVIEW_CLEANUP_TOKEN or pass --admin-token.",
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] [--admin-token token]",
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(`Warning: ${errorMessage(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 cleanupAdminToken: string | undefined;
3
+ readonly adminToken: string | undefined;
4
4
  readonly controlPlaneUrl: string;
5
5
  readonly maxInFlightRequests: number;
6
6
  readonly relayConnectTimeoutMs: number;
@@ -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,iBAAiB,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/C,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,cAoBH,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"}
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 cleanupAdminToken = env.LOCALPREVIEW_CLEANUP_TOKEN ??
82
- (controlPlaneUrl === LOCAL_CONTROL_PLANE_URL ? "local-dev-cleanup-token" : undefined);
81
+ const adminToken = env.LOCALPREVIEW_ADMIN_TOKEN;
83
82
  return {
84
- cleanupAdminToken,
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),
@@ -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":"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"}
1
+ {"version":3,"file":"control-plane.d.ts","sourceRoot":"","sources":["../src/control-plane.ts"],"names":[],"mappings":"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"}
@@ -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" to target a local control-plane at http://localhost:3000.',
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;AAahC,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;AA4UF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,MAAM,EAAE,YAAY,MAAM,KAAG,MAqC5E,CAAC;AA0BF,eAAO,MAAM,0BAA0B,GAAI,SAAS,OAAO,KAAG,WAqB7D,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"}
@@ -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(`-> ${message.method} ${logPath(message.path)}`);
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(`<- error ${logPath(request.path)} ${error.message}`)))), Effect.ensuring(Ref.update(requests, (map) => {
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(`<- ${response.status} ${logPath(request.path)} ${Date.now() - startedAt}ms`);
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;AAE7E,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"}
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"}
@@ -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(`${error.message}; reconnecting...`).pipe(Effect.andThen(Effect.sleep("1 second")), Effect.andThen(serveWithReconnect(config, localProxy, tunnel, target, captures)));
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,2 @@
1
+ export declare const CLI_PACKAGE_VERSION: string;
2
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,mBAAmB,QAAsB,CAAC"}
@@ -0,0 +1,2 @@
1
+ import packageJson from "../package.json" with { type: "json" };
2
+ export const CLI_PACKAGE_VERSION = packageJson.version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "localpreview",
3
- "version": "0.2.3",
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
+ }