@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.
- package/README.md +40 -13
- package/dist/cli/args.d.ts +12 -0
- package/dist/cli/args.js +40 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +308 -95
- package/dist/cli/usage-text.d.ts +1 -0
- package/dist/cli/usage-text.js +97 -0
- package/dist/cli/verb-surface.d.ts +24 -0
- package/dist/cli/verb-surface.js +49 -0
- package/dist/dx/clusters.d.ts +0 -17
- package/dist/dx/clusters.js +50 -1
- package/dist/lib/backend.d.ts +11 -0
- package/dist/lib/backend.js +23 -0
- package/dist/lib/classifyTarget.d.ts +42 -0
- package/dist/lib/classifyTarget.js +83 -0
- package/dist/lib/feedback.d.ts +46 -5
- package/dist/lib/feedback.js +76 -19
- package/dist/lib/feedbackCopy.d.ts +70 -0
- package/dist/lib/feedbackCopy.js +163 -0
- package/dist/lib/initFrom.d.ts +26 -4
- package/dist/lib/initFrom.js +65 -41
- package/dist/lib/lifecycle.d.ts +28 -3
- package/dist/lib/lifecycle.js +99 -7
- package/dist/lib/listProjects.d.ts +41 -0
- package/dist/lib/listProjects.js +77 -0
- package/dist/lib/projects.d.ts +64 -8
- package/dist/lib/projects.js +316 -24
- package/dist/lib/sourceRefCopy.d.ts +19 -0
- package/dist/lib/sourceRefCopy.js +36 -0
- package/dist/preview/command.d.ts +11 -0
- package/dist/preview/command.js +19 -1
- package/dist/preview/copy.d.ts +19 -3
- package/dist/preview/copy.js +21 -5
- package/dist/preview/runner.d.ts +6 -0
- package/dist/preview/runner.js +1 -0
- package/dist/preview/stream.d.ts +11 -0
- package/dist/preview/stream.js +13 -0
- package/dist/relay/annotations.d.ts +28 -4
- package/dist/relay/annotations.js +23 -7
- package/dist/relay/qa.f12.fixtures.d.ts +154 -0
- package/dist/relay/qa.f12.fixtures.js +358 -0
- package/dist/relay/server.d.ts +41 -3
- package/dist/relay/server.js +90 -32
- package/package.json +2 -2
- package/dist/library/qa.f5-b32.spec.d.ts +0 -1
- package/dist/library/qa.f5-b32.spec.js +0 -202
- package/dist/library/qa.f5-cli.spec.d.ts +0 -1
- package/dist/library/qa.f5-cli.spec.js +0 -433
- package/dist/library/qa.f5-contract.spec.d.ts +0 -1
- package/dist/library/qa.f5-contract.spec.js +0 -206
- package/dist/relay/injectors.d.ts +0 -29
- 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.
|
package/dist/preview/command.js
CHANGED
|
@@ -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
|
package/dist/preview/copy.d.ts
CHANGED
|
@@ -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.
|
|
96
|
-
*
|
|
97
|
-
*
|
|
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
|
*/
|
package/dist/preview/copy.js
CHANGED
|
@@ -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`, ""
|
|
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.
|
|
202
|
-
*
|
|
203
|
-
*
|
|
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
|
|
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
|
"",
|
package/dist/preview/runner.d.ts
CHANGED
|
@@ -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). */
|
package/dist/preview/runner.js
CHANGED
|
@@ -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;
|
package/dist/preview/stream.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/preview/stream.js
CHANGED
|
@@ -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.
|
|
24
|
-
*
|
|
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<
|
|
46
|
+
nextAfter(after: number, timeoutMs: number): Promise<NextOutcome>;
|
|
27
47
|
private wake;
|
|
28
|
-
/**
|
|
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.
|
|
86
|
-
*
|
|
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(
|
|
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
|
-
/**
|
|
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(
|
|
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 {};
|