@vincentt-xr/cli 0.9.0 → 0.11.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 (52) hide show
  1. package/README.md +40 -13
  2. package/dist/cli/args.d.ts +12 -0
  3. package/dist/cli/args.js +40 -0
  4. package/dist/cli/index.d.ts +1 -1
  5. package/dist/cli/index.js +308 -95
  6. package/dist/cli/usage-text.d.ts +1 -0
  7. package/dist/cli/usage-text.js +97 -0
  8. package/dist/cli/verb-surface.d.ts +24 -0
  9. package/dist/cli/verb-surface.js +49 -0
  10. package/dist/dx/clusters.d.ts +0 -17
  11. package/dist/dx/clusters.js +50 -1
  12. package/dist/lib/backend.d.ts +11 -0
  13. package/dist/lib/backend.js +23 -0
  14. package/dist/lib/classifyTarget.d.ts +42 -0
  15. package/dist/lib/classifyTarget.js +83 -0
  16. package/dist/lib/feedback.d.ts +46 -5
  17. package/dist/lib/feedback.js +76 -19
  18. package/dist/lib/feedbackCopy.d.ts +70 -0
  19. package/dist/lib/feedbackCopy.js +163 -0
  20. package/dist/lib/initFrom.d.ts +26 -4
  21. package/dist/lib/initFrom.js +65 -41
  22. package/dist/lib/lifecycle.d.ts +28 -3
  23. package/dist/lib/lifecycle.js +99 -7
  24. package/dist/lib/listProjects.d.ts +41 -0
  25. package/dist/lib/listProjects.js +77 -0
  26. package/dist/lib/projects.d.ts +64 -8
  27. package/dist/lib/projects.js +316 -24
  28. package/dist/lib/sourceRefCopy.d.ts +19 -0
  29. package/dist/lib/sourceRefCopy.js +36 -0
  30. package/dist/preview/command.d.ts +11 -0
  31. package/dist/preview/command.js +19 -1
  32. package/dist/preview/copy.d.ts +19 -3
  33. package/dist/preview/copy.js +21 -5
  34. package/dist/preview/runner.d.ts +6 -0
  35. package/dist/preview/runner.js +1 -0
  36. package/dist/preview/stream.d.ts +11 -0
  37. package/dist/preview/stream.js +13 -0
  38. package/dist/relay/annotations.d.ts +28 -4
  39. package/dist/relay/annotations.js +23 -7
  40. package/dist/relay/qa.f12.fixtures.d.ts +154 -0
  41. package/dist/relay/qa.f12.fixtures.js +358 -0
  42. package/dist/relay/server.d.ts +41 -3
  43. package/dist/relay/server.js +90 -32
  44. package/package.json +2 -2
  45. package/dist/library/qa.f5-b32.spec.d.ts +0 -1
  46. package/dist/library/qa.f5-b32.spec.js +0 -202
  47. package/dist/library/qa.f5-cli.spec.d.ts +0 -1
  48. package/dist/library/qa.f5-cli.spec.js +0 -433
  49. package/dist/library/qa.f5-contract.spec.d.ts +0 -1
  50. package/dist/library/qa.f5-contract.spec.js +0 -206
  51. package/dist/relay/injectors.d.ts +0 -29
  52. package/dist/relay/injectors.js +0 -72
@@ -60,6 +60,42 @@ export function printSourceRefFailure(print, failure, ctx) {
60
60
  print("");
61
61
  print(" Nothing was written.");
62
62
  }
63
+ /**
64
+ * `--template` / `--from` on `create` — the parser refusal, and it is a security
65
+ * control rather than a scope note. The basis for installing dependencies on the
66
+ * unauthenticated scaffold path is that `create` can only ever clone our own
67
+ * hardcoded template at a release-gated ref; this screen is what makes that
68
+ * structurally true rather than conventional.
69
+ *
70
+ * "on `create`" is REQUIRED on line 1 and is not required by the shipped
71
+ * `printFlagConflict` precedent: `--template` really does exist, on `init`, and the
72
+ * remedy three lines below depends on that being true. An unqualified claim is one
73
+ * a creator disproves in one command.
74
+ *
75
+ * The creator's own typed arguments are carried through verbatim, so all three
76
+ * remedy lines are copy-pasteable with no placeholder to guess.
77
+ */
78
+ export function printCreateSourceFlagRefusal(print, flagName, ctx) {
79
+ const dir = ctx.dir;
80
+ print(`✗ There's no \`${flagName}\` option on \`create\`.`);
81
+ print("");
82
+ if (flagName === "--from") {
83
+ print(" `create` scaffolds the standard starter and nothing else. To start from");
84
+ print(" a gallery template, scaffold first and register after:");
85
+ print("");
86
+ print(` vincentt init ${dir} --from ${ctx.value ?? "owner/repo@tag"}`);
87
+ }
88
+ else {
89
+ print(" `create` scaffolds the standard starter and nothing else. To choose a");
90
+ print(" template or a version, scaffold first and register after:");
91
+ print("");
92
+ print(` vincentt init ${dir} --template ${ctx.value ?? "<version>"}`);
93
+ }
94
+ print(` cd ${dir}`);
95
+ print(" vincentt create");
96
+ print("");
97
+ print(" Nothing was written.");
98
+ }
63
99
  /** `--from` with `--template`, or with an install-enabling flag. */
64
100
  export function printFlagConflict(print, conflict, ctx) {
65
101
  const [a, b] = conflict.flags;
@@ -65,6 +65,17 @@ export declare class PreviewUi {
65
65
  events(evs: DiagEvent[], tags: EventTags, opts?: {
66
66
  attribute?: boolean;
67
67
  }): void;
68
+ /**
69
+ * An annotation arrived from the phone. One line per annotation — it is an
70
+ * event, not a condition, so the once-per-stretch rule that governs advisories
71
+ * does not apply.
72
+ *
73
+ * An un-attributed send is dropped from the terminal for the same reason an
74
+ * un-attributed event is: column one is the tester, and inventing a
75
+ * placeholder would read as a real one. The annotation itself is still stored
76
+ * and still handed over by `vincentt feedback`.
77
+ */
78
+ annotation(tags: EventTags): void;
68
79
  /**
69
80
  * L18. Recovery is announced by the 200 that follows and by NOTHING else — a
70
81
  * "recovered!" line would be a second interruption for a non-event.
@@ -12,7 +12,7 @@
12
12
  // file works, and an agent reading the transcript gets the same information a
13
13
  // human does.
14
14
  import * as copy from "./copy.js";
15
- import { PerfThrottle, palette, renderEvent, uaLabel } from "./stream.js";
15
+ import { PerfThrottle, palette, renderAnnotation, renderEvent, uaLabel, } from "./stream.js";
16
16
  import { ViewerRoster } from "./viewers.js";
17
17
  import { MAX_TESTERS } from "../relay/testers.js";
18
18
  /**
@@ -151,6 +151,24 @@ export class PreviewUi {
151
151
  this.line(` ${rendered}`);
152
152
  }
153
153
  }
154
+ /**
155
+ * An annotation arrived from the phone. One line per annotation — it is an
156
+ * event, not a condition, so the once-per-stretch rule that governs advisories
157
+ * does not apply.
158
+ *
159
+ * An un-attributed send is dropped from the terminal for the same reason an
160
+ * un-attributed event is: column one is the tester, and inventing a
161
+ * placeholder would read as a real one. The annotation itself is still stored
162
+ * and still handed over by `vincentt feedback`.
163
+ */
164
+ annotation(tags) {
165
+ const { tester } = tags;
166
+ if (tester === undefined)
167
+ return;
168
+ if (this.deps.filterTester !== undefined && tester !== this.deps.filterTester)
169
+ return;
170
+ this.line(` ${renderAnnotation(tester)}`);
171
+ }
154
172
  // ---- advisories -----------------------------------------------------------
155
173
  /**
156
174
  * L18. Recovery is announced by the 200 that follows and by NOTHING else — a
@@ -5,6 +5,13 @@ export interface StartBlockInput {
5
5
  /** The forward target, printed BEFORE the URL so the 502 advisory matches it. */
6
6
  targetUrl: string;
7
7
  url: string;
8
+ /**
9
+ * The project's console page, or undefined for a folder with no binding. An
10
+ * unlinked folder gets NO console line rather than a guessed one — the same rule
11
+ * `consoleVerbCommand` follows, and for the same reason: a link to the wrong
12
+ * project is worse than no link.
13
+ */
14
+ consoleUrl?: string;
8
15
  /** Pre-rendered QR rows, or undefined for non-TTY / piped / --no-qr. */
9
16
  qr?: string[];
10
17
  expiresInHours: number;
@@ -92,9 +99,18 @@ export declare function stopped(ranMs: number, totalConnections: number): string
92
99
  *
93
100
  * The claim is exactly what the wire supports: `stopped` is produced by one
94
101
  * 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.
102
+ * the same account as the reader. Never a teammate, never an admin.
103
+ *
104
+ * IT NAMES NO SURFACE, and that is the whole design of this screen. `endReason`
105
+ * carries ONE value for every actor on that route — this CLI's own stop, another
106
+ * terminal, another machine, the console's Stop control, and any surface added
107
+ * later. The API does not learn which one called and must not: deriving it would
108
+ * mean guessing a surface from a credential kind (a PAT-driven script is not "a
109
+ * terminal") and reporting the guess to the creator as fact. That is the
110
+ * over-claim class that produced the abandoned→hard_max defect.
111
+ *
112
+ * So the headline states only the event, and the second line describes the actor
113
+ * exactly as far as the compound key supports: the same account, not this window.
98
114
  *
99
115
  * `✓`, not `✗`: someone with the authority to stop it stopped it.
100
116
  */
@@ -38,7 +38,14 @@ export function startBlock(i) {
38
38
  if (i.qr && i.qr.length > 0) {
39
39
  out.push(...i.qr.map((row) => ` ${row}`), "");
40
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…", "");
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`, "");
42
+ // BELOW the phone URL and its instruction, never above. The phone address is the
43
+ // line the creator is waiting on; a second URL printed first is a second thing to
44
+ // read before the one that matters. Dropped entirely when the folder is unlinked.
45
+ if (i.consoleUrl) {
46
+ out.push(" In the console:", "", ` ${i.consoleUrl}`, "");
47
+ }
48
+ out.push(" Waiting for a device…", "");
42
49
  return out;
43
50
  }
44
51
  /**
@@ -198,16 +205,25 @@ export function stopped(ranMs, totalConnections) {
198
205
  *
199
206
  * The claim is exactly what the wire supports: `stopped` is produced by one
200
207
  * 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.
208
+ * the same account as the reader. Never a teammate, never an admin.
209
+ *
210
+ * IT NAMES NO SURFACE, and that is the whole design of this screen. `endReason`
211
+ * carries ONE value for every actor on that route — this CLI's own stop, another
212
+ * terminal, another machine, the console's Stop control, and any surface added
213
+ * later. The API does not learn which one called and must not: deriving it would
214
+ * mean guessing a surface from a credential kind (a PAT-driven script is not "a
215
+ * terminal") and reporting the guess to the creator as fact. That is the
216
+ * over-claim class that produced the abandoned→hard_max defect.
217
+ *
218
+ * So the headline states only the event, and the second line describes the actor
219
+ * exactly as far as the compound key supports: the same account, not this window.
204
220
  *
205
221
  * `✓`, not `✗`: someone with the authority to stop it stopped it.
206
222
  */
207
223
  export function stoppedElsewhere() {
208
224
  return [
209
225
  "",
210
- "✓ Preview stopped from another terminal. The link no longer works.",
226
+ "✓ Preview stopped. The link no longer works.",
211
227
  "",
212
228
  " It was stopped on your account, not by this window. Nothing is wrong here.",
213
229
  "",
@@ -96,6 +96,12 @@ export interface StartPreviewOptions {
96
96
  * line; the connection's events are still stored, just un-attributed.
97
97
  */
98
98
  onCapReached?: () => void;
99
+ /**
100
+ * An annotation arrived from the phone, tagged with the tester that sent it.
101
+ * Carries nothing about its content — the terminal states arrival, and the
102
+ * record itself goes to whoever is consuming the reverse channel.
103
+ */
104
+ onAnnotation?: (tags: EventTags) => void;
99
105
  /** The tunnel's own state — what drives the L19 reconnect advisories. */
100
106
  onTunnelState?: (state: TunnelState) => void;
101
107
  /** The creator's dev server is not answering (L18). */
@@ -83,6 +83,7 @@ export async function startPreview(opts) {
83
83
  onEvents: opts.onEvents,
84
84
  onAttribute: opts.onAttribute,
85
85
  onCapReached: opts.onCapReached,
86
+ onAnnotation: opts.onAnnotation,
86
87
  });
87
88
  front = createServer((req, res) => {
88
89
  const toRelay = req.url?.startsWith(harnessPath) === true;
@@ -34,6 +34,17 @@ export declare function renderNetworkWithSize(label: string, e: NetworkEvent, by
34
34
  export declare function displayPath(url: string): string;
35
35
  export declare function renderLog(label: string, e: LogEvent, p: Palette): string;
36
36
  export declare function renderTrace(label: string, e: TraceEvent): string;
37
+ /**
38
+ * An annotation arrived from the phone. Same column grammar as every other
39
+ * stream line — tester in column one, kind in the status column, then the fact.
40
+ *
41
+ * It states THAT one arrived and nothing else: no message, no id, no screenshot
42
+ * path, no count, and no command to run. The message is viewer-authored and
43
+ * hostile, and this line lands in the creator's scrollback and, pasted, in their
44
+ * agent's context. It is an event and not a condition, so one line per
45
+ * annotation is correct here where an advisory would print once per stretch.
46
+ */
47
+ export declare function renderAnnotation(label: string): string;
37
48
  /** The dispatcher. Returns null for an event kind the terminal does not print. */
38
49
  export declare function renderEvent(label: string, e: DiagEvent, p: Palette): string | null;
39
50
  /**
@@ -134,6 +134,19 @@ export function renderTrace(label, e) {
134
134
  const fps = e.fps.toFixed(0);
135
135
  return `${labelCol(label)}${padEnd("perf", STATUS_COL)} ${fps} fps · ${e.longTaskCount} long tasks · ${e.longestTaskMs}ms peak`;
136
136
  }
137
+ /**
138
+ * An annotation arrived from the phone. Same column grammar as every other
139
+ * stream line — tester in column one, kind in the status column, then the fact.
140
+ *
141
+ * It states THAT one arrived and nothing else: no message, no id, no screenshot
142
+ * path, no count, and no command to run. The message is viewer-authored and
143
+ * hostile, and this line lands in the creator's scrollback and, pasted, in their
144
+ * agent's context. It is an event and not a condition, so one line per
145
+ * annotation is correct here where an advisory would print once per stretch.
146
+ */
147
+ export function renderAnnotation(label) {
148
+ return `${labelCol(label)}${padEnd("feedback", STATUS_COL)} annotation received`;
149
+ }
137
150
  /** The dispatcher. Returns null for an event kind the terminal does not print. */
138
151
  export function renderEvent(label, e, p) {
139
152
  if (e.kind === "network")
@@ -1,6 +1,19 @@
1
1
  import type { Annotation, AnnotationInput } from "@vincentt-xr/harness/events";
2
2
  export declare const ANNOTATIONS_DIR = ".vincentt/annotations";
3
3
  export declare function annotationsDir(projectDir: string): string;
4
+ /**
5
+ * What ended a long-poll. A timeout and a teardown used to be the same value
6
+ * (`null`), which made a dead session byte-identical to a quiet one on the wire
7
+ * — so a parked consumer waited forever on a channel that could never speak.
8
+ */
9
+ export type NextOutcome = {
10
+ kind: "annotation";
11
+ annotation: Annotation;
12
+ } | {
13
+ kind: "timeout";
14
+ } | {
15
+ kind: "closed";
16
+ };
4
17
  export declare class AnnotationStore {
5
18
  private readonly projectDir;
6
19
  private readonly dir;
@@ -8,6 +21,7 @@ export declare class AnnotationStore {
8
21
  private seq;
9
22
  private waiters;
10
23
  private loaded;
24
+ private closed;
11
25
  constructor(projectDir: string, dir?: string);
12
26
  /** Load persisted annotations so seq continues across preview restarts. */
13
27
  init(): Promise<void>;
@@ -18,13 +32,23 @@ export declare class AnnotationStore {
18
32
  }): Promise<Annotation>;
19
33
  /** The first annotation with seq greater than `after`, if one is already held. */
20
34
  peekAfter(after: number): Annotation | undefined;
35
+ /** Every held annotation with seq greater than `after`, in seq order. */
36
+ listAfter(after: number): Annotation[];
21
37
  /**
22
38
  * Long-poll: resolve with the first annotation whose seq is greater than
23
- * `after`, either immediately (already held) or as soon as one arrives. Resolves
24
- * null if none arrives within `timeoutMs`.
39
+ * `after`, either immediately (already held) or as soon as one arrives.
40
+ * Resolves `timeout` if none arrives within `timeoutMs`, and `closed` if the
41
+ * store tore down while the caller was parked.
42
+ *
43
+ * `timeoutMs` of 0 means "answer now": nothing held resolves `timeout` on the
44
+ * next tick rather than parking.
25
45
  */
26
- nextAfter(after: number, timeoutMs: number): Promise<Annotation | null>;
46
+ nextAfter(after: number, timeoutMs: number): Promise<NextOutcome>;
27
47
  private wake;
28
- /** Release any parked waiters on teardown so nothing hangs. */
48
+ /**
49
+ * Release any parked waiters on teardown, telling them the channel ENDED. A
50
+ * teardown that resolved them the same way a timeout does left a parked
51
+ * consumer unable to tell a dead session from a quiet one.
52
+ */
29
53
  close(): void;
30
54
  }
@@ -29,6 +29,7 @@ export class AnnotationStore {
29
29
  seq = 0;
30
30
  waiters = [];
31
31
  loaded = false;
32
+ closed = false;
32
33
  constructor(projectDir, dir = annotationsDir(projectDir)) {
33
34
  this.projectDir = projectDir;
34
35
  this.dir = dir;
@@ -80,22 +81,32 @@ export class AnnotationStore {
80
81
  peekAfter(after) {
81
82
  return this.items.find((a) => a.seq > after);
82
83
  }
84
+ /** Every held annotation with seq greater than `after`, in seq order. */
85
+ listAfter(after) {
86
+ return this.items.filter((a) => a.seq > after);
87
+ }
83
88
  /**
84
89
  * Long-poll: resolve with the first annotation whose seq is greater than
85
- * `after`, either immediately (already held) or as soon as one arrives. Resolves
86
- * null if none arrives within `timeoutMs`.
90
+ * `after`, either immediately (already held) or as soon as one arrives.
91
+ * Resolves `timeout` if none arrives within `timeoutMs`, and `closed` if the
92
+ * store tore down while the caller was parked.
93
+ *
94
+ * `timeoutMs` of 0 means "answer now": nothing held resolves `timeout` on the
95
+ * next tick rather than parking.
87
96
  */
88
97
  nextAfter(after, timeoutMs) {
89
98
  const held = this.peekAfter(after);
90
99
  if (held)
91
- return Promise.resolve(held);
100
+ return Promise.resolve({ kind: "annotation", annotation: held });
101
+ if (this.closed)
102
+ return Promise.resolve({ kind: "closed" });
92
103
  return new Promise((resolve) => {
93
104
  const waiter = {
94
105
  after,
95
106
  resolve,
96
107
  timer: setTimeout(() => {
97
108
  this.waiters = this.waiters.filter((w) => w !== waiter);
98
- resolve(null);
109
+ resolve({ kind: "timeout" });
99
110
  }, timeoutMs),
100
111
  };
101
112
  waiter.timer.unref?.();
@@ -107,7 +118,7 @@ export class AnnotationStore {
107
118
  for (const w of this.waiters) {
108
119
  if (a.seq > w.after) {
109
120
  clearTimeout(w.timer);
110
- w.resolve(a);
121
+ w.resolve({ kind: "annotation", annotation: a });
111
122
  }
112
123
  else {
113
124
  remaining.push(w);
@@ -115,11 +126,16 @@ export class AnnotationStore {
115
126
  }
116
127
  this.waiters = remaining;
117
128
  }
118
- /** Release any parked waiters on teardown so nothing hangs. */
129
+ /**
130
+ * Release any parked waiters on teardown, telling them the channel ENDED. A
131
+ * teardown that resolved them the same way a timeout does left a parked
132
+ * consumer unable to tell a dead session from a quiet one.
133
+ */
119
134
  close() {
135
+ this.closed = true;
120
136
  for (const w of this.waiters) {
121
137
  clearTimeout(w.timer);
122
- w.resolve(null);
138
+ w.resolve({ kind: "closed" });
123
139
  }
124
140
  this.waiters = [];
125
141
  }
@@ -0,0 +1,154 @@
1
+ import { type ChildProcessByStdio } from "node:child_process";
2
+ import type { Readable } from "node:stream";
3
+ import { AnnotationStore } from "./annotations.js";
4
+ import { type RelayHandle } from "./server.js";
5
+ /** src/relay → packages/cli. The built entry is the one a creator runs. */
6
+ export declare const CLI_ENTRY: string;
7
+ export interface F12Tree {
8
+ dir: string;
9
+ cleanup: () => Promise<void>;
10
+ }
11
+ /**
12
+ * A mkdtemp project root with `.vincentt/`, a written relay.json, and a project
13
+ * BINDING.
14
+ *
15
+ * The binding is what makes the tree a tree: `relayInfoDir` resolves through
16
+ * `locateProjectBinding`, which walks up from the cwd looking for
17
+ * `.vincentt/project.json`. Without it a subdirectory of the project is a
18
+ * DIFFERENT tree with its own cursor at 0 — which is precisely the
19
+ * re-deliver-everything bug feedback.ts's header comment describes, so a fixture
20
+ * that omitted the binding could not express the tree-anchoring case at all.
21
+ */
22
+ export declare function F12_TREE(opts?: {
23
+ relayPort: number;
24
+ pid?: number;
25
+ harnessPath?: string;
26
+ }): Promise<F12Tree>;
27
+ /** A fresh store over the tree, nothing on disk. readCursor ⇒ 0. */
28
+ export declare function F12_STORE_EMPTY(projectDir: string): AnnotationStore;
29
+ /** A valid 1×1 PNG data URL — the real screenshot shape `add()` decodes. */
30
+ export declare const PNG_1X1: string;
31
+ /**
32
+ * F12_STORE_EMPTY plus three real `add()` calls ⇒ seq 1,2,3. `add()` is the same
33
+ * function the real POST path calls, so driving it exercises the real delivery
34
+ * path from one function inward.
35
+ */
36
+ export declare function F12_STORE_BATCH_3(projectDir: string, messages?: readonly string[]): Promise<AnnotationStore>;
37
+ export interface F12Relay {
38
+ handle: RelayHandle;
39
+ port: number;
40
+ /** Everything the relay wrote to its `onLog` seam. */
41
+ logs: string[];
42
+ /**
43
+ * The `onAnnotation` notifications, one per accepted POST, carrying only the
44
+ * resolved tester. This is the seam `preview/runner.ts` wires to the preview
45
+ * terminal's `annotation received` line (Flow C8) — it is NOT `onLog`, so a
46
+ * case that watched `logs` for arrivals would see none and wrongly conclude
47
+ * the line was never wired.
48
+ */
49
+ arrivals: Array<{
50
+ tester?: string;
51
+ viewer?: string;
52
+ }>;
53
+ /** The rendered Flow C8 stream lines, as the preview terminal would print them. */
54
+ streamLines: string[];
55
+ close: () => Promise<void>;
56
+ }
57
+ /**
58
+ * A real relay on a free port with an injectable store. Cases drive it through
59
+ * REAL HTTP rather than by calling handlers directly: the exit contract is
60
+ * defined in terms of status codes on a wire, and a handler-level call would not
61
+ * observe a 410 the socket never sent.
62
+ */
63
+ export declare function F12_RELAY_LIVE(projectDir: string, store?: AnnotationStore): Promise<F12Relay>;
64
+ /**
65
+ * A tree whose relay.json names a port nothing listens on and a pid that is NOT
66
+ * alive. The input to the liveness probe's dead arm.
67
+ */
68
+ export declare function F12_RELAY_DEAD(): Promise<F12Tree>;
69
+ /** Write `.vincentt/annotations/offset` = n. n===null writes nothing (absent). */
70
+ export declare function F12_CURSOR_AT(tree: string, n: number | null): Promise<void>;
71
+ /** Raw cursor bytes, or null when the file is absent. */
72
+ export declare function readCursorRaw(tree: string): Promise<string | null>;
73
+ export interface HostileRow {
74
+ row: string;
75
+ payload: string;
76
+ attacks: string;
77
+ }
78
+ export declare const F12_HOSTILE_MESSAGES: readonly HostileRow[];
79
+ export interface F12TwoTrees {
80
+ a: {
81
+ tree: F12Tree;
82
+ relay: F12Relay;
83
+ store: AnnotationStore;
84
+ };
85
+ b: {
86
+ tree: F12Tree;
87
+ relay: F12Relay;
88
+ store: AnnotationStore;
89
+ };
90
+ cleanup: () => Promise<void>;
91
+ }
92
+ /**
93
+ * Two trees, two relays on two ports, two stores, two cursors. The
94
+ * PROJECT-isolation fixture (QA-F12-G7). Deliberately NOT a tenant fixture —
95
+ * these are one creator's two checkouts, the only multiplicity f12 has.
96
+ */
97
+ export declare function F12_TWO_TREES(): Promise<F12TwoTrees>;
98
+ /** stdin ignored, stdout + stderr on their own pipes. */
99
+ type PipedChild = ChildProcessByStdio<null, Readable, Readable>;
100
+ export interface CliRun {
101
+ child: PipedChild;
102
+ /** Raw stdout BYTES — never merged with stderr. */
103
+ stdout: () => Buffer;
104
+ /** Raw stderr BYTES — never merged with stdout. */
105
+ stderr: () => Buffer;
106
+ out: () => string;
107
+ err: () => string;
108
+ /** Resolves with the OS-reported exit status. */
109
+ exit: Promise<{
110
+ code: number | null;
111
+ signal: NodeJS.Signals | null;
112
+ }>;
113
+ exited: () => boolean;
114
+ kill: (sig?: NodeJS.Signals) => void;
115
+ }
116
+ /**
117
+ * Spawn the BUILT CLI with stdout/stderr on SEPARATE pipes and expose the real
118
+ * exit status. `stdio: pipe` is load-bearing twice over: it keeps the two streams
119
+ * distinguishable (QA-F12-C1) and it makes stdout a PIPE, which is the
120
+ * asynchronous-write case the cursor invariant exists for (§3.1).
121
+ */
122
+ export declare function runCli(args: readonly string[], opts?: {
123
+ cwd: string;
124
+ env?: Record<string, string | undefined>;
125
+ }): CliRun;
126
+ /** Spawn, wait for exit, return everything. */
127
+ export declare function runCliToEnd(args: readonly string[], opts: {
128
+ cwd: string;
129
+ env?: Record<string, string | undefined>;
130
+ }): Promise<{
131
+ code: number | null;
132
+ signal: NodeJS.Signals | null;
133
+ stdout: Buffer;
134
+ stderr: Buffer;
135
+ out: string;
136
+ err: string;
137
+ }>;
138
+ /**
139
+ * A throwaway HOME, so the dx journal (and therefore the dx nudge) starts empty.
140
+ *
141
+ * The nudge is journal-state-dependent and fires on the Nth recent run, so two
142
+ * otherwise-identical invocations differ in stderr purely because one came later.
143
+ * Any case comparing two runs byte-for-byte must isolate this or it is asserting
144
+ * on run order rather than on the property it names.
145
+ */
146
+ export declare function freshHome(): Promise<string>;
147
+ export declare function sleep(ms: number): Promise<void>;
148
+ /** Wait until `cond()` or the deadline. Returns whether it became true. */
149
+ export declare function until(cond: () => boolean, timeoutMs?: number, stepMs?: number): Promise<boolean>;
150
+ /** Non-empty `\n`-split fragments of a raw stdout capture. */
151
+ export declare function ndjsonLines(stdout: Buffer): string[];
152
+ /** The Annotation key set §4.2 pins on stdout. */
153
+ export declare const ANNOTATION_KEYS: readonly ["id", "seq", "createdAt", "projectDir", "message", "screenshotPath", "spec", "sessionId"];
154
+ export {};