localpreview 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ import type { CreateTunnelResponse, TunnelTarget } from "@localpreview/protocol";
2
+ type Theme = {
3
+ readonly color: boolean;
4
+ };
5
+ export declare const formatConnectReady: (input: {
6
+ readonly target: TunnelTarget;
7
+ readonly tunnel: CreateTunnelResponse;
8
+ }, theme?: Theme) => string;
9
+ export declare const formatRelayConnected: (theme?: Theme) => string;
10
+ export declare const formatRelayReconnect: (message: string, theme?: Theme) => string;
11
+ export declare const formatWarning: (message: string, theme?: Theme) => string;
12
+ export declare const formatError: (message: string, theme?: Theme) => string;
13
+ export declare const formatRequestStart: (input: {
14
+ readonly method: string;
15
+ readonly path: string;
16
+ }, theme?: Theme) => string;
17
+ export declare const formatRequestComplete: (input: {
18
+ readonly durationMs: number;
19
+ readonly path: string;
20
+ readonly status: number;
21
+ }, theme?: Theme) => string;
22
+ export declare const formatRequestFailure: (input: {
23
+ readonly message: string;
24
+ readonly path: string;
25
+ }, theme?: Theme) => string;
26
+ export declare const formatCleanAllResult: (input: {
27
+ readonly failedToStop: number;
28
+ readonly stopped: number;
29
+ readonly total: number;
30
+ }, theme?: Theme) => string;
31
+ export declare const formatCleanSubdomainResult: (input: {
32
+ readonly cleaned: boolean;
33
+ readonly relayStopped: boolean;
34
+ readonly subdomain: string;
35
+ readonly tunnelId?: string;
36
+ }, theme?: Theme) => string;
37
+ export {};
38
+ //# sourceMappingURL=ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEjF,KAAK,KAAK,GAAG;IACX,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB,CAAC;AA0BF,eAAO,MAAM,kBAAkB,GAC7B,OAAO;IACL,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;CACvC,EACD,QAAO,KAAsB,KAC5B,MAaF,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,QAAO,KAAsB,KAAG,MACpB,CAAC;AAElD,eAAO,MAAM,oBAAoB,GAAI,SAAS,MAAM,EAAE,QAAO,KAAsB,KAAG,MACf,CAAC;AAExE,eAAO,MAAM,aAAa,GAAI,SAAS,MAAM,EAAE,QAAO,KAAsB,KAAG,MAC7B,CAAC;AAEnD,eAAO,MAAM,WAAW,GAAI,SAAS,MAAM,EAAE,QAAO,KAAsB,KAAG,MAO5E,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,OAAO;IACL,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,EACD,QAAO,KAAsB,KAC5B,MACuH,CAAC;AAE3H,eAAO,MAAM,qBAAqB,GAChC,OAAO;IACL,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,EACD,QAAO,KAAsB,KAC5B,MAOU,CAAC;AAEd,eAAO,MAAM,oBAAoB,GAC/B,OAAO;IACL,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,EACD,QAAO,KAAsB,KAC5B,MAOU,CAAC;AAEd,eAAO,MAAM,oBAAoB,GAC/B,OAAO;IACL,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB,EACD,QAAO,KAAsB,KAC5B,MAOW,CAAC;AAEf,eAAO,MAAM,0BAA0B,GACrC,OAAO;IACL,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B,EACD,QAAO,KAAsB,KAC5B,MAmBF,CAAC"}
package/dist/ui.js ADDED
@@ -0,0 +1,109 @@
1
+ const ansi = {
2
+ bold: "\u001b[1m",
3
+ cyan: "\u001b[36m",
4
+ dim: "\u001b[2m",
5
+ green: "\u001b[32m",
6
+ red: "\u001b[31m",
7
+ reset: "\u001b[0m",
8
+ yellow: "\u001b[33m",
9
+ };
10
+ const defaultTheme = () => ({
11
+ color: process.stdout.isTTY === true &&
12
+ process.env.NO_COLOR === undefined &&
13
+ process.env.TERM !== "dumb",
14
+ });
15
+ const paint = (theme, style, value) => theme.color ? `${ansi[style]}${value}${ansi.reset}` : value;
16
+ const label = (theme, value) => ` ${paint(theme, "dim", value.padEnd(12))}`;
17
+ const line = (theme, name, value) => `${label(theme, name)}${value}`;
18
+ export const formatConnectReady = (input, theme = defaultTheme()) => {
19
+ const targetUrl = `${input.target.protocol}://${input.target.hostname}:${input.target.port}`;
20
+ return [
21
+ paint(theme, "bold", "LocalPreview tunnel is live"),
22
+ "",
23
+ line(theme, "Public URL", paint(theme, "cyan", input.tunnel.publicUrl)),
24
+ line(theme, "Forwarding", targetUrl),
25
+ line(theme, "Subdomain", input.tunnel.subdomain),
26
+ line(theme, "Expires", formatTimestamp(input.tunnel.expiresAt)),
27
+ "",
28
+ ` ${paint(theme, "dim", "Press Ctrl+C to stop forwarding.")}`,
29
+ ].join("\n");
30
+ };
31
+ export const formatRelayConnected = (theme = defaultTheme()) => `${paint(theme, "green", "[relay]")} connected`;
32
+ export const formatRelayReconnect = (message, theme = defaultTheme()) => `${paint(theme, "yellow", "[relay]")} ${message}; reconnecting in 1s`;
33
+ export const formatWarning = (message, theme = defaultTheme()) => `${paint(theme, "yellow", "[warn]")} ${message}`;
34
+ export const formatError = (message, theme = defaultTheme()) => {
35
+ const [first = "Unknown failure.", ...rest] = message.split("\n");
36
+ return [
37
+ `${paint(theme, "red", "[error]")} ${first}`,
38
+ ...rest.map((part) => ` ${part}`),
39
+ ].join("\n");
40
+ };
41
+ export const formatRequestStart = (input, theme = defaultTheme()) => `${paint(theme, "dim", timestamp())} ${paint(theme, "cyan", "->")} ${input.method.padEnd(6)} ${displayPath(input.path)}`;
42
+ export const formatRequestComplete = (input, theme = defaultTheme()) => [
43
+ paint(theme, "dim", timestamp()),
44
+ paint(theme, statusStyle(input.status), "<-"),
45
+ String(input.status).padEnd(3),
46
+ displayPath(input.path),
47
+ paint(theme, "dim", `${input.durationMs}ms`),
48
+ ].join(" ");
49
+ export const formatRequestFailure = (input, theme = defaultTheme()) => [
50
+ paint(theme, "dim", timestamp()),
51
+ paint(theme, "red", "<-"),
52
+ "ERR",
53
+ displayPath(input.path),
54
+ paint(theme, "red", input.message),
55
+ ].join(" ");
56
+ export const formatCleanAllResult = (input, theme = defaultTheme()) => [
57
+ paint(theme, "green", "Cleanup complete"),
58
+ "",
59
+ line(theme, "Subdomains", String(input.total)),
60
+ line(theme, "Stopped", String(input.stopped)),
61
+ line(theme, "Failed", String(input.failedToStop)),
62
+ ].join("\n");
63
+ export const formatCleanSubdomainResult = (input, theme = defaultTheme()) => {
64
+ if (!input.cleaned) {
65
+ return `${paint(theme, "green", "Already clean")} ${input.subdomain}`;
66
+ }
67
+ const details = input.tunnelId === undefined
68
+ ? []
69
+ : [
70
+ line(theme, "Tunnel", input.tunnelId),
71
+ line(theme, "Relay", input.relayStopped ? "stopped" : "removed from store"),
72
+ ];
73
+ return [
74
+ paint(theme, "green", "Cleanup complete"),
75
+ "",
76
+ line(theme, "Subdomain", input.subdomain),
77
+ ...details,
78
+ ].join("\n");
79
+ };
80
+ const statusStyle = (status) => {
81
+ if (status >= 500) {
82
+ return "red";
83
+ }
84
+ if (status >= 400) {
85
+ return "yellow";
86
+ }
87
+ return "green";
88
+ };
89
+ const displayPath = (path) => {
90
+ try {
91
+ return new URL(path, "http://localpreview.local").pathname;
92
+ }
93
+ catch {
94
+ return path.split("?")[0] ?? path;
95
+ }
96
+ };
97
+ const timestamp = () => new Intl.DateTimeFormat("en-US", {
98
+ hour: "2-digit",
99
+ hour12: false,
100
+ minute: "2-digit",
101
+ second: "2-digit",
102
+ }).format(new Date());
103
+ const formatTimestamp = (value) => {
104
+ const date = new Date(value);
105
+ if (Number.isNaN(date.getTime())) {
106
+ return value;
107
+ }
108
+ return date.toISOString().replace(".000Z", "Z");
109
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "localpreview",
3
+ "version": "0.1.0",
4
+ "bin": {
5
+ "localpreview": "./dist/index.js"
6
+ },
7
+ "type": "module",
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "types": "./dist/index.d.ts",
13
+ "dependencies": {
14
+ "@effect/platform-node": "beta",
15
+ "effect": "beta",
16
+ "ws": "latest",
17
+ "@localpreview/protocol": "0.1.0"
18
+ },
19
+ "devDependencies": {
20
+ "@effect/vitest": "beta",
21
+ "@types/node": "latest",
22
+ "@types/ws": "latest",
23
+ "tsx": "latest",
24
+ "typescript": "latest",
25
+ "vitest": "latest"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.build.json",
29
+ "dev": "tsx src/index.ts",
30
+ "lint": "oxlint .",
31
+ "test": "vitest run --passWithNoTests",
32
+ "typecheck": "tsc -p tsconfig.json --noEmit"
33
+ }
34
+ }