@vincentt-xr/cli 0.1.5 → 0.3.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.
Files changed (92) hide show
  1. package/README.md +94 -0
  2. package/dist/cli/args.d.ts +14 -0
  3. package/dist/cli/args.js +51 -0
  4. package/dist/cli/index.js +416 -100
  5. package/dist/lib/backend.d.ts +44 -20
  6. package/dist/lib/backend.js +98 -51
  7. package/dist/lib/diagnostics.d.ts +27 -2
  8. package/dist/lib/diagnostics.js +73 -8
  9. package/dist/lib/initCommand.d.ts +14 -0
  10. package/dist/lib/initCommand.js +75 -0
  11. package/dist/lib/lifecycle.d.ts +85 -19
  12. package/dist/lib/lifecycle.js +133 -130
  13. package/dist/lib/org.d.ts +29 -0
  14. package/dist/lib/org.js +45 -0
  15. package/dist/lib/projects.d.ts +82 -0
  16. package/dist/lib/projects.js +217 -0
  17. package/dist/lib/relayInfo.d.ts +41 -3
  18. package/dist/lib/relayInfo.js +94 -0
  19. package/dist/lib/whoami.d.ts +11 -0
  20. package/dist/lib/whoami.js +38 -0
  21. package/dist/login/authBackend.d.ts +30 -0
  22. package/dist/login/authBackend.js +80 -0
  23. package/dist/login/guard.d.ts +13 -0
  24. package/dist/login/guard.js +17 -0
  25. package/dist/login/loginCommand.d.ts +39 -0
  26. package/dist/login/loginCommand.js +72 -0
  27. package/dist/login/pkce.d.ts +47 -0
  28. package/dist/login/pkce.js +106 -0
  29. package/dist/login/pkceLogin.d.ts +53 -0
  30. package/dist/login/pkceLogin.js +152 -0
  31. package/dist/mcp/server.js +52 -25
  32. package/dist/preview/api.d.ts +84 -0
  33. package/dist/preview/api.js +174 -0
  34. package/dist/preview/command.d.ts +96 -0
  35. package/dist/preview/command.js +276 -0
  36. package/dist/preview/copy.d.ts +209 -0
  37. package/dist/preview/copy.js +444 -0
  38. package/dist/preview/index.d.ts +2 -3
  39. package/dist/preview/index.js +5 -6
  40. package/dist/preview/proxy.d.ts +12 -1
  41. package/dist/preview/proxy.js +17 -7
  42. package/dist/preview/qr.d.ts +30 -0
  43. package/dist/preview/qr.js +89 -0
  44. package/dist/preview/runner.d.ts +155 -30
  45. package/dist/preview/runner.js +219 -68
  46. package/dist/preview/stream.d.ts +52 -0
  47. package/dist/preview/stream.js +171 -0
  48. package/dist/preview/viewers.d.ts +113 -0
  49. package/dist/preview/viewers.js +242 -0
  50. package/dist/publish/api.d.ts +67 -0
  51. package/dist/publish/api.js +96 -0
  52. package/dist/publish/bounds.d.ts +25 -0
  53. package/dist/publish/bounds.js +53 -0
  54. package/dist/publish/classify.d.ts +51 -0
  55. package/dist/publish/classify.js +138 -0
  56. package/dist/publish/command.d.ts +27 -0
  57. package/dist/publish/command.js +328 -0
  58. package/dist/publish/consoleVerbs.d.ts +17 -0
  59. package/dist/publish/consoleVerbs.js +26 -0
  60. package/dist/publish/copy.d.ts +180 -0
  61. package/dist/publish/copy.js +606 -0
  62. package/dist/publish/defaults.d.ts +16 -0
  63. package/dist/publish/defaults.js +52 -0
  64. package/dist/publish/manifest.d.ts +27 -0
  65. package/dist/publish/manifest.js +81 -0
  66. package/dist/publish/paths.d.ts +25 -0
  67. package/dist/publish/paths.js +78 -0
  68. package/dist/publish/progress.d.ts +17 -0
  69. package/dist/publish/progress.js +45 -0
  70. package/dist/publish/prompt.d.ts +4 -0
  71. package/dist/publish/prompt.js +31 -0
  72. package/dist/publish/qa.f4.fixtures.d.ts +25 -0
  73. package/dist/publish/qa.f4.fixtures.js +140 -0
  74. package/dist/publish/upload.d.ts +30 -0
  75. package/dist/publish/upload.js +93 -0
  76. package/dist/relay/server.d.ts +32 -0
  77. package/dist/relay/server.js +80 -12
  78. package/dist/relay/store.d.ts +32 -3
  79. package/dist/relay/store.js +76 -22
  80. package/dist/relay/testers.d.ts +90 -0
  81. package/dist/relay/testers.js +144 -0
  82. package/dist/shared/authError.d.ts +1 -0
  83. package/dist/shared/authError.js +11 -0
  84. package/dist/shared/config.d.ts +73 -5
  85. package/dist/shared/config.js +133 -20
  86. package/package.json +2 -3
  87. package/dist/login/login.d.ts +0 -34
  88. package/dist/login/login.js +0 -148
  89. package/dist/preview/cloudflared.d.ts +0 -13
  90. package/dist/preview/cloudflared.js +0 -46
  91. package/dist/preview/tunnel.d.ts +0 -14
  92. 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
+ }
@@ -1,3 +1,2 @@
1
- export { startSessionTunnel, type SessionTunnel } from "./tunnel.js";
2
- export { ensureCloudflared, type EnsureCloudflaredOptions } from "./cloudflared.js";
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";
@@ -1,6 +1,5 @@
1
- // Public surface of the preview limb (`@vincentt-xr/harness/preview`): the tunnel
2
- // mint, the cloudflared resolver, and the full one-call preview runner. Barrel
3
- // only — implementations live in the sibling modules.
4
- export { startSessionTunnel } from "./tunnel.js";
5
- export { ensureCloudflared } from "./cloudflared.js";
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";