localpreview 0.2.3 → 0.2.5
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/capture-route.d.ts +1 -0
- package/dist/capture-route.d.ts.map +1 -1
- package/dist/capture-route.js +1 -0
- package/dist/capture-shim.d.ts +5 -1
- package/dist/capture-shim.d.ts.map +1 -1
- package/dist/capture-shim.js +3 -2
- package/dist/cli-ui.d.ts +36 -0
- package/dist/cli-ui.d.ts.map +1 -0
- package/dist/cli-ui.js +351 -0
- package/dist/command.d.ts +1 -0
- package/dist/command.d.ts.map +1 -1
- package/dist/command.js +208 -43
- 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/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/local-proxy.d.ts +3 -0
- package/dist/local-proxy.d.ts.map +1 -1
- package/dist/local-proxy.js +87 -9
- package/dist/missing-capture.d.ts +44 -0
- package/dist/missing-capture.d.ts.map +1 -0
- package/dist/missing-capture.js +128 -0
- package/dist/relay-client.d.ts +6 -2
- package/dist/relay-client.d.ts.map +1 -1
- package/dist/relay-client.js +14 -7
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/package.json +3 -2
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/capture-route.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture-route.d.ts","sourceRoot":"","sources":["../src/capture-route.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,wBAAwB,CAAC;AAEhC,eAAO,MAAM,mBAAmB,4BAA4B,CAAC;AAE7D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC,UAAU,CAAC,CAAC;CAC7C,CAAC;AAEF,KAAK,sBAAsB,GACvB;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;CACpC,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;CACpB,CAAC;AAEN;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,aAAa,MAAM,KAAG,sBA+CtD,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,4BAA4B,GAAI,QAAQ,iBAAiB,KAAG,MAC6B,CAAC;AAEvG,2DAA2D;AAC3D,eAAO,MAAM,qBAAqB,GAAI,UAAU,MAAM,KAAG,MACA,CAAC;AAE1D,MAAM,MAAM,aAAa,GACrB;IACE,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;IACxC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B,CAAC;AAEN,qFAAqF;AACrF,eAAO,MAAM,YAAY,GACvB,aAAa,MAAM,EACnB,eAAe,YAAY,EAC3B,UAAU,aAAa,CAAC,aAAa,CAAC,KACrC,
|
|
1
|
+
{"version":3,"file":"capture-route.d.ts","sourceRoot":"","sources":["../src/capture-route.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,wBAAwB,CAAC;AAEhC,eAAO,MAAM,mBAAmB,4BAA4B,CAAC;AAE7D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC,UAAU,CAAC,CAAC;CAC7C,CAAC;AAEF,KAAK,sBAAsB,GACvB;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;CACpC,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;CACpB,CAAC;AAEN;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,aAAa,MAAM,KAAG,sBA+CtD,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,4BAA4B,GAAI,QAAQ,iBAAiB,KAAG,MAC6B,CAAC;AAEvG,2DAA2D;AAC3D,eAAO,MAAM,qBAAqB,GAAI,UAAU,MAAM,KAAG,MACA,CAAC;AAE1D,MAAM,MAAM,aAAa,GACrB;IACE,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;IACxC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B,GACD;IACE,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;IACxC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B,CAAC;AAEN,qFAAqF;AACrF,eAAO,MAAM,YAAY,GACvB,aAAa,MAAM,EACnB,eAAe,YAAY,EAC3B,UAAU,aAAa,CAAC,aAAa,CAAC,KACrC,aA0BF,CAAC"}
|
package/dist/capture-route.js
CHANGED
|
@@ -59,6 +59,7 @@ export const resolveRoute = (requestPath, primaryTarget, captures) => {
|
|
|
59
59
|
const capture = findCapture(captures, parsed.hostname, parsed.port);
|
|
60
60
|
if (capture === undefined) {
|
|
61
61
|
return {
|
|
62
|
+
capturePath: parsed,
|
|
62
63
|
kind: "invalid-capture",
|
|
63
64
|
message: `Capture route is not allowlisted: ${parsed.hostname}:${parsed.port}.`,
|
|
64
65
|
};
|
package/dist/capture-shim.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { CaptureTarget } from "@localpreview/protocol";
|
|
2
2
|
import { encodeCaptureHostname } from "./capture-route.js";
|
|
3
3
|
export { encodeCaptureHostname };
|
|
4
|
+
type CaptureShimPrimaryTarget = {
|
|
5
|
+
readonly hostname: string;
|
|
6
|
+
readonly port: number;
|
|
7
|
+
};
|
|
4
8
|
/** Generates the inline browser shim injected into proxied HTML responses. */
|
|
5
|
-
export declare const buildCaptureShimScript: (captures: ReadonlyArray<CaptureTarget>) => string;
|
|
9
|
+
export declare const buildCaptureShimScript: (primary: CaptureShimPrimaryTarget, captures: ReadonlyArray<CaptureTarget>) => string;
|
|
6
10
|
/** Injects the capture shim into an HTML document string. */
|
|
7
11
|
export declare const injectCaptureShim: (html: string, shimScript: string) => string;
|
|
8
12
|
/** Returns true when the response looks like HTML suitable for shim injection. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture-shim.d.ts","sourceRoot":"","sources":["../src/capture-shim.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAuB,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAEhF,OAAO,EAAE,qBAAqB,EAAE,CAAC;AAEjC,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,
|
|
1
|
+
{"version":3,"file":"capture-shim.d.ts","sourceRoot":"","sources":["../src/capture-shim.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAuB,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAEhF,OAAO,EAAE,qBAAqB,EAAE,CAAC;AAEjC,KAAK,wBAAwB,GAAG;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,GACjC,SAAS,wBAAwB,EACjC,UAAU,aAAa,CAAC,aAAa,CAAC,KACrC,MAWF,CAAC;AAEF,6DAA6D;AAC7D,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,EAAE,YAAY,MAAM,KAAG,MAcpE,CAAC;AAEF,kFAAkF;AAClF,eAAO,MAAM,cAAc,GAAI,aAAa,MAAM,GAAG,IAAI,KAAG,OACa,CAAC;AAE1E,6DAA6D;AAC7D,eAAO,MAAM,0BAA0B,GACrC,SAAS,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,KAChD,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAI9B,CAAC;AAEL,qFAAqF;AACrF,eAAO,MAAM,mBAAmB,GAAI,QAAQ;IAC1C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;CACrC,KAAG,MAAkE,CAAC"}
|
package/dist/capture-shim.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { CAPTURE_PATH_PREFIX, encodeCaptureHostname } from "./capture-route.js";
|
|
2
2
|
export { encodeCaptureHostname };
|
|
3
3
|
/** Generates the inline browser shim injected into proxied HTML responses. */
|
|
4
|
-
export const buildCaptureShimScript = (captures) => {
|
|
4
|
+
export const buildCaptureShimScript = (primary, captures) => {
|
|
5
5
|
const config = JSON.stringify({
|
|
6
6
|
captures: captures.map((capture) => ({
|
|
7
7
|
hostname: capture.hostname,
|
|
8
8
|
port: capture.port,
|
|
9
9
|
})),
|
|
10
10
|
prefix: CAPTURE_PATH_PREFIX,
|
|
11
|
+
primary,
|
|
11
12
|
});
|
|
12
|
-
return `(function(){var cfg=${config};var caps=cfg.captures;var prefix=cfg.prefix;function normHost(h){return h==="::1"?"[::1]":h.toLowerCase();}function
|
|
13
|
+
return `(function(){var cfg=${config};var caps=cfg.captures;var primary=cfg.primary;var prefix=cfg.prefix;var eventsPath="/__localpreview/events";var reported={};function normHost(h){return h==="::1"?"[::1]":h.toLowerCase();}function isLoopbackHost(h){var lower=normHost(h);if(lower==="localhost"||lower==="127.0.0.1"||lower==="[::1]")return true;return lower.endsWith(".localhost");}function loopbackGroup(h){var lower=normHost(h);if(lower==="localhost"||lower==="127.0.0.1"||lower==="[::1]")return "__loopback__";return lower;}function sameLoopbackPort(host,port,otherHost,otherPort){return port===otherPort&&loopbackGroup(host)===loopbackGroup(otherHost);}function dedupeKey(proto,host,port){var group=loopbackGroup(host);return proto+"://"+(group==="__loopback__"?group:normHost(host))+":"+port;}function urlPort(url){return url.port?Number(url.port):(url.protocol==="https:"||url.protocol==="wss:"?443:80);}function matchesCapture(url){var host=normHost(url.hostname);var port=urlPort(url);for(var i=0;i<caps.length;i++){var cap=caps[i];if(sameLoopbackPort(host,port,cap.hostname,cap.port))return true;}return false;}function isPrimaryPort(url){return sameLoopbackPort(url.hostname,urlPort(url),primary.hostname,primary.port);}function isAllowlisted(url){return matchesCapture(url)||isPrimaryPort(url);}function reportMissing(url,transport,method){try{if(!isLoopbackHost(url.hostname))return;if(isAllowlisted(url))return;var proto=url.protocol.replace(":","");if(proto!=="http"&&proto!=="https"&&proto!=="ws"&&proto!=="wss")return;var host=normHost(url.hostname);var port=urlPort(url);var key=dedupeKey(proto,host,port);if(reported[key])return;reported[key]=true;var payload={protocol:proto,hostname:host,port:port,transport:transport};if(typeof method==="string"&&method){payload.method=method;}var body=JSON.stringify({type:"missing-capture",payload:payload});if(typeof navigator!=="undefined"&&navigator.sendBeacon){navigator.sendBeacon(eventsPath,body);}else{fetch(eventsPath,{method:"POST",body:body,keepalive:true});}}catch(e){}}function rewriteUrl(input){var url=new URL(input,window.location.href);if(!matchesCapture(url)){return input;}var proto=url.protocol.replace(":","");var host=encodeURIComponent(normHost(url.hostname));var path=url.pathname+url.search;return prefix+"/"+proto+"/"+host+"/"+url.port+path;}function warnUnsupported(api,url){console.error("[localpreview] "+api+" to captured local backend is not supported in v1: "+url+". Use fetch/XHR or run localpreview connect for that service directly.");}function fetchMethod(input,init){if(init&&typeof init==="object"&&typeof init.method==="string")return init.method.toUpperCase();if(typeof Request!=="undefined"&&input instanceof Request)return input.method.toUpperCase();return "GET";}var origFetch=window.fetch;window.fetch=function(input,init){try{var url=typeof input==="string"?input:input instanceof URL?input.href:input.url;if(typeof url==="string"&&/^https?:\\/\\//.test(url)){var parsed=new URL(url,window.location.href);reportMissing(parsed,"fetch",fetchMethod(input,init));var rewritten=rewriteUrl(url);if(rewritten!==url){return origFetch.call(this,rewritten,init);}}}catch(e){}return origFetch.call(this,input,init);};if(typeof XMLHttpRequest!=="undefined"){var origOpen=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(method,url){try{if(typeof url==="string"&&/^https?:\\/\\//.test(url)){var parsed=new URL(url,window.location.href);reportMissing(parsed,"xhr",typeof method==="string"?method.toUpperCase():undefined);var rewritten=rewriteUrl(url);if(rewritten!==url){return origOpen.call(this,method,rewritten);}}}catch(e){}return origOpen.apply(this,arguments);};}if(typeof window.WebSocket!=="undefined"){var OrigWebSocket=window.WebSocket;window.WebSocket=function(url,protocols){try{var parsed=new URL(url,window.location.href);reportMissing(parsed,"websocket");if(matchesCapture(parsed)){warnUnsupported("WebSocket",url);throw new Error("LocalPreview does not proxy WebSocket connections to captured backends in v1.");}}catch(e){if(e instanceof Error&&e.message.indexOf("LocalPreview")!==-1){throw e;}}return protocols===undefined?new OrigWebSocket(url):new OrigWebSocket(url,protocols);};if(OrigWebSocket.prototype){window.WebSocket.prototype=OrigWebSocket.prototype;}}if(typeof window.EventSource!=="undefined"){var OrigEventSource=window.EventSource;window.EventSource=function(url,init){try{var parsed=new URL(url,window.location.href);reportMissing(parsed,"eventsource");if(matchesCapture(parsed)){warnUnsupported("EventSource",url);throw new Error("LocalPreview does not proxy EventSource connections to captured backends in v1.");}}catch(e){if(e instanceof Error&&e.message.indexOf("LocalPreview")!==-1){throw e;}}return init===undefined?new OrigEventSource(url):new OrigEventSource(url,init);};if(OrigEventSource.prototype){window.EventSource.prototype=OrigEventSource.prototype;}}})();`;
|
|
13
14
|
};
|
|
14
15
|
/** Injects the capture shim into an HTML document string. */
|
|
15
16
|
export const injectCaptureShim = (html, shimScript) => {
|
package/dist/cli-ui.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
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 formatCapturedRequestOutbound: (method: string, targetUrl: string) => string;
|
|
10
|
+
export declare const formatRequestInbound: (status: number, path: string, durationMs: number) => string;
|
|
11
|
+
export declare const formatCapturedRequestInbound: (status: number, targetUrl: string, durationMs: number) => string;
|
|
12
|
+
export declare const formatMissingCaptureWarning: (options: {
|
|
13
|
+
readonly origin: string;
|
|
14
|
+
readonly method?: string;
|
|
15
|
+
readonly suggestedCommand: string;
|
|
16
|
+
readonly transport?: string;
|
|
17
|
+
}) => string;
|
|
18
|
+
export declare const formatRequestError: (path: string, message: string) => string;
|
|
19
|
+
export declare const formatInvalidCaptureRouteError: (options: {
|
|
20
|
+
readonly hostname: string;
|
|
21
|
+
readonly method: string;
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly port: number;
|
|
24
|
+
}) => string;
|
|
25
|
+
export declare const removedAdminTokenFlagMessage: () => string;
|
|
26
|
+
export declare const adminTokenRequiredMessage: () => string;
|
|
27
|
+
export type HelpRenderOptions = {
|
|
28
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
29
|
+
};
|
|
30
|
+
export declare const renderGlobalHelp: (options?: HelpRenderOptions) => string;
|
|
31
|
+
export declare const renderConnectHelp: () => string;
|
|
32
|
+
export declare const renderCleanHelp: (options?: HelpRenderOptions) => string;
|
|
33
|
+
export declare const renderListHelp: (options?: HelpRenderOptions) => string;
|
|
34
|
+
export declare const formatListTunnelsOutput: (response: ListTunnelsResponse) => string;
|
|
35
|
+
export declare const formatCompactAge: (activeForMs: number | undefined) => string;
|
|
36
|
+
//# 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,6BAA6B,GAAI,QAAQ,MAAM,EAAE,WAAW,MAAM,KAAG,MAUjF,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,QAAQ,MAAM,EAAE,MAAM,MAAM,EAAE,YAAY,MAAM,KAAG,MAgBvF,CAAC;AAEF,eAAO,MAAM,4BAA4B,GACvC,QAAQ,MAAM,EACd,WAAW,MAAM,EACjB,YAAY,MAAM,KACjB,MAiBF,CAAC;AAIF,eAAO,MAAM,2BAA2B,GAAI,SAAS;IACnD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,KAAG,MAiBH,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,EAAE,SAAS,MAAM,KAAG,MASlE,CAAC;AAEF,eAAO,MAAM,8BAA8B,GAAI,SAAS;IACtD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,KAAG,MAaH,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,MA+BjE,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,UAAS,iBAAsB,KAAG,MA+BhE,CAAC;AAaF,eAAO,MAAM,uBAAuB,GAAI,UAAU,mBAAmB,KAAG,MAkEvE,CAAC;AAwCF,eAAO,MAAM,gBAAgB,GAAI,aAAa,MAAM,GAAG,SAAS,KAAG,MA0BlE,CAAC"}
|
package/dist/cli-ui.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
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 formatCapturedRequestOutbound = (method, targetUrl) => {
|
|
39
|
+
const arrow = "→";
|
|
40
|
+
const label = "CAPTURED";
|
|
41
|
+
const line = `${arrow} ${label} ${method} ${targetUrl}`;
|
|
42
|
+
if (!pc.isColorSupported) {
|
|
43
|
+
return line;
|
|
44
|
+
}
|
|
45
|
+
return `${pc.dim(arrow)} ${formatCaptureBadge(label)} ${pc.bold(method)} ${targetUrl}`;
|
|
46
|
+
};
|
|
47
|
+
export const formatRequestInbound = (status, path, durationMs) => {
|
|
48
|
+
const arrow = "←";
|
|
49
|
+
const line = `${arrow} ${status} ${path} ${durationMs}ms`;
|
|
50
|
+
if (!pc.isColorSupported) {
|
|
51
|
+
return line;
|
|
52
|
+
}
|
|
53
|
+
const statusText = status >= 500
|
|
54
|
+
? pc.red(String(status))
|
|
55
|
+
: status >= 400
|
|
56
|
+
? pc.yellow(String(status))
|
|
57
|
+
: pc.green(String(status));
|
|
58
|
+
return `${pc.dim(arrow)} ${statusText} ${path} ${pc.dim(`${durationMs}ms`)}`;
|
|
59
|
+
};
|
|
60
|
+
export const formatCapturedRequestInbound = (status, targetUrl, durationMs) => {
|
|
61
|
+
const arrow = "←";
|
|
62
|
+
const label = "CAPTURED";
|
|
63
|
+
const line = `${arrow} ${label} ${status} ${targetUrl} ${durationMs}ms`;
|
|
64
|
+
if (!pc.isColorSupported) {
|
|
65
|
+
return line;
|
|
66
|
+
}
|
|
67
|
+
const statusText = status >= 500
|
|
68
|
+
? pc.red(String(status))
|
|
69
|
+
: status >= 400
|
|
70
|
+
? pc.yellow(String(status))
|
|
71
|
+
: pc.green(String(status));
|
|
72
|
+
return `${pc.dim(arrow)} ${formatCaptureBadge(label)} ${statusText} ${targetUrl} ${pc.dim(`${durationMs}ms`)}`;
|
|
73
|
+
};
|
|
74
|
+
const formatCaptureBadge = (label) => pc.yellow(pc.dim(` ${label} `));
|
|
75
|
+
export const formatMissingCaptureWarning = (options) => {
|
|
76
|
+
const transportNote = options.transport === "websocket" || options.transport === "eventsource"
|
|
77
|
+
? " WebSocket/EventSource may still fail in preview."
|
|
78
|
+
: "";
|
|
79
|
+
const arrow = "←";
|
|
80
|
+
const requestLabel = options.method ?? options.transport ?? "request";
|
|
81
|
+
const lines = [
|
|
82
|
+
`${arrow} ${requestLabel} ${options.origin}`,
|
|
83
|
+
` Browser tried to reach uncaptured loopback backend ${options.origin}. Restart with: ${options.suggestedCommand}.${transportNote}`,
|
|
84
|
+
];
|
|
85
|
+
if (!pc.isColorSupported) {
|
|
86
|
+
return lines.join("\n");
|
|
87
|
+
}
|
|
88
|
+
return lines.map((line) => pc.red(line)).join("\n");
|
|
89
|
+
};
|
|
90
|
+
export const formatRequestError = (path, message) => {
|
|
91
|
+
const arrow = "←";
|
|
92
|
+
const line = `${arrow} error ${path} ${message}`;
|
|
93
|
+
if (!pc.isColorSupported) {
|
|
94
|
+
return line;
|
|
95
|
+
}
|
|
96
|
+
return `${pc.dim(arrow)} ${pc.red("error")} ${path} ${pc.red(message)}`;
|
|
97
|
+
};
|
|
98
|
+
export const formatInvalidCaptureRouteError = (options) => {
|
|
99
|
+
const arrow = "←";
|
|
100
|
+
const origin = `${options.hostname}:${options.port}`;
|
|
101
|
+
const lines = [
|
|
102
|
+
`${arrow} ${options.method} ${options.path}`,
|
|
103
|
+
` Browser tried to reach blocked loopback backend ${origin}. Allow it with --capture ${origin}.`,
|
|
104
|
+
];
|
|
105
|
+
if (!pc.isColorSupported) {
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
return lines.map((line) => pc.red(line)).join("\n");
|
|
109
|
+
};
|
|
110
|
+
const REMOVED_ADMIN_TOKEN_FLAG_MESSAGE = "The --admin-token flag was removed. Set the LOCALPREVIEW_ADMIN_TOKEN environment variable instead.";
|
|
111
|
+
export const removedAdminTokenFlagMessage = () => REMOVED_ADMIN_TOKEN_FLAG_MESSAGE;
|
|
112
|
+
export const adminTokenRequiredMessage = () => `Admin commands require ${ADMIN_TOKEN_ENV}. Export it in your shell, then rerun the command.`;
|
|
113
|
+
const heading = (text) => (pc.isColorSupported ? pc.bold(text) : text);
|
|
114
|
+
const commandName = (text) => (pc.isColorSupported ? pc.cyan(text) : text);
|
|
115
|
+
const flagName = (text) => (pc.isColorSupported ? pc.yellow(text) : text);
|
|
116
|
+
const dim = (text) => (pc.isColorSupported ? pc.dim(text) : text);
|
|
117
|
+
const localFlagHelpLine = () => ` ${flagName("-l")}, ${flagName("--local")} Shorthand for ${flagName("--control-plane")} ${dim(LOCAL_CONTROL_PLANE_URL)}`;
|
|
118
|
+
const joinLines = (lines) => lines.join("\n");
|
|
119
|
+
const publicUsageLines = () => [
|
|
120
|
+
heading("localpreview — expose local dev servers through a public preview URL"),
|
|
121
|
+
"",
|
|
122
|
+
heading("Usage"),
|
|
123
|
+
` ${commandName("localpreview")} ${commandName("connect")} <port|target-url> [options]`,
|
|
124
|
+
` ${commandName("localpreview")} ${flagName("--version")}`,
|
|
125
|
+
` ${commandName("localpreview")} ${flagName("-h")} | ${flagName("--help")}`,
|
|
126
|
+
"",
|
|
127
|
+
heading("Commands"),
|
|
128
|
+
` ${commandName("connect")} Start a tunnel to your local target`,
|
|
129
|
+
"",
|
|
130
|
+
heading("Global options"),
|
|
131
|
+
` ${flagName("-h")}, ${flagName("--help")} Show help`,
|
|
132
|
+
` ${flagName("--version")} Show version (${CLI_PACKAGE_VERSION})`,
|
|
133
|
+
];
|
|
134
|
+
const publicExamplesLines = () => [
|
|
135
|
+
"",
|
|
136
|
+
heading("Examples"),
|
|
137
|
+
` ${dim("localpreview connect 3000")}`,
|
|
138
|
+
` ${dim("localpreview connect 5173 --capture localhost:4000")}`,
|
|
139
|
+
` ${dim("localpreview connect https://localhost:3000 --name my-app")}`,
|
|
140
|
+
` ${dim("localpreview connect 3000 -l")}`,
|
|
141
|
+
` ${dim("localpreview connect 3000 --control-plane https://staging.localpreview.dev")}`,
|
|
142
|
+
];
|
|
143
|
+
const adminSectionLines = () => [
|
|
144
|
+
"",
|
|
145
|
+
heading("Admin commands"),
|
|
146
|
+
` ${dim(`Requires ${ADMIN_TOKEN_ENV} in the environment.`)}`,
|
|
147
|
+
` ${commandName("clean")} Remove subdomains from the control plane`,
|
|
148
|
+
` ${commandName("list")} List active tunnels and inventory orphans`,
|
|
149
|
+
];
|
|
150
|
+
export const renderGlobalHelp = (options = {}) => {
|
|
151
|
+
const env = options.env ?? process.env;
|
|
152
|
+
const lines = [...publicUsageLines(), ...publicExamplesLines()];
|
|
153
|
+
if (hasAdminTokenInEnv(env)) {
|
|
154
|
+
lines.push(...adminSectionLines());
|
|
155
|
+
}
|
|
156
|
+
return joinLines(lines);
|
|
157
|
+
};
|
|
158
|
+
export const renderConnectHelp = () => joinLines([
|
|
159
|
+
heading("localpreview connect — start a tunnel to a local target"),
|
|
160
|
+
"",
|
|
161
|
+
heading("Usage"),
|
|
162
|
+
` ${commandName("localpreview")} ${commandName("connect")} <port|target-url> [options]`,
|
|
163
|
+
` ${commandName("localpreview")} <port|target-url> [options] ${dim("(deprecated alias)")}`,
|
|
164
|
+
"",
|
|
165
|
+
heading("Arguments"),
|
|
166
|
+
` <port|target-url> Local port (e.g. 3000) or full http(s) URL with explicit port`,
|
|
167
|
+
"",
|
|
168
|
+
heading("Options"),
|
|
169
|
+
` ${flagName("--name")} <subdomain> Request a public subdomain`,
|
|
170
|
+
` ${flagName("--capture")} <host:port> Expose an extra loopback backend through the preview URL`,
|
|
171
|
+
localFlagHelpLine(),
|
|
172
|
+
` ${flagName("--control-plane")} <url> Remote control plane base URL (HTTPS required off localhost)`,
|
|
173
|
+
` ${flagName("-h")}, ${flagName("--help")} Show this help`,
|
|
174
|
+
"",
|
|
175
|
+
heading("Examples"),
|
|
176
|
+
` ${dim("localpreview connect 3000")}`,
|
|
177
|
+
` ${dim("localpreview connect 5173 --capture localhost:4000")}`,
|
|
178
|
+
` ${dim("localpreview connect https://localhost:3000 --name my-app")}`,
|
|
179
|
+
` ${dim("localpreview connect 3000 -l")}`,
|
|
180
|
+
]);
|
|
181
|
+
export const renderCleanHelp = (options = {}) => {
|
|
182
|
+
const env = options.env ?? process.env;
|
|
183
|
+
if (!hasAdminTokenInEnv(env)) {
|
|
184
|
+
return joinLines([renderGlobalHelp(options), "", adminTokenRequiredMessage()]);
|
|
185
|
+
}
|
|
186
|
+
return joinLines([
|
|
187
|
+
heading("localpreview clean — admin subdomain cleanup"),
|
|
188
|
+
"",
|
|
189
|
+
heading("Usage"),
|
|
190
|
+
` ${commandName("localpreview")} ${commandName("clean")} <subdomain> [options]`,
|
|
191
|
+
` ${commandName("localpreview")} ${commandName("clean")} ${flagName("--all")} ${flagName("--force")} [options]`,
|
|
192
|
+
"",
|
|
193
|
+
heading("Arguments"),
|
|
194
|
+
` <subdomain> Subdomain to remove from the control plane`,
|
|
195
|
+
"",
|
|
196
|
+
heading("Options"),
|
|
197
|
+
` ${flagName("--force")} Force cleanup when relays are still connected`,
|
|
198
|
+
` ${flagName("--all")} Bulk cleanup (requires ${flagName("--force")})`,
|
|
199
|
+
localFlagHelpLine(),
|
|
200
|
+
` ${flagName("--control-plane")} <url> Control plane base URL`,
|
|
201
|
+
` ${flagName("-h")}, ${flagName("--help")} Show this help`,
|
|
202
|
+
"",
|
|
203
|
+
dim(`Authorization uses ${ADMIN_TOKEN_ENV} from the environment.`),
|
|
204
|
+
"",
|
|
205
|
+
heading("Examples"),
|
|
206
|
+
` ${dim("localpreview clean my-app --force")}`,
|
|
207
|
+
` ${dim("localpreview clean --all --force -l")}`,
|
|
208
|
+
` ${dim("localpreview clean demo --control-plane https://staging.localpreview.dev --force")}`,
|
|
209
|
+
]);
|
|
210
|
+
};
|
|
211
|
+
export const renderListHelp = (options = {}) => {
|
|
212
|
+
const env = options.env ?? process.env;
|
|
213
|
+
if (!hasAdminTokenInEnv(env)) {
|
|
214
|
+
return joinLines([renderGlobalHelp(options), "", adminTokenRequiredMessage()]);
|
|
215
|
+
}
|
|
216
|
+
return joinLines([
|
|
217
|
+
heading("localpreview list — inspect tunnel inventory"),
|
|
218
|
+
"",
|
|
219
|
+
heading("Usage"),
|
|
220
|
+
` ${commandName("localpreview")} ${commandName("list")} [options]`,
|
|
221
|
+
"",
|
|
222
|
+
dim("Shows active tunnels, Redis inventory drift, and sandbox leftovers without revealing tokens or preview URLs."),
|
|
223
|
+
"",
|
|
224
|
+
heading("Options"),
|
|
225
|
+
` ${flagName("--limit")} <n> Page size (default 100)`,
|
|
226
|
+
` ${flagName("--skip")} <n> Offset into the sorted inventory (default 0)`,
|
|
227
|
+
localFlagHelpLine(),
|
|
228
|
+
` ${flagName("--control-plane")} <url> Control plane base URL`,
|
|
229
|
+
` ${flagName("-h")}, ${flagName("--help")} Show this help`,
|
|
230
|
+
"",
|
|
231
|
+
dim(`Authorization uses ${ADMIN_TOKEN_ENV} from the environment.`),
|
|
232
|
+
"",
|
|
233
|
+
heading("Examples"),
|
|
234
|
+
` ${dim("localpreview list")}`,
|
|
235
|
+
` ${dim("localpreview list --limit 20 --skip 20 -l")}`,
|
|
236
|
+
` ${dim("localpreview list --control-plane <control-plane-url>")}`,
|
|
237
|
+
]);
|
|
238
|
+
};
|
|
239
|
+
const LIST_COLUMNS = [
|
|
240
|
+
"kind",
|
|
241
|
+
"subdomain",
|
|
242
|
+
"tunnel",
|
|
243
|
+
"age",
|
|
244
|
+
"relay",
|
|
245
|
+
"sandbox",
|
|
246
|
+
"sandbox id",
|
|
247
|
+
"note",
|
|
248
|
+
];
|
|
249
|
+
export const formatListTunnelsOutput = (response) => {
|
|
250
|
+
const lines = [];
|
|
251
|
+
lines.push(heading("Tunnel inventory"));
|
|
252
|
+
lines.push("");
|
|
253
|
+
for (const warning of response.warnings ?? []) {
|
|
254
|
+
lines.push(formatWarning(warning));
|
|
255
|
+
}
|
|
256
|
+
if ((response.warnings?.length ?? 0) > 0) {
|
|
257
|
+
lines.push("");
|
|
258
|
+
}
|
|
259
|
+
lines.push(heading("Summary"));
|
|
260
|
+
lines.push(` tracked: ${response.counts.tracked} redis orphans: ${response.counts["redis-orphan"]} sandbox orphans: ${response.counts["sandbox-orphan"]}`);
|
|
261
|
+
lines.push("");
|
|
262
|
+
lines.push(heading("Page"));
|
|
263
|
+
lines.push(` showing: ${response.items.length} of ${response.total} skip: ${response.skip} limit: ${response.limit}`);
|
|
264
|
+
const nextSkip = response.skip + response.limit;
|
|
265
|
+
if (nextSkip < response.total) {
|
|
266
|
+
lines.push(` next: localpreview list --skip ${nextSkip} --limit ${response.limit}`);
|
|
267
|
+
}
|
|
268
|
+
if (response.items.length === 0) {
|
|
269
|
+
lines.push("");
|
|
270
|
+
lines.push(dim("No active tunnels or inventory orphans on this page."));
|
|
271
|
+
return joinLines(lines);
|
|
272
|
+
}
|
|
273
|
+
const rows = response.items.map(formatListTunnelRow);
|
|
274
|
+
const widths = LIST_COLUMNS.map((column, index) => Math.max(column.length, ...rows.map((row) => row[index]?.length ?? 0)));
|
|
275
|
+
lines.push("");
|
|
276
|
+
lines.push(heading("Items"));
|
|
277
|
+
lines.push(LIST_COLUMNS.map((column, index) => column.padEnd(widths[index] ?? column.length)).join(" "));
|
|
278
|
+
lines.push(widths.map((width) => "-".repeat(width)).join(" "));
|
|
279
|
+
for (const row of rows) {
|
|
280
|
+
lines.push(row
|
|
281
|
+
.map((cell, index) => cell.padEnd(widths[index] ?? cell.length))
|
|
282
|
+
.join(" "));
|
|
283
|
+
}
|
|
284
|
+
if (response.items.some((item) => item.relayHealthWarning !== undefined && item.relayHealthWarning.length > 0)) {
|
|
285
|
+
lines.push("");
|
|
286
|
+
lines.push(dim("Relay values ending in ! include a health warning from the control plane."));
|
|
287
|
+
}
|
|
288
|
+
return joinLines(lines);
|
|
289
|
+
};
|
|
290
|
+
const formatListTunnelRow = (item) => [
|
|
291
|
+
formatListItemKind(item.kind),
|
|
292
|
+
item.subdomain ?? "-",
|
|
293
|
+
formatShortId(item.tunnelId),
|
|
294
|
+
formatCompactAge(item.activeForMs),
|
|
295
|
+
formatRelayHealth(item),
|
|
296
|
+
item.sandboxStatus ?? "-",
|
|
297
|
+
formatShortId(item.sandboxId),
|
|
298
|
+
item.reason ?? item.orphanReason ?? "-",
|
|
299
|
+
];
|
|
300
|
+
const formatListItemKind = (kind) => {
|
|
301
|
+
switch (kind) {
|
|
302
|
+
case "tracked-tunnel":
|
|
303
|
+
return "tracked";
|
|
304
|
+
case "redis-orphan":
|
|
305
|
+
return "redis-orphan";
|
|
306
|
+
case "redis-index-orphan":
|
|
307
|
+
return "redis-index";
|
|
308
|
+
case "sandbox-orphan":
|
|
309
|
+
return "sandbox-orphan";
|
|
310
|
+
default: {
|
|
311
|
+
const unreachable = kind;
|
|
312
|
+
throw new Error(`Unhandled tunnel list item kind: ${String(unreachable)}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
const formatRelayHealth = (item) => {
|
|
317
|
+
const base = item.relayHealth;
|
|
318
|
+
if (item.relayHealthWarning !== undefined && item.relayHealthWarning.length > 0) {
|
|
319
|
+
return `${base}!`;
|
|
320
|
+
}
|
|
321
|
+
return base;
|
|
322
|
+
};
|
|
323
|
+
export const formatCompactAge = (activeForMs) => {
|
|
324
|
+
if (activeForMs === undefined) {
|
|
325
|
+
return "unknown";
|
|
326
|
+
}
|
|
327
|
+
const totalMinutes = Math.floor(activeForMs / 60_000);
|
|
328
|
+
if (totalMinutes < 1) {
|
|
329
|
+
return `${Math.max(1, Math.floor(activeForMs / 1_000))}s`;
|
|
330
|
+
}
|
|
331
|
+
if (totalMinutes < 60) {
|
|
332
|
+
return `${totalMinutes}m`;
|
|
333
|
+
}
|
|
334
|
+
const totalHours = Math.floor(totalMinutes / 60);
|
|
335
|
+
const minutes = totalMinutes % 60;
|
|
336
|
+
if (totalHours < 24) {
|
|
337
|
+
return minutes === 0 ? `${totalHours}h` : `${totalHours}h ${minutes}m`;
|
|
338
|
+
}
|
|
339
|
+
const days = Math.floor(totalHours / 24);
|
|
340
|
+
const hours = totalHours % 24;
|
|
341
|
+
return hours === 0 ? `${days}d` : `${days}d ${hours}h`;
|
|
342
|
+
};
|
|
343
|
+
const formatShortId = (value) => {
|
|
344
|
+
if (value === undefined || value.length === 0) {
|
|
345
|
+
return "-";
|
|
346
|
+
}
|
|
347
|
+
if (value.length <= 12) {
|
|
348
|
+
return value;
|
|
349
|
+
}
|
|
350
|
+
return `${value.slice(0, 8)}…`;
|
|
351
|
+
};
|
package/dist/command.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ import { Effect } from "effect";
|
|
|
2
2
|
import { CliUsageError, type CliRuntimeError } from "./errors.js";
|
|
3
3
|
export declare const runCli: (argv: ReadonlyArray<string>) => Effect.Effect<void, CliRuntimeError | CliUsageError>;
|
|
4
4
|
export declare const normalizeCliArgs: (argv: ReadonlyArray<string>) => ReadonlyArray<string>;
|
|
5
|
+
export declare const buildConnectOriginalArgv: (argv: ReadonlyArray<string>, legacy: boolean) => ReadonlyArray<string>;
|
|
5
6
|
//# sourceMappingURL=command.d.ts.map
|
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;AAE1C,eAAO,MAAM,wBAAwB,GACnC,MAAM,aAAa,CAAC,MAAM,CAAC,EAC3B,QAAQ,OAAO,KACd,aAAa,CAAC,MAAM,CAAmE,CAAC"}
|