@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
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/** Flow L13 — the start block. */
|
|
2
|
+
export interface StartBlockInput {
|
|
3
|
+
projectName: string;
|
|
4
|
+
orgName: string;
|
|
5
|
+
/** The forward target, printed BEFORE the URL so the 502 advisory matches it. */
|
|
6
|
+
targetUrl: string;
|
|
7
|
+
url: string;
|
|
8
|
+
/** Pre-rendered QR rows, or undefined for non-TTY / piped / --no-qr. */
|
|
9
|
+
qr?: string[];
|
|
10
|
+
expiresInHours: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function startBlock(i: StartBlockInput): string[];
|
|
13
|
+
/**
|
|
14
|
+
* Flow L14 — not signed in. The B2/B21 line, and the one screen that must not
|
|
15
|
+
* feel like a wall.
|
|
16
|
+
*
|
|
17
|
+
* Line 1 is f1's exact string, so a creator meeting this gate in a second
|
|
18
|
+
* command reads one product. The last paragraph is the most important copy in
|
|
19
|
+
* f3's CLI: the overview commits that the local room stays a real room, and a
|
|
20
|
+
* creator told only "sign in" has just been told the product does not work
|
|
21
|
+
* without an account, which is false. It is offered as a CAPABILITY, not as
|
|
22
|
+
* consolation — no "unfortunately", no "you can still".
|
|
23
|
+
*/
|
|
24
|
+
export declare function notSignedIn(): string[];
|
|
25
|
+
/** Flow L16 — `--viewer`, a second terminal, not a second preview. */
|
|
26
|
+
export declare function followingBlock(label: string, descriptor: string): string[];
|
|
27
|
+
export declare function followingFooter(): string[];
|
|
28
|
+
/**
|
|
29
|
+
* Flow L18 — their dev server is down.
|
|
30
|
+
*
|
|
31
|
+
* `!`, not `✗`, and the command does not exit: this is the most common failure
|
|
32
|
+
* in the whole feature and it is entirely the creator's to fix. Ending the
|
|
33
|
+
* preview would take away the link and force a re-scan of every phone in the
|
|
34
|
+
* room over an `npm run dev` they forgot.
|
|
35
|
+
*
|
|
36
|
+
* The middle line splits the wire in two, because the creator's first instinct
|
|
37
|
+
* on a 502 is to suspect us.
|
|
38
|
+
*/
|
|
39
|
+
export declare function originDown(targetUrl: string): string[];
|
|
40
|
+
/** Flow L19 — the creator's own network drops. */
|
|
41
|
+
export declare function reconnecting(): string[];
|
|
42
|
+
export declare function reconnected(): string[];
|
|
43
|
+
export declare function graceExhausted(): string[];
|
|
44
|
+
/**
|
|
45
|
+
* The drop past the cap. The rule is stated AT THE MOMENT IT BITES and never
|
|
46
|
+
* before — explaining it up front would be a limit the creator carries through
|
|
47
|
+
* every healthy session.
|
|
48
|
+
*
|
|
49
|
+
* THE NUMBER IS DELIBERATELY NOT NAMED, and that reverses an earlier draft which
|
|
50
|
+
* read "for the third time" and "up to three times… a fourth time". Three
|
|
51
|
+
* reasons, in the order they decided it:
|
|
52
|
+
*
|
|
53
|
+
* 1. At three the numeral WAS the explanation — a creator could recognise their
|
|
54
|
+
* own three drops. At ten nobody has been counting, so the numeral states a
|
|
55
|
+
* tally the creator never kept and cannot audit (`graceUsed` is not
|
|
56
|
+
* observable from the CLI), and an orienting sentence becomes one that
|
|
57
|
+
* invites them to argue with the tally. What is left doing the work is the
|
|
58
|
+
* STATE, not the count: the connection kept dropping.
|
|
59
|
+
* 2. Copy that spells a config value is a SECOND ENCODING of that constant. The
|
|
60
|
+
* cap has already moved once mid-build (3 → 10) and this copy went false
|
|
61
|
+
* with nothing failing. Not naming it removes the class of bug rather than
|
|
62
|
+
* fixing one instance.
|
|
63
|
+
* 3. The remedy is what the creator needed and the old copy omitted it — the
|
|
64
|
+
* block ended on "so the link is gone" with no next command, breaking f3's
|
|
65
|
+
* own rule. It is now byte-identical to graceExhausted's, because from the
|
|
66
|
+
* creator's position the two endings are the same event with a different
|
|
67
|
+
* cause.
|
|
68
|
+
*
|
|
69
|
+
* "but only so many times" is a real limit stated without a value: a bound
|
|
70
|
+
* exists, it is finite, it was reached. If the cap is raised or lowered again the
|
|
71
|
+
* sentence is still true, which is the property the previous version lacked.
|
|
72
|
+
*/
|
|
73
|
+
export declare function graceCapReached(): string[];
|
|
74
|
+
/**
|
|
75
|
+
* a. The creator stops it (SIGINT). The connected count appears on the stopping
|
|
76
|
+
* line ONLY when there are any, because the creator is about to blank someone's
|
|
77
|
+
* screen and has a half-second to reconsider. It does NOT prompt — a
|
|
78
|
+
* confirmation on Ctrl-C is the wrong trade in a command a creator stops fifty
|
|
79
|
+
* times a day, and it would break the unattended agent path (B20).
|
|
80
|
+
*/
|
|
81
|
+
export declare function stopping(connected: number): string;
|
|
82
|
+
export declare function stopped(ranMs: number, totalConnections: number): string[];
|
|
83
|
+
/**
|
|
84
|
+
* `stopped` arriving on a 410 — someone ended the preview from somewhere else.
|
|
85
|
+
*
|
|
86
|
+
* NOT `stopped()` with a different first line, and the difference is not tone.
|
|
87
|
+
* `stopped()` prints `Ran … · N viewers total` from the timer and counter THIS
|
|
88
|
+
* process held. A window that was not the actor holds neither, so it would
|
|
89
|
+
* render `Ran 0m 00s · 0 viewers total` for a preview that ran three hours to a
|
|
90
|
+
* roomful of phones. A fabricated zero is worse than an omitted line, so this
|
|
91
|
+
* screen carries no stats at all.
|
|
92
|
+
*
|
|
93
|
+
* The claim is exactly what the wire supports: `stopped` is produced by one
|
|
94
|
+
* route, authorized by the compound key {_id, userId}, so the actor is ALWAYS
|
|
95
|
+
* the same account as the reader. Never a teammate, never an admin. It does not
|
|
96
|
+
* say WHICH window, because another terminal, another machine and a future
|
|
97
|
+
* console control are all the same 410.
|
|
98
|
+
*
|
|
99
|
+
* `✓`, not `✗`: someone with the authority to stop it stopped it.
|
|
100
|
+
*/
|
|
101
|
+
export declare function stoppedElsewhere(): string[];
|
|
102
|
+
/**
|
|
103
|
+
* `disconnected` — the connection closed and the session did not come back.
|
|
104
|
+
*
|
|
105
|
+
* This deliberately UNDER-CLAIMS. `disconnected` is the DEFAULT arm of the
|
|
106
|
+
* edge's reason mapping: a clean socket close, a crashed harness, a laptop lid,
|
|
107
|
+
* an edge restarting under a deploy, and a genuinely dropped network all land
|
|
108
|
+
* on this one value. Naming any of them is the over-claim that produced the
|
|
109
|
+
* abandoned→hard_max defect, so the copy states the one certain fact and then
|
|
110
|
+
* says out loud that it cannot narrow it.
|
|
111
|
+
*
|
|
112
|
+
* "or something on our side" stays. Removing it turns an honest unknown into a
|
|
113
|
+
* list of the creator's own failures.
|
|
114
|
+
*
|
|
115
|
+
* NOT merged with graceExhausted(), though they look adjacent: that screen is
|
|
116
|
+
* about the CLI's OWN observation — it watched the link go and tried to get it
|
|
117
|
+
* back, so "check your network" is earned. This one is a reason REPORTED to a
|
|
118
|
+
* process that may not have witnessed the disconnect, and its target case is
|
|
119
|
+
* the window that still looks healthy. Merging reinstates the original defect.
|
|
120
|
+
*/
|
|
121
|
+
export declare function endedDisconnected(): string[];
|
|
122
|
+
export declare function formatElapsed(ms: number): string;
|
|
123
|
+
/**
|
|
124
|
+
* b. The 12-hour hard maximum, warned at 30 minutes.
|
|
125
|
+
*
|
|
126
|
+
* The warning names the wall-clock time AND the interval — the first is what a
|
|
127
|
+
* creator plans a demo around, the second is what they feel — and it states the
|
|
128
|
+
* cost of restarting, which is the new link. A creator with a printed QR on a
|
|
129
|
+
* desk needs that sentence 30 minutes early, not at the ceiling.
|
|
130
|
+
*/
|
|
131
|
+
export declare function hardMaxWarning(clockTime: string, minutesLeft: number): string[];
|
|
132
|
+
/**
|
|
133
|
+
* `✓`, not `✗`. Nothing failed; the preview did exactly what the start block
|
|
134
|
+
* said it would 12 hours earlier, and marking it as a failure would teach the
|
|
135
|
+
* creator that the ceiling is a fault. This is the ONLY place in f3 that points
|
|
136
|
+
* at f4, and it belongs here: it is the exact moment a creator first wants an
|
|
137
|
+
* address that lasts.
|
|
138
|
+
*/
|
|
139
|
+
export declare function hardMaxReached(): string[];
|
|
140
|
+
/** c. Superseded — the terminal that LOST. */
|
|
141
|
+
export declare function supersededLoser(): string[];
|
|
142
|
+
/**
|
|
143
|
+
* c. Superseded — the terminal that WON. This sits ABOVE the `✓ Preview is live`
|
|
144
|
+
* block so the consequence is read before the URL is copied.
|
|
145
|
+
*/
|
|
146
|
+
export declare function supersededWinner(): string[];
|
|
147
|
+
/**
|
|
148
|
+
* d. The fail-closed drain. TWO causes, one screen each, deliberately NOT
|
|
149
|
+
* merged, because the remedies differ: one is "find out who you are", the other
|
|
150
|
+
* is "try again". Neither names the edge, the API, or an outage — a creator
|
|
151
|
+
* cannot act on that and the retry is the whole of the advice.
|
|
152
|
+
*/
|
|
153
|
+
export declare function endedUnreachable(orgName: string): string[];
|
|
154
|
+
export declare function endedUnconfirmed(): string[];
|
|
155
|
+
/**
|
|
156
|
+
* e. Abandoned — this machine stopped checking in (the 30-minute heartbeat-
|
|
157
|
+
* silence flip, `D-Abandoned-sessions-are-judged-on-heartbeat-silence-alone`).
|
|
158
|
+
*
|
|
159
|
+
* THIS ENDING EXISTS BECAUSE REPORTING IT AS ANY OF THE OTHER FOUR WOULD BE A
|
|
160
|
+
* FALSE STATEMENT ABOUT OUR OWN BEHAVIOUR. The nearest neighbour is the hard max,
|
|
161
|
+
* and it is the one that must never be borrowed: a session flipped at minute 30
|
|
162
|
+
* has not reached a 12-hour ceiling, and that ending is a `✓` saying the preview
|
|
163
|
+
* did exactly what it promised. Rendering it here would tell a creator their 12
|
|
164
|
+
* hours elapsed when 30 minutes did.
|
|
165
|
+
*
|
|
166
|
+
* Written for the UNCOMMON reader. In the common case the CLI is dead and nobody
|
|
167
|
+
* sees this; the screen exists for the creator whose heartbeat channel wedged
|
|
168
|
+
* while the process lived, who is at a terminal that still looks alive and may
|
|
169
|
+
* have a working tunnel and a running app in front of them.
|
|
170
|
+
*
|
|
171
|
+
* - "this machine", not the network and not the tunnel: `heartbeatAt` measures
|
|
172
|
+
* the creator's check-in and nothing else. "You lost connection" would be
|
|
173
|
+
* L19's event and would send them to their router; "the tunnel dropped" would
|
|
174
|
+
* be false in the exact case this screen is for. "This machine" also
|
|
175
|
+
* disambiguates in a two-machine org.
|
|
176
|
+
* - "Nothing is wrong with your project" is load-bearing, not filler. This
|
|
177
|
+
* ending arrives with no visible cause, and the first thing a creator does
|
|
178
|
+
* with an unexplained ending is look for a fault in their own work.
|
|
179
|
+
* - NO duration and no "30 minutes": the interval is a server-side threshold
|
|
180
|
+
* the creator cannot observe or plan around, and naming it invites the same
|
|
181
|
+
* drift the grace copy just removed.
|
|
182
|
+
* - The remedy is `vincentt preview` and there is NO "resume": by the time this
|
|
183
|
+
* fires the session's resume tokens are gone, so offering a resume would be
|
|
184
|
+
* offering something that cannot work.
|
|
185
|
+
*/
|
|
186
|
+
export declare function endedAbandoned(): string[];
|
|
187
|
+
/**
|
|
188
|
+
* The 2 GiB soft warning, printed ONCE. It names the ceiling (a warning without
|
|
189
|
+
* a number is only anxiety), names the cause (a full reload per edit is
|
|
190
|
+
* genuinely surprising), and names the remedy WITH ITS COST — the new link,
|
|
191
|
+
* which is otherwise learned the hard way.
|
|
192
|
+
*/
|
|
193
|
+
export declare function egressSoft(): string[];
|
|
194
|
+
export declare function egressHard(): string[];
|
|
195
|
+
/**
|
|
196
|
+
* The mint limit. No remaining-attempt count and no policy text. The second
|
|
197
|
+
* paragraph is load-bearing: the person most likely to hit 10/hour is someone
|
|
198
|
+
* restarting in a loop who has not realised a running preview already serves
|
|
199
|
+
* them, and that sentence is the actual fix.
|
|
200
|
+
*/
|
|
201
|
+
export declare function mintRateLimited(retryAfterSec: number): string[];
|
|
202
|
+
/**
|
|
203
|
+
* The wait is rounded and hedged because the number comes from a token bucket
|
|
204
|
+
* that keeps refilling. "about 20 minutes" that turns out to be 19 is fine;
|
|
205
|
+
* "20 minutes" that turns out to be 21 reads as a broken promise.
|
|
206
|
+
*/
|
|
207
|
+
export declare function approxWait(seconds: number): string;
|
|
208
|
+
/** The conflict a supersede lost (409). Rare: the mint supersedes first. */
|
|
209
|
+
export declare function previewConflict(): string[];
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
// Every line `vincentt preview` prints that is not a stream event (Flows
|
|
2
|
+
// L13-L21). Pure functions returning string arrays, so the transcripts are
|
|
3
|
+
// assertable without a terminal.
|
|
4
|
+
//
|
|
5
|
+
// Four vocabulary rules govern all of it:
|
|
6
|
+
// - "Preview" is the noun and the verb. `session` is the API's word and never
|
|
7
|
+
// reaches a creator's screen. What ends is THE PREVIEW; what the phone holds
|
|
8
|
+
// is THE LINK.
|
|
9
|
+
// - "Viewer" is a phone, spoken of as a DEVICE, never as a person. The moment
|
|
10
|
+
// the UI says "who", the label has been promoted into an identity.
|
|
11
|
+
// - The link is "temporary", never "expired", "revoked" or "invalid".
|
|
12
|
+
// - The creator's own dev server is "your dev server" / "your app", never
|
|
13
|
+
// "the origin" or "upstream".
|
|
14
|
+
//
|
|
15
|
+
// NEVER PRINTED, at any point, in any state: the grant, the resume token, the
|
|
16
|
+
// session id, the edge hostname, or the workingTreeKey. The grant is the
|
|
17
|
+
// sharpest — it transits an agent's tool-output transcript by default in this
|
|
18
|
+
// product, so f1's "a token is never printed" extends to it verbatim.
|
|
19
|
+
export function startBlock(i) {
|
|
20
|
+
const out = [
|
|
21
|
+
"",
|
|
22
|
+
// The org is dropped rather than rendered as a dangling separator when it
|
|
23
|
+
// is not known. `Museum AR Tour · ` reads as a truncated line — the one
|
|
24
|
+
// moment a wrong binding is cheap to catch is the one moment the line must
|
|
25
|
+
// not look broken.
|
|
26
|
+
i.orgName ? `→ ${i.projectName} · ${i.orgName}` : `→ ${i.projectName}`,
|
|
27
|
+
`→ Serving ${i.targetUrl}`,
|
|
28
|
+
"",
|
|
29
|
+
"✓ Preview is live",
|
|
30
|
+
"",
|
|
31
|
+
// The URL is on its own line, indented, unwrapped and UNADORNED. No `URL:`
|
|
32
|
+
// label, no backticks, no surrounding punctuation — it is the single
|
|
33
|
+
// most-copied string in the product, and anything beside it ends up in
|
|
34
|
+
// someone's paste.
|
|
35
|
+
` ${i.url}`,
|
|
36
|
+
"",
|
|
37
|
+
];
|
|
38
|
+
if (i.qr && i.qr.length > 0) {
|
|
39
|
+
out.push(...i.qr.map((row) => ` ${row}`), "");
|
|
40
|
+
}
|
|
41
|
+
out.push(" Scan it, or open the link on any phone. No sign-in needed on the phone.", ` Expires in ${i.expiresInHours}h · Ctrl-C to stop`, "", " Waiting for a device…", "");
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Flow L14 — not signed in. The B2/B21 line, and the one screen that must not
|
|
46
|
+
* feel like a wall.
|
|
47
|
+
*
|
|
48
|
+
* Line 1 is f1's exact string, so a creator meeting this gate in a second
|
|
49
|
+
* command reads one product. The last paragraph is the most important copy in
|
|
50
|
+
* f3's CLI: the overview commits that the local room stays a real room, and a
|
|
51
|
+
* creator told only "sign in" has just been told the product does not work
|
|
52
|
+
* without an account, which is false. It is offered as a CAPABILITY, not as
|
|
53
|
+
* consolation — no "unfortunately", no "you can still".
|
|
54
|
+
*/
|
|
55
|
+
export function notSignedIn() {
|
|
56
|
+
return [
|
|
57
|
+
"",
|
|
58
|
+
"✗ You're not signed in.",
|
|
59
|
+
"",
|
|
60
|
+
" A preview needs a hostname your phone can reach, and that needs an account.",
|
|
61
|
+
"",
|
|
62
|
+
" Sign in: vincentt login",
|
|
63
|
+
"",
|
|
64
|
+
" Your local dev loop does not need one — `npm run dev` and open",
|
|
65
|
+
" http://localhost:5173 on this machine. The camera works there.",
|
|
66
|
+
" What signing in adds is the phone.",
|
|
67
|
+
"",
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
/** Flow L16 — `--viewer`, a second terminal, not a second preview. */
|
|
71
|
+
export function followingBlock(label, descriptor) {
|
|
72
|
+
return [
|
|
73
|
+
"→ Attaching to the preview running in this folder",
|
|
74
|
+
descriptor ? `✓ Following ${label} · ${descriptor}` : `✓ Following ${label}`,
|
|
75
|
+
"",
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
export function followingFooter() {
|
|
79
|
+
return ["", " Ctrl-C to stop following. The preview keeps running."];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Flow L18 — their dev server is down.
|
|
83
|
+
*
|
|
84
|
+
* `!`, not `✗`, and the command does not exit: this is the most common failure
|
|
85
|
+
* in the whole feature and it is entirely the creator's to fix. Ending the
|
|
86
|
+
* preview would take away the link and force a re-scan of every phone in the
|
|
87
|
+
* room over an `npm run dev` they forgot.
|
|
88
|
+
*
|
|
89
|
+
* The middle line splits the wire in two, because the creator's first instinct
|
|
90
|
+
* on a 502 is to suspect us.
|
|
91
|
+
*/
|
|
92
|
+
export function originDown(targetUrl) {
|
|
93
|
+
return [
|
|
94
|
+
`! Your dev server isn't responding on ${targetUrl}.`,
|
|
95
|
+
"",
|
|
96
|
+
" The preview is still live — the phone is reaching us, we can't reach your app.",
|
|
97
|
+
" Start it and reload the phone: npm run dev",
|
|
98
|
+
"",
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
/** Flow L19 — the creator's own network drops. */
|
|
102
|
+
export function reconnecting() {
|
|
103
|
+
return [
|
|
104
|
+
"! Lost the connection to Vincentt. Reconnecting…",
|
|
105
|
+
' Your phone will show "reconnecting" and retry on its own.',
|
|
106
|
+
"",
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
export function reconnected() {
|
|
110
|
+
return ["✓ Reconnected. Same link, same session.", ""];
|
|
111
|
+
}
|
|
112
|
+
export function graceExhausted() {
|
|
113
|
+
return [
|
|
114
|
+
"",
|
|
115
|
+
"✗ Couldn't reconnect. The preview has ended and the link no longer works.",
|
|
116
|
+
"",
|
|
117
|
+
" Check your network, then: vincentt preview",
|
|
118
|
+
"",
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The drop past the cap. The rule is stated AT THE MOMENT IT BITES and never
|
|
123
|
+
* before — explaining it up front would be a limit the creator carries through
|
|
124
|
+
* every healthy session.
|
|
125
|
+
*
|
|
126
|
+
* THE NUMBER IS DELIBERATELY NOT NAMED, and that reverses an earlier draft which
|
|
127
|
+
* read "for the third time" and "up to three times… a fourth time". Three
|
|
128
|
+
* reasons, in the order they decided it:
|
|
129
|
+
*
|
|
130
|
+
* 1. At three the numeral WAS the explanation — a creator could recognise their
|
|
131
|
+
* own three drops. At ten nobody has been counting, so the numeral states a
|
|
132
|
+
* tally the creator never kept and cannot audit (`graceUsed` is not
|
|
133
|
+
* observable from the CLI), and an orienting sentence becomes one that
|
|
134
|
+
* invites them to argue with the tally. What is left doing the work is the
|
|
135
|
+
* STATE, not the count: the connection kept dropping.
|
|
136
|
+
* 2. Copy that spells a config value is a SECOND ENCODING of that constant. The
|
|
137
|
+
* cap has already moved once mid-build (3 → 10) and this copy went false
|
|
138
|
+
* with nothing failing. Not naming it removes the class of bug rather than
|
|
139
|
+
* fixing one instance.
|
|
140
|
+
* 3. The remedy is what the creator needed and the old copy omitted it — the
|
|
141
|
+
* block ended on "so the link is gone" with no next command, breaking f3's
|
|
142
|
+
* own rule. It is now byte-identical to graceExhausted's, because from the
|
|
143
|
+
* creator's position the two endings are the same event with a different
|
|
144
|
+
* cause.
|
|
145
|
+
*
|
|
146
|
+
* "but only so many times" is a real limit stated without a value: a bound
|
|
147
|
+
* exists, it is finite, it was reached. If the cap is raised or lowered again the
|
|
148
|
+
* sentence is still true, which is the property the previous version lacked.
|
|
149
|
+
*/
|
|
150
|
+
export function graceCapReached() {
|
|
151
|
+
return [
|
|
152
|
+
"",
|
|
153
|
+
"✗ The connection to Vincentt kept dropping. Ending the preview.",
|
|
154
|
+
"",
|
|
155
|
+
" A preview holds its link across a brief drop, but only so many times, and",
|
|
156
|
+
" this one has used them up. The link is gone.",
|
|
157
|
+
"",
|
|
158
|
+
" Check your network, then: vincentt preview",
|
|
159
|
+
"",
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
// ---- Flow L20, the four endings ---------------------------------------------
|
|
163
|
+
/**
|
|
164
|
+
* a. The creator stops it (SIGINT). The connected count appears on the stopping
|
|
165
|
+
* line ONLY when there are any, because the creator is about to blank someone's
|
|
166
|
+
* screen and has a half-second to reconsider. It does NOT prompt — a
|
|
167
|
+
* confirmation on Ctrl-C is the wrong trade in a command a creator stops fifty
|
|
168
|
+
* times a day, and it would break the unattended agent path (B20).
|
|
169
|
+
*/
|
|
170
|
+
// Both lines say CONNECTIONS, not viewers. One device opens several (the edge is
|
|
171
|
+
// HTTP/1.1 only, so a browser cannot multiplex and opens a second connection for
|
|
172
|
+
// assets), and the device count is not available to print instead — ViewerConn
|
|
173
|
+
// deliberately holds nothing a correlator could use. Naming the unit is the only
|
|
174
|
+
// honest option; "2 viewers" for one phone was not.
|
|
175
|
+
export function stopping(connected) {
|
|
176
|
+
return connected > 0
|
|
177
|
+
? `→ Stopping… ${connected} connection${connected === 1 ? "" : "s"} open`
|
|
178
|
+
: "→ Stopping…";
|
|
179
|
+
}
|
|
180
|
+
export function stopped(ranMs, totalConnections) {
|
|
181
|
+
return [
|
|
182
|
+
"✓ Preview stopped. The link no longer works.",
|
|
183
|
+
// Kept at stop time: a connection total is a true statement about the run,
|
|
184
|
+
// and unlike the live count it does not masquerade as "who is watching".
|
|
185
|
+
// Nothing is retained.
|
|
186
|
+
` Ran ${formatElapsed(ranMs)} · ${totalConnections} connection${totalConnections === 1 ? "" : "s"} total`,
|
|
187
|
+
];
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* `stopped` arriving on a 410 — someone ended the preview from somewhere else.
|
|
191
|
+
*
|
|
192
|
+
* NOT `stopped()` with a different first line, and the difference is not tone.
|
|
193
|
+
* `stopped()` prints `Ran … · N viewers total` from the timer and counter THIS
|
|
194
|
+
* process held. A window that was not the actor holds neither, so it would
|
|
195
|
+
* render `Ran 0m 00s · 0 viewers total` for a preview that ran three hours to a
|
|
196
|
+
* roomful of phones. A fabricated zero is worse than an omitted line, so this
|
|
197
|
+
* screen carries no stats at all.
|
|
198
|
+
*
|
|
199
|
+
* The claim is exactly what the wire supports: `stopped` is produced by one
|
|
200
|
+
* route, authorized by the compound key {_id, userId}, so the actor is ALWAYS
|
|
201
|
+
* the same account as the reader. Never a teammate, never an admin. It does not
|
|
202
|
+
* say WHICH window, because another terminal, another machine and a future
|
|
203
|
+
* console control are all the same 410.
|
|
204
|
+
*
|
|
205
|
+
* `✓`, not `✗`: someone with the authority to stop it stopped it.
|
|
206
|
+
*/
|
|
207
|
+
export function stoppedElsewhere() {
|
|
208
|
+
return [
|
|
209
|
+
"",
|
|
210
|
+
"✓ Preview stopped from another terminal. The link no longer works.",
|
|
211
|
+
"",
|
|
212
|
+
" It was stopped on your account, not by this window. Nothing is wrong here.",
|
|
213
|
+
"",
|
|
214
|
+
" Start a new one: vincentt preview",
|
|
215
|
+
"",
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* `disconnected` — the connection closed and the session did not come back.
|
|
220
|
+
*
|
|
221
|
+
* This deliberately UNDER-CLAIMS. `disconnected` is the DEFAULT arm of the
|
|
222
|
+
* edge's reason mapping: a clean socket close, a crashed harness, a laptop lid,
|
|
223
|
+
* an edge restarting under a deploy, and a genuinely dropped network all land
|
|
224
|
+
* on this one value. Naming any of them is the over-claim that produced the
|
|
225
|
+
* abandoned→hard_max defect, so the copy states the one certain fact and then
|
|
226
|
+
* says out loud that it cannot narrow it.
|
|
227
|
+
*
|
|
228
|
+
* "or something on our side" stays. Removing it turns an honest unknown into a
|
|
229
|
+
* list of the creator's own failures.
|
|
230
|
+
*
|
|
231
|
+
* NOT merged with graceExhausted(), though they look adjacent: that screen is
|
|
232
|
+
* about the CLI's OWN observation — it watched the link go and tried to get it
|
|
233
|
+
* back, so "check your network" is earned. This one is a reason REPORTED to a
|
|
234
|
+
* process that may not have witnessed the disconnect, and its target case is
|
|
235
|
+
* the window that still looks healthy. Merging reinstates the original defect.
|
|
236
|
+
*/
|
|
237
|
+
export function endedDisconnected() {
|
|
238
|
+
return [
|
|
239
|
+
"",
|
|
240
|
+
"✗ Preview ended — the connection to Vincentt closed.",
|
|
241
|
+
"",
|
|
242
|
+
" This window can't tell whether that was your network, your machine going to",
|
|
243
|
+
" sleep, or something on our side. Nothing is wrong with your project.",
|
|
244
|
+
"",
|
|
245
|
+
" Start a new one: vincentt preview",
|
|
246
|
+
"",
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
export function formatElapsed(ms) {
|
|
250
|
+
const total = Math.max(0, Math.round(ms / 1000));
|
|
251
|
+
const h = Math.floor(total / 3600);
|
|
252
|
+
const m = Math.floor((total % 3600) / 60);
|
|
253
|
+
const s = total % 60;
|
|
254
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
255
|
+
return h > 0 ? `${h}h ${pad(m)}m ${pad(s)}s` : `${m}m ${pad(s)}s`;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* b. The 12-hour hard maximum, warned at 30 minutes.
|
|
259
|
+
*
|
|
260
|
+
* The warning names the wall-clock time AND the interval — the first is what a
|
|
261
|
+
* creator plans a demo around, the second is what they feel — and it states the
|
|
262
|
+
* cost of restarting, which is the new link. A creator with a printed QR on a
|
|
263
|
+
* desk needs that sentence 30 minutes early, not at the ceiling.
|
|
264
|
+
*/
|
|
265
|
+
export function hardMaxWarning(clockTime, minutesLeft) {
|
|
266
|
+
return [
|
|
267
|
+
`! This preview reaches its 12-hour maximum at ${clockTime} (in ${minutesLeft}m) and will stop.`,
|
|
268
|
+
"",
|
|
269
|
+
" A preview link is deliberately short-lived. To keep going, start a new one",
|
|
270
|
+
" then — you'll get a new link, so any QR code on a desk will need re-scanning.",
|
|
271
|
+
"",
|
|
272
|
+
];
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* `✓`, not `✗`. Nothing failed; the preview did exactly what the start block
|
|
276
|
+
* said it would 12 hours earlier, and marking it as a failure would teach the
|
|
277
|
+
* creator that the ceiling is a fault. This is the ONLY place in f3 that points
|
|
278
|
+
* at f4, and it belongs here: it is the exact moment a creator first wants an
|
|
279
|
+
* address that lasts.
|
|
280
|
+
*/
|
|
281
|
+
export function hardMaxReached() {
|
|
282
|
+
return [
|
|
283
|
+
"",
|
|
284
|
+
"✓ Preview stopped — 12-hour maximum reached. The link no longer works.",
|
|
285
|
+
"",
|
|
286
|
+
" Start a new one: vincentt preview",
|
|
287
|
+
" For an address that lasts, publish it: vincentt publish",
|
|
288
|
+
"",
|
|
289
|
+
];
|
|
290
|
+
}
|
|
291
|
+
/** c. Superseded — the terminal that LOST. */
|
|
292
|
+
export function supersededLoser() {
|
|
293
|
+
return [
|
|
294
|
+
"",
|
|
295
|
+
"✗ This preview was replaced by a newer one for this folder.",
|
|
296
|
+
"",
|
|
297
|
+
" Its link no longer works. The new preview has a different link — whoever",
|
|
298
|
+
" is holding a phone will need it again.",
|
|
299
|
+
"",
|
|
300
|
+
];
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* c. Superseded — the terminal that WON. This sits ABOVE the `✓ Preview is live`
|
|
304
|
+
* block so the consequence is read before the URL is copied.
|
|
305
|
+
*/
|
|
306
|
+
export function supersededWinner() {
|
|
307
|
+
return [
|
|
308
|
+
"",
|
|
309
|
+
"! Replaced the preview that was already running for this folder.",
|
|
310
|
+
" Its link stopped working. Anyone on it will need the new one below.",
|
|
311
|
+
];
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* d. The fail-closed drain. TWO causes, one screen each, deliberately NOT
|
|
315
|
+
* merged, because the remedies differ: one is "find out who you are", the other
|
|
316
|
+
* is "try again". Neither names the edge, the API, or an outage — a creator
|
|
317
|
+
* cannot act on that and the retry is the whole of the advice.
|
|
318
|
+
*/
|
|
319
|
+
export function endedUnreachable(orgName) {
|
|
320
|
+
return [
|
|
321
|
+
"",
|
|
322
|
+
"✗ Preview ended — this project is no longer available to you.",
|
|
323
|
+
"",
|
|
324
|
+
` Your access to ${orgName} may have changed. Check who you are: vincentt whoami`,
|
|
325
|
+
"",
|
|
326
|
+
];
|
|
327
|
+
}
|
|
328
|
+
export function endedUnconfirmed() {
|
|
329
|
+
return [
|
|
330
|
+
"",
|
|
331
|
+
"✗ Preview ended — couldn't confirm your access to this project.",
|
|
332
|
+
"",
|
|
333
|
+
" Try again: vincentt preview",
|
|
334
|
+
"",
|
|
335
|
+
];
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* e. Abandoned — this machine stopped checking in (the 30-minute heartbeat-
|
|
339
|
+
* silence flip, `D-Abandoned-sessions-are-judged-on-heartbeat-silence-alone`).
|
|
340
|
+
*
|
|
341
|
+
* THIS ENDING EXISTS BECAUSE REPORTING IT AS ANY OF THE OTHER FOUR WOULD BE A
|
|
342
|
+
* FALSE STATEMENT ABOUT OUR OWN BEHAVIOUR. The nearest neighbour is the hard max,
|
|
343
|
+
* and it is the one that must never be borrowed: a session flipped at minute 30
|
|
344
|
+
* has not reached a 12-hour ceiling, and that ending is a `✓` saying the preview
|
|
345
|
+
* did exactly what it promised. Rendering it here would tell a creator their 12
|
|
346
|
+
* hours elapsed when 30 minutes did.
|
|
347
|
+
*
|
|
348
|
+
* Written for the UNCOMMON reader. In the common case the CLI is dead and nobody
|
|
349
|
+
* sees this; the screen exists for the creator whose heartbeat channel wedged
|
|
350
|
+
* while the process lived, who is at a terminal that still looks alive and may
|
|
351
|
+
* have a working tunnel and a running app in front of them.
|
|
352
|
+
*
|
|
353
|
+
* - "this machine", not the network and not the tunnel: `heartbeatAt` measures
|
|
354
|
+
* the creator's check-in and nothing else. "You lost connection" would be
|
|
355
|
+
* L19's event and would send them to their router; "the tunnel dropped" would
|
|
356
|
+
* be false in the exact case this screen is for. "This machine" also
|
|
357
|
+
* disambiguates in a two-machine org.
|
|
358
|
+
* - "Nothing is wrong with your project" is load-bearing, not filler. This
|
|
359
|
+
* ending arrives with no visible cause, and the first thing a creator does
|
|
360
|
+
* with an unexplained ending is look for a fault in their own work.
|
|
361
|
+
* - NO duration and no "30 minutes": the interval is a server-side threshold
|
|
362
|
+
* the creator cannot observe or plan around, and naming it invites the same
|
|
363
|
+
* drift the grace copy just removed.
|
|
364
|
+
* - The remedy is `vincentt preview` and there is NO "resume": by the time this
|
|
365
|
+
* fires the session's resume tokens are gone, so offering a resume would be
|
|
366
|
+
* offering something that cannot work.
|
|
367
|
+
*/
|
|
368
|
+
export function endedAbandoned() {
|
|
369
|
+
return [
|
|
370
|
+
"",
|
|
371
|
+
"✗ Preview ended — this machine stopped checking in with Vincentt.",
|
|
372
|
+
"",
|
|
373
|
+
" The preview needed this computer to stay in touch and it went quiet, so the",
|
|
374
|
+
" link was released. Nothing is wrong with your project.",
|
|
375
|
+
"",
|
|
376
|
+
" Start a new one: vincentt preview",
|
|
377
|
+
"",
|
|
378
|
+
];
|
|
379
|
+
}
|
|
380
|
+
// ---- Flow L21, the two budget stops -----------------------------------------
|
|
381
|
+
/**
|
|
382
|
+
* The 2 GiB soft warning, printed ONCE. It names the ceiling (a warning without
|
|
383
|
+
* a number is only anxiety), names the cause (a full reload per edit is
|
|
384
|
+
* genuinely surprising), and names the remedy WITH ITS COST — the new link,
|
|
385
|
+
* which is otherwise learned the hard way.
|
|
386
|
+
*/
|
|
387
|
+
export function egressSoft() {
|
|
388
|
+
return [
|
|
389
|
+
"! This preview has served 2 GB. There's a 5 GB ceiling per preview.",
|
|
390
|
+
"",
|
|
391
|
+
" Large models reload in full on every edit. If you're near the ceiling,",
|
|
392
|
+
" stopping and starting gives you a fresh budget (and a new link).",
|
|
393
|
+
"",
|
|
394
|
+
];
|
|
395
|
+
}
|
|
396
|
+
export function egressHard() {
|
|
397
|
+
return [
|
|
398
|
+
"",
|
|
399
|
+
"✗ Preview ended — it reached the 5 GB ceiling.",
|
|
400
|
+
"",
|
|
401
|
+
" Start a new one: vincentt preview",
|
|
402
|
+
"",
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* The mint limit. No remaining-attempt count and no policy text. The second
|
|
407
|
+
* paragraph is load-bearing: the person most likely to hit 10/hour is someone
|
|
408
|
+
* restarting in a loop who has not realised a running preview already serves
|
|
409
|
+
* them, and that sentence is the actual fix.
|
|
410
|
+
*/
|
|
411
|
+
export function mintRateLimited(retryAfterSec) {
|
|
412
|
+
return [
|
|
413
|
+
"",
|
|
414
|
+
`✗ Too many previews started recently. Try again in ${approxWait(retryAfterSec)}.`,
|
|
415
|
+
"",
|
|
416
|
+
" A running preview does not count against this — if one is already running",
|
|
417
|
+
" for this folder, you don't need a new one.",
|
|
418
|
+
"",
|
|
419
|
+
];
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* The wait is rounded and hedged because the number comes from a token bucket
|
|
423
|
+
* that keeps refilling. "about 20 minutes" that turns out to be 19 is fine;
|
|
424
|
+
* "20 minutes" that turns out to be 21 reads as a broken promise.
|
|
425
|
+
*/
|
|
426
|
+
export function approxWait(seconds) {
|
|
427
|
+
if (seconds < 90)
|
|
428
|
+
return "about a minute";
|
|
429
|
+
const minutes = Math.round(seconds / 60);
|
|
430
|
+
if (minutes < 60)
|
|
431
|
+
return `about ${minutes} minutes`;
|
|
432
|
+
const hours = Math.round(minutes / 60);
|
|
433
|
+
return `about ${hours} hour${hours === 1 ? "" : "s"}`;
|
|
434
|
+
}
|
|
435
|
+
/** The conflict a supersede lost (409). Rare: the mint supersedes first. */
|
|
436
|
+
export function previewConflict() {
|
|
437
|
+
return [
|
|
438
|
+
"",
|
|
439
|
+
"✗ A preview is already running for this folder.",
|
|
440
|
+
"",
|
|
441
|
+
" Watch it from here: vincentt preview",
|
|
442
|
+
"",
|
|
443
|
+
];
|
|
444
|
+
}
|
package/dist/preview/index.d.ts
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export { startPreview, type StartPreviewOptions, type RunningPreview } from "./runner.js";
|
|
1
|
+
export { startPreview, controlToReason, runHeartbeat, type StartPreviewOptions, type RunningPreview, type PreviewEndReason, } from "./runner.js";
|
|
2
|
+
export { endPreviewSession, heartbeatPreviewSession, mintPreviewSession, parseMint, parseRetryAfter, workingTreeKey, PreviewConflictError, PreviewEndedError, PreviewRateLimitedError, type HeartbeatResult, type MintedPreview, } from "./api.js";
|
package/dist/preview/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
// Public surface of the preview limb (`@vincentt-xr/
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export { startPreview } from "./runner.js";
|
|
1
|
+
// Public surface of the preview limb (`@vincentt-xr/cli/preview`): the preview-
|
|
2
|
+
// session API calls and the one-call preview runner. Barrel only —
|
|
3
|
+
// implementations live in the sibling modules.
|
|
4
|
+
export { startPreview, controlToReason, runHeartbeat, } from "./runner.js";
|
|
5
|
+
export { endPreviewSession, heartbeatPreviewSession, mintPreviewSession, parseMint, parseRetryAfter, workingTreeKey, PreviewConflictError, PreviewEndedError, PreviewRateLimitedError, } from "./api.js";
|