@vincentt-xr/cli 0.24.0 → 0.25.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/MIGRATION.md CHANGED
@@ -3,6 +3,17 @@
3
3
  Per version, newest first. Read by the platform's own version surface and served to
4
4
  creators' agents. Accurate or absent, never speculative (B-F5-2).
5
5
 
6
+ ## 0.25.0
7
+
8
+ required: false
9
+ No action required. `vincentt preview` leads with the console page and no creator-facing screen prints a localhost address.
10
+ - The start block prints `→ <project> · <workspace>` before the preview is created, then `✓ Preview is live`, then `Open the console:` with the project's console page, then `On a phone:` with the phone link and QR. The `→ Serving http://localhost:<port>` line is gone.
11
+ - Running `vincentt preview` while one is running in the folder prints the same two blocks: the console page, then the phone link.
12
+ - The not-signed-in screen ends with `Sign in: vincentt login` and `Then start it again: vincentt preview`. It no longer names the dev command or a local address. The preview's upgrade refusal likewise ends on the upgrade and `vincentt preview`.
13
+ - When the dev server is down, the advisory reads `! Your dev server isn't responding on port <n>.`
14
+ - `vincentt create` and `vincentt init` next steps no longer list the dev server; `vincentt preview` starts it. A non-interactive `create` that scaffolded without an account ends with `Signing in is what gives it an address a phone can open.`
15
+ - The browser page at the end of `vincentt login` reads `You're Signed In` and `Go back to your AI assistant to continue.` The problem-report consent page reads `Problem Reports Are On` or `Problem Reports Are Off` with the same line. Neither mentions the terminal.
16
+
6
17
  ## 0.24.0
7
18
 
8
19
  required: false
package/README.md CHANGED
@@ -108,9 +108,8 @@ later verbs run without prompting. `init` never reaches that gate at all.
108
108
 
109
109
  ## The loop, once a project exists
110
110
 
111
- - `pnpm dev` — local dev server at `http://localhost:5173`.
112
- - `npx vincentt preview` — build, serve, and tunnel the app so you get one `https`
113
- URL (and a desktop QR page) to open on a phone. AR needs a secure context for
111
+ - `npx vincentt preview` — start the dev server and tunnel the app, then hand over
112
+ the project's console page, which carries the phone link and its QR. AR needs a secure context for
114
113
  camera access, so this is how you test tracking and gestures on a device.
115
114
  - `npx vincentt logs | network | trace` — read what the phone reported to the
116
115
  preview relay. Debug on-device misbehavior instead of guessing.
package/dist/cli/index.js CHANGED
@@ -39,7 +39,7 @@ import { USAGE_TEXT } from "./usage-text.js";
39
39
  import { PreviewUi } from "../preview/command.js";
40
40
  import { httpRelayClient } from "../lib/diagnostics.js";
41
41
  import { readLivePreview, relayInfoDir } from "../lib/relayInfo.js";
42
- import { attachedToRunning, mintRateLimited, noDiagnosticsStatic, notSignedIn, previewConflict, previewUserLimitRefused, settingsBanner, startBlock, } from "../preview/copy.js";
42
+ import { attachedToRunning, bindingLine, mintRateLimited, noDiagnosticsStatic, notSignedIn, previewConflict, previewUserLimitRefused, settingsBanner, startBlock, } from "../preview/copy.js";
43
43
  import { chooseDevPort } from "../preview/devPort.js";
44
44
  import { isPortListening } from "../preview/net.js";
45
45
  import { readSettings, resolveSettings, SettingsError, TEMPLATE_DEFAULTS, validateField, } from "../lib/settings.js";
@@ -570,9 +570,6 @@ async function preview() {
570
570
  }
571
571
  const devCommand = settings.devCommand.value;
572
572
  const { port: devPort, fresh: devPortFresh } = await chooseDevPort(settings);
573
- // What the creator gets running the dev command by hand, without PORT.
574
- const handRunUrl = `http://localhost:${settings.devPort.value}`;
575
- const localUrl = `http://localhost:${devPort}`;
576
573
  for (const l of settingsBanner({
577
574
  devCommand,
578
575
  devCommandSource: sourceWords(settings.devCommand),
@@ -589,7 +586,7 @@ async function preview() {
589
586
  // The gate is LOCAL and NOTHING IS ATTEMPTED: no spinner, no partial mint, no
590
587
  // session left in `pending`. The command stops before it touches the network.
591
588
  if (!(await currentToken())) {
592
- for (const l of notSignedIn(devCommand, handRunUrl))
589
+ for (const l of notSignedIn())
593
590
  console.log(l);
594
591
  process.exitCode = EXIT.NOT_AUTHENTICATED;
595
592
  return;
@@ -600,7 +597,7 @@ async function preview() {
600
597
  const ui = new PreviewUi({
601
598
  print: (l) => console.log(l),
602
599
  now: () => Date.now(),
603
- targetUrl: localUrl,
600
+ devPort,
604
601
  color,
605
602
  filterTester,
606
603
  filterViewer,
@@ -629,8 +626,10 @@ async function preview() {
629
626
  // No filter means the caller wanted the preview, not a diagnostics stream:
630
627
  // hand them the link and exit rather than blocking in the follow loop.
631
628
  // Following a live preview is what `vincentt logs`/`network`/`trace` are.
632
- for (const l of attachedToRunning(existing.url, existing.expiresAt))
629
+ const consoleUrl = await projectConsoleUrl(cwd, existing.sessionId);
630
+ for (const l of attachedToRunning(existing.url, consoleUrl, existing.expiresAt)) {
633
631
  console.log(l);
632
+ }
634
633
  return;
635
634
  }
636
635
  else {
@@ -642,11 +641,7 @@ async function preview() {
642
641
  let endingRefusal;
643
642
  let resolveDone;
644
643
  const refusedAtStart = (facts) => {
645
- for (const l of upgradeRefusalLines(facts, {
646
- kind: "preview_start",
647
- devCommand,
648
- localUrl: handRunUrl,
649
- })) {
644
+ for (const l of upgradeRefusalLines(facts, { kind: "preview_start" })) {
650
645
  console.log(l);
651
646
  }
652
647
  process.exitCode = EXIT.UPGRADE_REQUIRED;
@@ -654,6 +649,14 @@ async function preview() {
654
649
  const done = new Promise((r) => {
655
650
  resolveDone = r;
656
651
  });
652
+ // The binding line prints BEFORE the mint: a wrong binding is cheap to catch
653
+ // only while no session exists. An unlinked folder prints none — previewStart
654
+ // refuses it, and a guessed name would be a line about the wrong project.
655
+ if (await locateProjectBinding(cwd)) {
656
+ for (const l of bindingLine(await projectDisplayName(cwd), await activeOrgName())) {
657
+ console.log(l);
658
+ }
659
+ }
657
660
  let running;
658
661
  try {
659
662
  const started = await previewStart(cwd, (m) => console.error(`[preview] ${m}`), {
@@ -723,9 +726,6 @@ async function preview() {
723
726
  ? await renderTerminalQr(running.url).catch(() => undefined)
724
727
  : undefined;
725
728
  for (const l of startBlock({
726
- projectName: await projectDisplayName(cwd),
727
- orgName: await activeOrgName(),
728
- targetUrl: `http://localhost:${running.appPort}`,
729
729
  url: running.url,
730
730
  consoleUrl: await projectConsoleUrl(cwd, running.sessionId),
731
731
  feedbackOrigin: running.consoleOrigin !== undefined &&
@@ -195,7 +195,7 @@ export async function runConsentInBrowser(opts) {
195
195
  value: result.decision,
196
196
  page: {
197
197
  status: 200,
198
- html: browserPage(granted, granted ? "Reports allowed" : "Not enabled", "Return to your terminal. You can close this tab."),
198
+ html: browserPage(granted, granted ? "Problem Reports Are On" : "Problem Reports Are Off", "Go back to your AI assistant to continue."),
199
199
  },
200
200
  };
201
201
  }
@@ -134,11 +134,13 @@ export async function initCommand(deps, opts = {}) {
134
134
  }
135
135
  }
136
136
  // The template ships a pnpm lockfile and a hoisted node-linker, so telling a
137
- // creator to `npm run dev` in it produces a second lockfile and an unproven
137
+ // creator to `npm install` in it produces a second lockfile and an unproven
138
138
  // node_modules layout. Name the manager the scaffold is actually set up for.
139
139
  const pm = await detectPackageManager(target).catch(() => "npm");
140
140
  deps.print("");
141
141
  deps.print(`✓ Created ${label}`);
142
+ // No dev-server step: `vincentt preview` starts it, and the product never
143
+ // points the creator at the local loop (B-F3-6).
142
144
  const steps = [];
143
145
  // Keyed off the RESOLVED dir, not the flag — a prompted name needs the cd too.
144
146
  if (dir !== ".")
@@ -146,16 +148,18 @@ export async function initCommand(deps, opts = {}) {
146
148
  if (!installed || opts.skipInstall) {
147
149
  steps.push([`${pm} install`, "Install dependencies"]);
148
150
  }
149
- steps.push([`${pm} run dev`, "Start the local dev server"]);
150
151
  if (!opts.from) {
151
- deps.print("");
152
- for (const line of alignedSteps(steps))
153
- deps.print(line);
152
+ if (steps.length > 0) {
153
+ deps.print("");
154
+ for (const line of alignedSteps(steps))
155
+ deps.print(line);
156
+ }
154
157
  deps.print("");
155
158
  deps.print(" No account needed. When you're ready to put it online:");
156
159
  for (const line of alignedSteps([
157
160
  ["vincentt login", "Connect this machine to Vincentt"],
158
161
  ["vincentt create", "Register the project and reserve its address"],
162
+ ["vincentt preview", "Try it on your phone"],
159
163
  ])) {
160
164
  deps.print(line);
161
165
  }
@@ -181,6 +185,7 @@ export async function initCommand(deps, opts = {}) {
181
185
  return code;
182
186
  deps.print("");
183
187
  deps.print(" Next:");
188
+ steps.push(["vincentt preview", "Try it on your phone"]);
184
189
  for (const line of alignedSteps(steps))
185
190
  deps.print(line);
186
191
  return 0;
@@ -86,10 +86,10 @@ export async function registerAfterScaffold(deps, opts = {}) {
86
86
  resume(deps.print, deps.dirLabel, resumeCommand, needsCd);
87
87
  if (scaffolded) {
88
88
  deps.print("");
89
- // B-F2-9(a): the open route must be named in something the creator's
90
- // agent reads. On a non-TTY this output IS that artifact — the TTY
91
- // branch's login prompt, which carries the same line, is unreachable here.
92
- deps.print(" Or don't: the local dev loop needs no account.");
89
+ // B-F3-6 (amended): the product points only at the platform, so this
90
+ // ending names what signing in buys, never the local dev loop. The open
91
+ // route B-F2-9(a) requires stays named by the template's verb list.
92
+ deps.print(" Signing in is what gives it an address a phone can open.");
93
93
  }
94
94
  deps.print("");
95
95
  deps.print(closing);
@@ -176,6 +176,7 @@ export async function previewStart(projectCwd, onLog, extra = {}) {
176
176
  expiresAt: preview.expiresAt,
177
177
  pidStartTime: await readProcessStartTime(process.pid),
178
178
  token: preview.relayToken,
179
+ sessionId: preview.sessionId,
179
180
  });
180
181
  return { preview, started: true };
181
182
  }
@@ -203,16 +203,15 @@ export async function createCommand(deps, opts = {}) {
203
203
  if (code !== 0 && code !== EXIT.DELIVERED_NOT_REGISTERED)
204
204
  return code;
205
205
  if (scaffolded) {
206
- // This reader has never run the dev server, so the block that fits is the one
207
- // `init` already ships for exactly this reader — two aligned columns off the
208
- // DETECTED package manager, never a hardcoded one (the template ships a pnpm
209
- // lockfile and a hoisted node-linker).
206
+ // Two aligned columns off the DETECTED package manager, never a hardcoded one
207
+ // (the template ships a pnpm lockfile and a hoisted node-linker). No dev-server
208
+ // step: `vincentt preview` starts it, and the product never points the creator
209
+ // at the local loop (B-F3-6).
210
210
  const steps = [];
211
211
  if (opts.dir)
212
212
  steps.push([`cd ${opts.dir}`, ""]);
213
213
  if (!installed || opts.skipInstall)
214
214
  steps.push([`${pm} install`, "Install dependencies"]);
215
- steps.push([`${pm} run dev`, "Start the local dev server"]);
216
215
  steps.push(["vincentt preview", "Try it on your phone"]);
217
216
  deps.print("");
218
217
  for (const line of alignedSteps(steps))
@@ -30,6 +30,12 @@ export interface RelayInfo {
30
30
  * whose relay asks for none.
31
31
  */
32
32
  token?: string;
33
+ /**
34
+ * The running session's id, so a re-run can hand over the console page that
35
+ * shows THIS preview rather than the bare project page. Never printed on its
36
+ * own; absent from markers written by an older CLI.
37
+ */
38
+ sessionId?: string;
33
39
  }
34
40
  export declare function relayInfoPath(cwd: string): string;
35
41
  /**
@@ -166,7 +166,7 @@ export async function runPkceLogin(opts) {
166
166
  value: result.code,
167
167
  page: {
168
168
  status: 200,
169
- html: browserPage(true, "You're all set", "Return to your terminal. You can close this tab."),
169
+ html: browserPage(true, "You're Signed In", "Go back to your AI assistant to continue."),
170
170
  },
171
171
  };
172
172
  }
@@ -6,8 +6,8 @@ import { type UpgradeFacts } from "../shared/upgradeError.js";
6
6
  export interface PreviewUiDeps {
7
7
  print: (line: string) => void;
8
8
  now: () => number;
9
- /** The forward target, named identically in the start block and the 502. */
10
- targetUrl: string;
9
+ /** The forwarded dev port, named identically in the settings banner and the 502. */
10
+ devPort: number;
11
11
  color: boolean;
12
12
  /** Filter the stream to one tester (`--tester`) — one tab, across refreshes. */
13
13
  filterTester?: string;
@@ -238,7 +238,7 @@ export class PreviewUi {
238
238
  const settings = this.deps.devCommand !== undefined && this.deps.portSource !== undefined
239
239
  ? { devCommand: this.deps.devCommand, portSource: this.deps.portSource }
240
240
  : undefined;
241
- this.lines(copy.originDown(this.deps.targetUrl, settings).map((l) => ` ${l}`));
241
+ this.lines(copy.originDown(this.deps.devPort, settings).map((l) => ` ${l}`));
242
242
  }
243
243
  originRecovered() {
244
244
  this.originDown = false;
@@ -1,14 +1,17 @@
1
+ /**
2
+ * Flow L13 — the binding line, printed BEFORE the mint. The creator sees which
3
+ * project this folder is before anything is minted, the only moment a wrong
4
+ * binding is cheap to catch; the start block follows once the preview is live.
5
+ */
6
+ export declare function bindingLine(projectName: string, orgName: string): string[];
7
+ /** Flow L13 — the start block, after the binding line. */
1
8
  export interface StartBlockInput {
2
- projectName: string;
3
- orgName: string;
4
- /** The forward target, printed BEFORE the URL so the 502 advisory matches it. */
5
- targetUrl: string;
6
9
  url: string;
7
10
  /**
8
- * The project's console page, or undefined for a folder with no binding. An
9
- * unlinked folder gets NO console line rather than a guessed one — the same rule
10
- * `consoleVerbCommand` follows, and for the same reason: a link to the wrong
11
- * project is worse than no link.
11
+ * The project's console page. Always set on the start path, because a preview
12
+ * refuses an unlinked folder; kept optional so a missing binding prints NO
13
+ * console line rather than a guessed one — a link to the wrong project is worse
14
+ * than no link.
12
15
  */
13
16
  consoleUrl?: string;
14
17
  /**
@@ -24,17 +27,15 @@ export interface StartBlockInput {
24
27
  }
25
28
  export declare function startBlock(i: StartBlockInput): string[];
26
29
  /**
27
- * Flow L14 — not signed in. The B2/B21 line, and the one screen that must not
28
- * feel like a wall.
30
+ * Flow L14 — not signed in.
29
31
  *
30
32
  * Line 1 is f1's exact string, so a creator meeting this gate in a second
31
- * command reads one product. The last paragraph is the most important copy in
32
- * f3's CLI: the overview commits that the local room stays a real room, and a
33
- * creator told only "sign in" has just been told the product does not work
34
- * without an account, which is false. It is offered as a CAPABILITY, not as
35
- * consolation — no "unfortunately", no "you can still".
33
+ * command reads one product. The second line is the reason, not a rule. No local
34
+ * alternative is offered (B-F3-6 as amended: the local loop keeps working, but
35
+ * no product surface points at it) — an agent reading this screen relays what
36
+ * it says, and the only thing to relay is: sign in.
36
37
  */
37
- export declare function notSignedIn(devCommand?: string, localUrl?: string): string[];
38
+ export declare function notSignedIn(): string[];
38
39
  /**
39
40
  * Flow P1 — the two settings lines, printed right after resolution and before
40
41
  * the sign-in check, the mint and the spawn. They are the ONLY disclosure of the
@@ -72,9 +73,10 @@ export declare function followingFooter(): string[];
72
73
  * room over an `npm run dev` they forgot.
73
74
  *
74
75
  * The middle line splits the wire in two, because the creator's first instinct
75
- * on a 502 is to suspect us.
76
+ * on a 502 is to suspect us. It names the PORT, matching the settings banner,
77
+ * never a loopback URL an agent could paste.
76
78
  */
77
- export declare function originDown(targetUrl: string, settings?: {
79
+ export declare function originDown(devPort: number, settings?: {
78
80
  devCommand: string;
79
81
  portSource: "flag" | "vincentt.json" | "default";
80
82
  }): string[];
@@ -197,10 +199,13 @@ export declare function supersededLoser(): string[];
197
199
  * each of those re-runs superseded the previous session: the link really did
198
200
  * keep changing, because the attach never printed one.
199
201
  *
200
- * The URL renders bare on its own line, matching the start block, so anything
201
- * scraping for a link finds it in the same shape either way.
202
+ * The two addresses render in the start block's order and shape (Flow L16a),
203
+ * console page first, so an agent that re-runs preview to recover a link finds
204
+ * the console page exactly where the start block put it. `consoleUrl` carries
205
+ * the running session when the marker holds its id, and is undefined only for an
206
+ * unlinked folder, which prints no console line rather than a guessed one.
202
207
  */
203
- export declare function attachedToRunning(url: string, expiresAt?: string): string[];
208
+ export declare function attachedToRunning(url: string, consoleUrl?: string, expiresAt?: string): string[];
204
209
  /**
205
210
  * c. Superseded — the terminal that WON. This sits ABOVE the `✓ Preview is live`
206
211
  * block so the consequence is read before the URL is copied.
@@ -16,38 +16,44 @@
16
16
  // session id, the edge hostname, or the workingTreeKey. The grant is the
17
17
  // sharpest — it transits an agent's tool-output transcript by default in this
18
18
  // product, so f1's "a token is never printed" extends to it verbatim.
19
- /** Flow L13 — the start block. */
20
19
  import { escapeControl, terminalSafe } from "../lib/terminalText.js";
21
20
  import { INSTALL_COMMAND } from "../shared/upgradeError.js";
22
- export function startBlock(i) {
23
- const out = [
21
+ /**
22
+ * Flow L13 — the binding line, printed BEFORE the mint. The creator sees which
23
+ * project this folder is before anything is minted, the only moment a wrong
24
+ * binding is cheap to catch; the start block follows once the preview is live.
25
+ */
26
+ export function bindingLine(projectName, orgName) {
27
+ return [
24
28
  "",
25
29
  // The org is dropped rather than rendered as a dangling separator when it
26
30
  // is not known. `Museum AR Tour · ` reads as a truncated line — the one
27
31
  // moment a wrong binding is cheap to catch is the one moment the line must
28
32
  // not look broken.
29
- i.orgName ? `→ ${i.projectName} · ${i.orgName}` : `→ ${i.projectName}`,
30
- `→ Serving ${i.targetUrl}`,
31
- "",
32
- "✓ Preview is live",
33
- "",
34
- // The URL is on its own line, indented, unwrapped and UNADORNED. No `URL:`
35
- // label, no backticks, no surrounding punctuation — it is the single
36
- // most-copied string in the product, and anything beside it ends up in
37
- // someone's paste.
38
- ` ${i.url}`,
39
- "",
33
+ orgName ? `→ ${projectName} · ${orgName}` : `→ ${projectName}`,
40
34
  ];
35
+ }
36
+ export function startBlock(i) {
37
+ // No loopback address: the settings banner already names the dev port, and a
38
+ // URL in this transcript is a URL an agent can paste. A loopback one reaches
39
+ // nobody but this machine (B-F3-6: the local loop works, no surface points at
40
+ // it).
41
+ const out = ["", "✓ Preview is live", ""];
42
+ // Each URL is on its own line, indented, unwrapped and UNADORNED. No `URL:`
43
+ // label, no backticks, no surrounding punctuation — anything beside it ends up
44
+ // in someone's paste.
45
+ //
46
+ // The console page comes FIRST: it carries the phone link, the QR, the live
47
+ // device view and Stop, so it is the one address an agent hands over, and the
48
+ // first one it reads.
49
+ if (i.consoleUrl) {
50
+ out.push(" Open the console:", "", ` ${i.consoleUrl}`, "");
51
+ }
52
+ out.push(" On a phone:", "", ` ${i.url}`, "");
41
53
  if (i.qr && i.qr.length > 0) {
42
54
  out.push(...i.qr.map((row) => ` ${row}`), "");
43
55
  }
44
56
  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`, "");
45
- // BELOW the phone URL and its instruction, never above. The phone address is the
46
- // line the creator is waiting on; a second URL printed first is a second thing to
47
- // read before the one that matters. Dropped entirely when the folder is unlinked.
48
- if (i.consoleUrl) {
49
- out.push(" In the console:", "", ` ${i.consoleUrl}`, "");
50
- }
51
57
  if (i.feedbackOrigin) {
52
58
  out.push(` Feedback is accepted from the console at ${terminalSafe(i.feedbackOrigin)}`, "");
53
59
  }
@@ -55,17 +61,15 @@ export function startBlock(i) {
55
61
  return out;
56
62
  }
57
63
  /**
58
- * Flow L14 — not signed in. The B2/B21 line, and the one screen that must not
59
- * feel like a wall.
64
+ * Flow L14 — not signed in.
60
65
  *
61
66
  * Line 1 is f1's exact string, so a creator meeting this gate in a second
62
- * command reads one product. The last paragraph is the most important copy in
63
- * f3's CLI: the overview commits that the local room stays a real room, and a
64
- * creator told only "sign in" has just been told the product does not work
65
- * without an account, which is false. It is offered as a CAPABILITY, not as
66
- * consolation — no "unfortunately", no "you can still".
67
+ * command reads one product. The second line is the reason, not a rule. No local
68
+ * alternative is offered (B-F3-6 as amended: the local loop keeps working, but
69
+ * no product surface points at it) — an agent reading this screen relays what
70
+ * it says, and the only thing to relay is: sign in.
67
71
  */
68
- export function notSignedIn(devCommand = "npm run dev", localUrl = "http://localhost:5173") {
72
+ export function notSignedIn() {
69
73
  return [
70
74
  "",
71
75
  "✗ You're not signed in.",
@@ -73,10 +77,7 @@ export function notSignedIn(devCommand = "npm run dev", localUrl = "http://local
73
77
  " A preview needs a hostname your phone can reach, and that needs an account.",
74
78
  "",
75
79
  " Sign in: vincentt login",
76
- "",
77
- ` Your local dev loop does not need one. Run \`${devCommand}\` and open`,
78
- ` ${localUrl} on this machine. The camera works there.`,
79
- " What signing in adds is the phone.",
80
+ " Then start it again: vincentt preview",
80
81
  "",
81
82
  ];
82
83
  }
@@ -156,12 +157,13 @@ export function followingFooter() {
156
157
  * room over an `npm run dev` they forgot.
157
158
  *
158
159
  * The middle line splits the wire in two, because the creator's first instinct
159
- * on a 502 is to suspect us.
160
+ * on a 502 is to suspect us. It names the PORT, matching the settings banner,
161
+ * never a loopback URL an agent could paste.
160
162
  */
161
- export function originDown(targetUrl, settings) {
163
+ export function originDown(devPort, settings) {
162
164
  if (settings === undefined) {
163
165
  return [
164
- `! Your dev server isn't responding on ${targetUrl}.`,
166
+ `! Your dev server isn't responding on port ${devPort}.`,
165
167
  "",
166
168
  " The preview is still live. The phone is reaching us; we can't reach your app.",
167
169
  " Start it and reload the phone: npm run dev",
@@ -179,7 +181,7 @@ export function originDown(targetUrl, settings) {
179
181
  ? " If it runs on another port, pass that port with --port."
180
182
  : " If it runs on another port, pass --port or add vincentt.json.";
181
183
  return [
182
- `! Your dev server isn't responding on ${targetUrl}${source}.`,
184
+ `! Your dev server isn't responding on port ${devPort}${source}.`,
183
185
  "",
184
186
  " The preview is still live. The phone is reaching us; we can't reach your app.",
185
187
  ` Start it and reload the phone: ${settings.devCommand}`,
@@ -405,11 +407,18 @@ export function supersededLoser() {
405
407
  * each of those re-runs superseded the previous session: the link really did
406
408
  * keep changing, because the attach never printed one.
407
409
  *
408
- * The URL renders bare on its own line, matching the start block, so anything
409
- * scraping for a link finds it in the same shape either way.
410
+ * The two addresses render in the start block's order and shape (Flow L16a),
411
+ * console page first, so an agent that re-runs preview to recover a link finds
412
+ * the console page exactly where the start block put it. `consoleUrl` carries
413
+ * the running session when the marker holds its id, and is undefined only for an
414
+ * unlinked folder, which prints no console line rather than a guessed one.
410
415
  */
411
- export function attachedToRunning(url, expiresAt) {
412
- const lines = ["", "→ A preview is already running for this folder.", "", url, ""];
416
+ export function attachedToRunning(url, consoleUrl, expiresAt) {
417
+ const lines = ["", "→ A preview is already running for this folder.", ""];
418
+ if (consoleUrl) {
419
+ lines.push(" Open the console:", "", ` ${consoleUrl}`, "");
420
+ }
421
+ lines.push(" On a phone:", "", ` ${url}`, "");
413
422
  // Stated only when known: a marker written by an older CLI has no expiry, and
414
423
  // inventing one would be the over-claim this copy exists to avoid.
415
424
  if (expiresAt !== undefined) {
@@ -147,7 +147,9 @@ export function startRelay(opts = {}) {
147
147
  });
148
148
  ws.on("close", () => log("client disconnected"));
149
149
  });
150
- http.listen(port, host, () => log(`relay listening on ${host}:${port} (ws ${wsPath}, http /query)`));
150
+ // Port only: this line reaches `vincentt preview`'s transcript, and a loopback
151
+ // address there is one an agent can paste and hand over.
152
+ http.listen(port, host, () => log(`relay listening on port ${port} (ws ${wsPath}, http /query)`));
151
153
  return {
152
154
  store,
153
155
  annotations,
@@ -50,8 +50,6 @@ export type RefusalArm = {
50
50
  stillServing?: number;
51
51
  } | {
52
52
  kind: "preview_start";
53
- devCommand: string;
54
- localUrl: string;
55
53
  };
56
54
  /** Flow L22: the version refusal on any verb, in the arm that knows where it landed. */
57
55
  export declare function upgradeRefusalLines(facts: UpgradeFacts, arm?: RefusalArm, own?: string): string[];
@@ -171,11 +171,10 @@ export function upgradeRefusalLines(facts, arm = { kind: "generic" }, own = clie
171
171
  "",
172
172
  remedy(variant),
173
173
  ];
174
+ // No local-loop paragraph on preview_start (B-F3-6 as amended): the remedy is
175
+ // the upgrade, and the screen ends on the command that leads back to a preview.
174
176
  if (variant !== "not_received")
175
177
  out.push(thenLine(arm));
176
- if (arm.kind === "preview_start") {
177
- out.push("", ` Your local dev loop does not need an upgrade. Run \`${arm.devCommand}\` and open`, ` ${arm.localUrl} on this machine. The camera works there.`);
178
- }
179
178
  out.push("");
180
179
  return out;
181
180
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincentt-xr/cli",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Vincentt CLI — the vincentt command-line dev loop (login, create, publish, preview, diagnostics, feedback, relay)",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",