@vincentt-xr/cli 0.1.4 → 0.2.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/README.md +94 -0
- package/dist/cli/args.d.ts +13 -0
- package/dist/cli/args.js +41 -0
- package/dist/cli/index.js +362 -92
- package/dist/lib/backend.d.ts +45 -5
- package/dist/lib/backend.js +99 -18
- package/dist/lib/diagnostics.d.ts +27 -2
- package/dist/lib/diagnostics.js +73 -8
- package/dist/lib/initCommand.d.ts +14 -0
- package/dist/lib/initCommand.js +75 -0
- package/dist/lib/lifecycle.d.ts +73 -15
- package/dist/lib/lifecycle.js +111 -114
- package/dist/lib/org.d.ts +29 -0
- package/dist/lib/org.js +45 -0
- package/dist/lib/projects.d.ts +82 -0
- package/dist/lib/projects.js +217 -0
- package/dist/lib/relayInfo.d.ts +41 -3
- package/dist/lib/relayInfo.js +94 -0
- package/dist/lib/whoami.d.ts +11 -0
- package/dist/lib/whoami.js +38 -0
- package/dist/login/authBackend.d.ts +30 -0
- package/dist/login/authBackend.js +80 -0
- package/dist/login/guard.d.ts +13 -0
- package/dist/login/guard.js +17 -0
- package/dist/login/loginCommand.d.ts +39 -0
- package/dist/login/loginCommand.js +72 -0
- package/dist/login/pkce.d.ts +47 -0
- package/dist/login/pkce.js +106 -0
- package/dist/login/pkceLogin.d.ts +53 -0
- package/dist/login/pkceLogin.js +152 -0
- package/dist/mcp/server.js +47 -20
- package/dist/preview/api.d.ts +84 -0
- package/dist/preview/api.js +174 -0
- package/dist/preview/command.d.ts +96 -0
- package/dist/preview/command.js +276 -0
- package/dist/preview/copy.d.ts +209 -0
- package/dist/preview/copy.js +444 -0
- package/dist/preview/index.d.ts +2 -3
- package/dist/preview/index.js +5 -6
- package/dist/preview/proxy.d.ts +12 -1
- package/dist/preview/proxy.js +17 -7
- package/dist/preview/qr.d.ts +30 -0
- package/dist/preview/qr.js +89 -0
- package/dist/preview/runner.d.ts +155 -30
- package/dist/preview/runner.js +219 -68
- package/dist/preview/stream.d.ts +52 -0
- package/dist/preview/stream.js +171 -0
- package/dist/preview/viewers.d.ts +113 -0
- package/dist/preview/viewers.js +242 -0
- package/dist/relay/server.d.ts +32 -0
- package/dist/relay/server.js +80 -12
- package/dist/relay/store.d.ts +32 -3
- package/dist/relay/store.js +76 -22
- package/dist/relay/testers.d.ts +90 -0
- package/dist/relay/testers.js +144 -0
- package/dist/shared/authError.d.ts +1 -0
- package/dist/shared/authError.js +11 -0
- package/dist/shared/config.d.ts +73 -5
- package/dist/shared/config.js +133 -20
- package/package.json +3 -7
- package/dist/cli/deprecated/harness-mcp.d.ts +0 -2
- package/dist/cli/deprecated/harness-mcp.js +0 -7
- package/dist/cli/deprecated/harness-relay.d.ts +0 -2
- package/dist/cli/deprecated/harness-relay.js +0 -7
- package/dist/cli/deprecated/harness.d.ts +0 -2
- package/dist/cli/deprecated/harness.js +0 -7
- package/dist/login/login.d.ts +0 -34
- package/dist/login/login.js +0 -148
- package/dist/preview/cloudflared.d.ts +0 -13
- package/dist/preview/cloudflared.js +0 -46
- package/dist/preview/tunnel.d.ts +0 -14
- package/dist/preview/tunnel.js +0 -28
package/dist/preview/proxy.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
2
|
import type { Duplex } from "node:stream";
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* onOriginError is where the creator's dev server being down is ACTUALLY
|
|
5
|
+
* OBSERVED.
|
|
6
|
+
*
|
|
7
|
+
* The tunnel client's own `onOriginError` fires on a socket error, and it never
|
|
8
|
+
* sees one: this proxy sits between the two and answers a refused connection
|
|
9
|
+
* with its own clean 502, so the client reads a perfectly valid HTTP response.
|
|
10
|
+
* The 502 advisory — the most common failure in the whole feature — was
|
|
11
|
+
* unreachable until the hook moved here. Found by killing a real dev server
|
|
12
|
+
* under a live preview, not by reading the code.
|
|
13
|
+
*/
|
|
14
|
+
export declare function proxyWeb(req: IncomingMessage, res: ServerResponse, targetPort: number, onOriginError?: () => void): void;
|
|
4
15
|
export declare function proxyWs(req: IncomingMessage, socket: Duplex, head: Buffer, targetPort: number): void;
|
package/dist/preview/proxy.js
CHANGED
|
@@ -9,7 +9,18 @@ import { request, } from "node:http";
|
|
|
9
9
|
function localHeaders(headers, targetPort) {
|
|
10
10
|
return { ...headers, host: `localhost:${targetPort}` };
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* onOriginError is where the creator's dev server being down is ACTUALLY
|
|
14
|
+
* OBSERVED.
|
|
15
|
+
*
|
|
16
|
+
* The tunnel client's own `onOriginError` fires on a socket error, and it never
|
|
17
|
+
* sees one: this proxy sits between the two and answers a refused connection
|
|
18
|
+
* with its own clean 502, so the client reads a perfectly valid HTTP response.
|
|
19
|
+
* The 502 advisory — the most common failure in the whole feature — was
|
|
20
|
+
* unreachable until the hook moved here. Found by killing a real dev server
|
|
21
|
+
* under a live preview, not by reading the code.
|
|
22
|
+
*/
|
|
23
|
+
export function proxyWeb(req, res, targetPort, onOriginError) {
|
|
13
24
|
const proxyReq = request({
|
|
14
25
|
host: "127.0.0.1",
|
|
15
26
|
port: targetPort,
|
|
@@ -17,12 +28,10 @@ export function proxyWeb(req, res, targetPort) {
|
|
|
17
28
|
method: req.method,
|
|
18
29
|
headers: localHeaders(req.headers, targetPort),
|
|
19
30
|
}, (proxyRes) => {
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// preview response so neither the CDN nor the device caches it, and drop the
|
|
25
|
-
// validators that would otherwise drive a 304 back to a cached copy.
|
|
31
|
+
// A preview response must never be cached: a capability URL in a shared
|
|
32
|
+
// or intermediary cache is a capability store, and a device that reloads
|
|
33
|
+
// after an edit must get the new bundle rather than a validated copy of
|
|
34
|
+
// the old one. The validators go with it, or a 304 defeats the point.
|
|
26
35
|
const headers = { ...proxyRes.headers, "cache-control": "no-store" };
|
|
27
36
|
delete headers.etag;
|
|
28
37
|
delete headers["last-modified"];
|
|
@@ -30,6 +39,7 @@ export function proxyWeb(req, res, targetPort) {
|
|
|
30
39
|
proxyRes.pipe(res);
|
|
31
40
|
});
|
|
32
41
|
proxyReq.on("error", () => {
|
|
42
|
+
onOriginError?.();
|
|
33
43
|
if (!res.headersSent)
|
|
34
44
|
res.writeHead(502);
|
|
35
45
|
res.end();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive-only decoration DEGRADES, it does not disappear. The QR renders
|
|
3
|
+
* on a TTY and is omitted when piped — an agent has no camera, and a thousand
|
|
4
|
+
* block characters in a tool transcript is a real cost to the creator's context
|
|
5
|
+
* window. The URL beside it is always present and is the actual payload.
|
|
6
|
+
*/
|
|
7
|
+
export declare function shouldRenderQr(opts: {
|
|
8
|
+
isTTY: boolean;
|
|
9
|
+
noQr: boolean;
|
|
10
|
+
columns?: number;
|
|
11
|
+
}): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* renderModules turns a boolean module matrix into half-block rows.
|
|
14
|
+
*
|
|
15
|
+
* The convention is INVERTED from the obvious one: a set module is rendered as
|
|
16
|
+
* the BACKGROUND (a space) and an unset module as a block. Terminals are
|
|
17
|
+
* overwhelmingly light-on-dark, and a scanner needs the quiet zone and the light
|
|
18
|
+
* modules to be the brighter of the two. Getting this backwards produces a
|
|
19
|
+
* symbol that looks perfect and scans on nothing.
|
|
20
|
+
*/
|
|
21
|
+
export declare function renderModules(modules: boolean[][]): string[];
|
|
22
|
+
/**
|
|
23
|
+
* Build the terminal QR for a preview URL.
|
|
24
|
+
*
|
|
25
|
+
* A ~62-character preview URL fits a version-3/4 symbol at error-correction
|
|
26
|
+
* level L. L rather than M because the terminal render is pixel-perfect — there
|
|
27
|
+
* is no print smudge or camera blur to recover from at the source, only at the
|
|
28
|
+
* lens — and every step up in correction level adds modules, which adds rows.
|
|
29
|
+
*/
|
|
30
|
+
export declare function renderTerminalQr(url: string): Promise<string[]>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// The terminal QR (Flow L13 step 4) — the handoff, and the moment the feature
|
|
2
|
+
// exists for. It sits above the fold with the URL.
|
|
3
|
+
//
|
|
4
|
+
// Half-block glyphs, so one terminal row carries TWO module rows: a 25x25
|
|
5
|
+
// symbol renders in ~13 rows instead of 25, which is what keeps the QR and the
|
|
6
|
+
// URL on one screen. Rendering one module per row would push the URL below the
|
|
7
|
+
// fold on a standard terminal, and the URL is the payload.
|
|
8
|
+
import QRCode from "qrcode";
|
|
9
|
+
/**
|
|
10
|
+
* Interactive-only decoration DEGRADES, it does not disappear. The QR renders
|
|
11
|
+
* on a TTY and is omitted when piped — an agent has no camera, and a thousand
|
|
12
|
+
* block characters in a tool transcript is a real cost to the creator's context
|
|
13
|
+
* window. The URL beside it is always present and is the actual payload.
|
|
14
|
+
*/
|
|
15
|
+
export function shouldRenderQr(opts) {
|
|
16
|
+
if (opts.noQr)
|
|
17
|
+
return false;
|
|
18
|
+
if (!opts.isTTY)
|
|
19
|
+
return false;
|
|
20
|
+
// A symbol narrower than the terminal or it wraps, and a wrapped QR does not
|
|
21
|
+
// scan at all — which is worse than no QR, because the creator tries.
|
|
22
|
+
return (opts.columns ?? 80) >= 33;
|
|
23
|
+
}
|
|
24
|
+
const UPPER = "▀";
|
|
25
|
+
const LOWER = "▄";
|
|
26
|
+
const FULL = "█";
|
|
27
|
+
const BLANK = " ";
|
|
28
|
+
/**
|
|
29
|
+
* renderModules turns a boolean module matrix into half-block rows.
|
|
30
|
+
*
|
|
31
|
+
* The convention is INVERTED from the obvious one: a set module is rendered as
|
|
32
|
+
* the BACKGROUND (a space) and an unset module as a block. Terminals are
|
|
33
|
+
* overwhelmingly light-on-dark, and a scanner needs the quiet zone and the light
|
|
34
|
+
* modules to be the brighter of the two. Getting this backwards produces a
|
|
35
|
+
* symbol that looks perfect and scans on nothing.
|
|
36
|
+
*/
|
|
37
|
+
export function renderModules(modules) {
|
|
38
|
+
const rows = [];
|
|
39
|
+
for (let y = 0; y < modules.length; y += 2) {
|
|
40
|
+
const top = modules[y] ?? [];
|
|
41
|
+
const bottom = modules[y + 1];
|
|
42
|
+
let line = "";
|
|
43
|
+
for (let x = 0; x < top.length; x++) {
|
|
44
|
+
// `dark === true` means a set (black) module, which renders as absence.
|
|
45
|
+
const t = top[x] === true;
|
|
46
|
+
const b = bottom === undefined ? false : bottom[x] === true;
|
|
47
|
+
if (t && b)
|
|
48
|
+
line += BLANK;
|
|
49
|
+
else if (t)
|
|
50
|
+
line += LOWER;
|
|
51
|
+
else if (b)
|
|
52
|
+
line += UPPER;
|
|
53
|
+
else
|
|
54
|
+
line += FULL;
|
|
55
|
+
}
|
|
56
|
+
rows.push(line);
|
|
57
|
+
}
|
|
58
|
+
return rows;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Build the terminal QR for a preview URL.
|
|
62
|
+
*
|
|
63
|
+
* A ~62-character preview URL fits a version-3/4 symbol at error-correction
|
|
64
|
+
* level L. L rather than M because the terminal render is pixel-perfect — there
|
|
65
|
+
* is no print smudge or camera blur to recover from at the source, only at the
|
|
66
|
+
* lens — and every step up in correction level adds modules, which adds rows.
|
|
67
|
+
*/
|
|
68
|
+
export async function renderTerminalQr(url) {
|
|
69
|
+
const qr = QRCode.create(url, { errorCorrectionLevel: "L" });
|
|
70
|
+
const size = qr.modules.size;
|
|
71
|
+
const data = qr.modules.data;
|
|
72
|
+
// A two-module quiet zone on every side. The spec asks for four; two is the
|
|
73
|
+
// practical floor that still scans against a terminal background, and it
|
|
74
|
+
// costs two rows rather than four.
|
|
75
|
+
const quiet = 2;
|
|
76
|
+
const total = size + quiet * 2;
|
|
77
|
+
const grid = [];
|
|
78
|
+
for (let y = 0; y < total; y++) {
|
|
79
|
+
const row = [];
|
|
80
|
+
for (let x = 0; x < total; x++) {
|
|
81
|
+
const my = y - quiet;
|
|
82
|
+
const mx = x - quiet;
|
|
83
|
+
const inside = my >= 0 && my < size && mx >= 0 && mx < size;
|
|
84
|
+
row.push(inside ? data[my * size + mx] === 1 : false);
|
|
85
|
+
}
|
|
86
|
+
grid.push(row);
|
|
87
|
+
}
|
|
88
|
+
return renderModules(grid);
|
|
89
|
+
}
|
package/dist/preview/runner.d.ts
CHANGED
|
@@ -1,53 +1,178 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ControlKind, type TunnelState, type UAClass } from "@vincentt-xr/harness/tunnel";
|
|
2
|
+
import type { DiagEvent } from "@vincentt-xr/harness/events";
|
|
3
|
+
import { type EventTags } from "../relay/server.js";
|
|
4
|
+
import type { ResolvedConfig } from "../shared/config.js";
|
|
5
|
+
/** Why a preview ended. Each maps to exactly one screen (Flow L20/L21). */
|
|
6
|
+
export type PreviewEndReason = "stopped"
|
|
7
|
+
/**
|
|
8
|
+
* `stopped` reported by the API, not observed locally. A separate member from
|
|
9
|
+
* `stopped` because this window was not the actor: it holds no timer and no
|
|
10
|
+
* viewer count, so it must not print the stats the local screen prints.
|
|
11
|
+
*/
|
|
12
|
+
| "stopped_elsewhere"
|
|
13
|
+
/**
|
|
14
|
+
* The connection closed and the session did not come back. Deliberately
|
|
15
|
+
* distinct from `grace_exhausted`, which is the CLI's own observation after
|
|
16
|
+
* it watched the link go and tried; this one is reported to a process that
|
|
17
|
+
* may never have witnessed the drop.
|
|
18
|
+
*/
|
|
19
|
+
| "disconnected" | "hard_max" | "superseded" | "grace_exhausted"
|
|
20
|
+
/**
|
|
21
|
+
* The drop PAST the cap — the edge refused the reconnect with a 409 because
|
|
22
|
+
* `graceUsed` is spent. Distinct from `grace_exhausted` (the grace window
|
|
23
|
+
* elapsed without the client getting back): same remedy, different diagnosis,
|
|
24
|
+
* and the copy explains the bound only at the moment it bites.
|
|
25
|
+
*/
|
|
26
|
+
| "grace_cap" | "authz_revoked" | "authz_unconfirmed"
|
|
27
|
+
/**
|
|
28
|
+
* The 30-minute heartbeat-silence flip. NEVER reported as `hard_max`: a
|
|
29
|
+
* session ended at minute 30 has not reached its 12-hour ceiling, and saying
|
|
30
|
+
* so is a false statement about the product's own behaviour.
|
|
31
|
+
*/
|
|
32
|
+
| "abandoned" | "egress_cap";
|
|
2
33
|
export interface StartPreviewOptions {
|
|
3
34
|
/** Project directory — its .vincentt binding + dev script drive the preview. */
|
|
4
35
|
projectCwd: string;
|
|
5
|
-
/**
|
|
6
|
-
|
|
36
|
+
/** The API host + PAT. Resolved by the caller so the runner makes no policy. */
|
|
37
|
+
config: ResolvedConfig;
|
|
38
|
+
projectId: string;
|
|
39
|
+
/** App dev-serve port (default 5173). A server already here is REUSED. */
|
|
7
40
|
appPort?: number;
|
|
8
|
-
/** Relay port. Default: an OS-assigned free port (returned as relayPort). */
|
|
9
41
|
relayPort?: number;
|
|
10
|
-
/** Front-proxy port the tunnel points at. Default: an OS-assigned free port. */
|
|
11
42
|
frontPort?: number;
|
|
12
43
|
/** Path routed to the relay instead of the app (default /__harness). */
|
|
13
44
|
harnessPath?: string;
|
|
14
|
-
/** Command that starts the app dev serve on $PORT (default `npm run dev`). */
|
|
15
45
|
devCommand?: string[];
|
|
16
|
-
/** stdio for the app dev serve. "ignore" (default) keeps an MCP server's stdout
|
|
17
|
-
* clean; a CLI can pass "inherit" to surface build output. */
|
|
18
46
|
appStdio?: "ignore" | "inherit";
|
|
19
|
-
/** How long to wait for cloudflared to register before giving up (default 90s).
|
|
20
|
-
* Generous because on a network that blocks the QUIC edge, cloudflared spends
|
|
21
|
-
* ~20-30s failing QUIC before it falls back to the http2 edge, which does work. */
|
|
22
|
-
registerTimeoutMs?: number;
|
|
23
|
-
/** How long to wait for the app to serve before returning anyway (default 8s).
|
|
24
|
-
* Kept short so an MCP tool call stays within strict clients' ~30s timeout;
|
|
25
|
-
* a not-yet-ready app still returns (appReady=false), it isn't an error. */
|
|
26
47
|
appReadyTimeoutMs?: number;
|
|
27
|
-
/** Progress sink
|
|
48
|
+
/** Progress sink. Never write app stdout to an MCP server's stdout. */
|
|
28
49
|
onLog?: (message: string) => void;
|
|
50
|
+
/**
|
|
51
|
+
* A viewer label appearing on a stream. Fires per OPEN, so the roster's
|
|
52
|
+
* per-label idempotence is what keeps one device from announcing itself once
|
|
53
|
+
* per asset.
|
|
54
|
+
*
|
|
55
|
+
* A label is per CONNECTION (the edge binds it in `ViewerForConn` and holds
|
|
56
|
+
* it for the connection's life), so a page plus nine assets is ONE label and
|
|
57
|
+
* the dedupe above is what keeps it to one line. It is NOT per device: under
|
|
58
|
+
* HTTP/1.1 a browser cannot multiplex, so one phone opens several.
|
|
59
|
+
*/
|
|
60
|
+
onViewer?: (label: string, uaClass: UAClass) => void;
|
|
61
|
+
/**
|
|
62
|
+
* The other half of `onViewer`: a viewer's connection closed. Fires once,
|
|
63
|
+
* carrying the label to drop from the roster.
|
|
64
|
+
*/
|
|
65
|
+
onViewerGone?: (label: string) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Diagnostics landing at the relay, tagged with the connection that produced
|
|
68
|
+
* them and the tester that connection resolved to. Either tag can be absent:
|
|
69
|
+
* the tester is unknown until the app mounts and the harness introduces
|
|
70
|
+
* itself, so the first events of a load are routinely un-attributed.
|
|
71
|
+
*/
|
|
72
|
+
onEvents?: (events: DiagEvent[], tags: EventTags) => void;
|
|
73
|
+
/**
|
|
74
|
+
* The tester cap was reached, once per refused session. The CLI prints one
|
|
75
|
+
* line; the connection's events are still stored, just un-attributed.
|
|
76
|
+
*/
|
|
77
|
+
onCapReached?: () => void;
|
|
78
|
+
/** The tunnel's own state — what drives the L19 reconnect advisories. */
|
|
79
|
+
onTunnelState?: (state: TunnelState) => void;
|
|
80
|
+
/** The creator's dev server is not answering (L18). */
|
|
81
|
+
onOriginError?: (label: string, code: string) => void;
|
|
82
|
+
/** Their dev server answered again. Re-arms the advisory; prints NOTHING. */
|
|
83
|
+
onOriginRecovered?: () => void;
|
|
84
|
+
/** The session ended for a reason the creator did not cause. */
|
|
85
|
+
onEnded?: (reason: PreviewEndReason) => void;
|
|
86
|
+
/** The hard max is approaching. A WARNING, never an ending (L20b). */
|
|
87
|
+
onExpiring?: () => void;
|
|
88
|
+
/** Injected for tests. */
|
|
89
|
+
fetchImpl?: typeof fetch;
|
|
29
90
|
}
|
|
30
91
|
export interface RunningPreview {
|
|
31
|
-
/** Public https URL to open on the device. */
|
|
92
|
+
/** Public https URL to open on the device. SERVER-COMPUTED, never assembled. */
|
|
32
93
|
url: string;
|
|
33
|
-
|
|
94
|
+
sessionId: string;
|
|
95
|
+
/** ISO8601, the hard max. Never extended by a heartbeat. */
|
|
96
|
+
expiresAt: string;
|
|
34
97
|
relayPort: number;
|
|
35
|
-
/** The app dev-serve port the tunnel ultimately serves. */
|
|
36
98
|
appPort: number;
|
|
37
|
-
/**
|
|
38
|
-
* an orphaned tunnel by pid. Undefined if the spawn never yielded one. */
|
|
39
|
-
tunnelPid?: number;
|
|
40
|
-
/** Whether the app was responding when we returned (false = URL may be blank). */
|
|
99
|
+
/** Whether the app was responding when we returned. */
|
|
41
100
|
appReady: boolean;
|
|
42
|
-
/** Tear down tunnel
|
|
101
|
+
/** Tear down the tunnel, the app dev serve, the front proxy and the relay. */
|
|
43
102
|
stop: () => Promise<void>;
|
|
44
103
|
}
|
|
45
104
|
export declare function startPreview(opts: StartPreviewOptions): Promise<RunningPreview>;
|
|
46
105
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
106
|
+
* Two of the three CONTROL kinds are endings. `expiring` is NOT: it is the
|
|
107
|
+
* hard-max warning, and it arrives on the same channel. Mapping it to an ending
|
|
108
|
+
* would take away a link that has half an hour left, at exactly the moment the
|
|
109
|
+
* creator is being warned to plan around it — so it returns null and the caller
|
|
110
|
+
* renders the warning instead.
|
|
111
|
+
*/
|
|
112
|
+
export declare function controlToReason(kind: ControlKind): PreviewEndReason | null;
|
|
113
|
+
/**
|
|
114
|
+
* A refused RECONNECT dial → the ending screen it means.
|
|
115
|
+
*
|
|
116
|
+
* The edge answers a reattach with three distinguishable statuses and they are
|
|
117
|
+
* deliberately not collapsed (`ingress.go:299-310`): the consume is what
|
|
118
|
+
* separates them, and collapsing would cost the CLI its ability to say "another
|
|
119
|
+
* tunnel holds this preview" versus "your preview ended" — different creator
|
|
120
|
+
* actions, and the debugging creator is the common case.
|
|
121
|
+
*
|
|
122
|
+
* A 409 is the grace cap being spent, which is the one ending that explains the
|
|
123
|
+
* bound. A 401 (the token was refused) and a 410 (the session is gone) both mean
|
|
124
|
+
* the link is not coming back, and `grace_exhausted` is that screen.
|
|
125
|
+
*
|
|
126
|
+
* Anything else — 502, 503, a transient edge fault — returns null and keeps the
|
|
127
|
+
* backoff. Ending a preview over a momentarily unavailable edge would take away
|
|
128
|
+
* a link that is about to work again.
|
|
129
|
+
*/
|
|
130
|
+
export declare function refusalToReason(status: number): PreviewEndReason | null;
|
|
131
|
+
/**
|
|
132
|
+
* One heartbeat. A 410 is the session ending on the server's terms; a 404/403
|
|
133
|
+
* is authorization lost. They are DELIBERATELY NOT MERGED, because the remedies
|
|
134
|
+
* differ: one is "find out who you are", the other is "try again".
|
|
135
|
+
*
|
|
136
|
+
* A transport failure is neither — the API being briefly unreachable must not
|
|
137
|
+
* end a healthy preview, because the edge already fails closed on its own
|
|
138
|
+
* 60-second deadline and will drain if the outage is real.
|
|
139
|
+
*
|
|
140
|
+
* THE 410 IS NOT REPORTED AS `hard_max`. It previously was, unconditionally, and
|
|
141
|
+
* that is a FALSE STATEMENT about the product's own behaviour for every terminal
|
|
142
|
+
* cause that is not the ceiling: a session flipped `abandoned` at minute 30, or
|
|
143
|
+
* superseded, or stopped elsewhere, all answer 410, and the hard-max screen is a
|
|
144
|
+
* `✓` telling the creator the preview did exactly what it promised over 12 hours.
|
|
145
|
+
*
|
|
146
|
+
* The API's 410 body carries ONE opaque code (`preview_session_ended`) for every
|
|
147
|
+
* terminal reason — verified against the running stack: an `abandoned` row and an
|
|
148
|
+
* `expired` row return byte-identical bodies — so the CLI CANNOT tell them apart
|
|
149
|
+
* and must not guess. `session_ended` is the honest, cause-free rendering until
|
|
150
|
+
* the wire carries the discriminator. See the note on PreviewEndedError.
|
|
151
|
+
*/
|
|
152
|
+
export declare function runHeartbeat(config: ResolvedConfig, sessionId: string, fetchImpl: typeof fetch, onEnded?: (reason: PreviewEndReason) => void): Promise<void>;
|
|
153
|
+
/**
|
|
154
|
+
* The API's `endReason` → the CLI's ending screen.
|
|
155
|
+
*
|
|
156
|
+
* `hard_max` is returned ONLY for an explicit `expired`. An UNLABELLED 410 —
|
|
157
|
+
* which is every 410 the API sends today — maps to `grace_exhausted`, the one
|
|
158
|
+
* ending whose copy states only what is actually known: the preview has ended
|
|
159
|
+
* and the link no longer works. It asserts no cause.
|
|
160
|
+
*
|
|
161
|
+
* That default is the conservative choice and it is deliberate. `hard_max` is a
|
|
162
|
+
* `✓` claiming the preview ran its full 12 hours; rendering it for a session the
|
|
163
|
+
* platform reaped at minute 30 tells the creator something untrue about their own
|
|
164
|
+
* session. `grace_exhausted` is a `✗` that says the link is gone and gives the
|
|
165
|
+
* next command, which is true of EVERY terminal cause. Under-claiming a cause
|
|
166
|
+
* costs the creator one sentence of diagnosis; over-claiming one states a fact
|
|
167
|
+
* only the product can see, and states it wrong.
|
|
168
|
+
*
|
|
169
|
+
* EVERY endReason the domain defines must have a case here. The `default` is for
|
|
170
|
+
* a reason a NEWER api invented, not for one this client forgot: a fall-through
|
|
171
|
+
* renders "your network dropped" for a cause that may be nothing of the kind.
|
|
172
|
+
* That is not hypothetical — `stopped` and `disconnected` fell through for a
|
|
173
|
+
* build, so a creator who stopped their own preview from another terminal was
|
|
174
|
+
* told their connection failed. The set is enumerated from the domain by
|
|
175
|
+
* QA-F3-C9(f), which fails when a new member is added rather than mapping it
|
|
176
|
+
* silently to a wrong screen.
|
|
52
177
|
*/
|
|
53
|
-
export declare function
|
|
178
|
+
export declare function endReasonToPreviewEnd(reason: string | undefined): PreviewEndReason;
|