@vincentt-xr/cli 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -0
- package/dist/cli/args.d.ts +13 -0
- package/dist/cli/args.js +41 -0
- package/dist/cli/index.js +362 -92
- package/dist/lib/backend.d.ts +45 -5
- package/dist/lib/backend.js +99 -18
- package/dist/lib/diagnostics.d.ts +27 -2
- package/dist/lib/diagnostics.js +73 -8
- package/dist/lib/initCommand.d.ts +14 -0
- package/dist/lib/initCommand.js +75 -0
- package/dist/lib/lifecycle.d.ts +73 -15
- package/dist/lib/lifecycle.js +111 -114
- package/dist/lib/org.d.ts +29 -0
- package/dist/lib/org.js +45 -0
- package/dist/lib/projects.d.ts +82 -0
- package/dist/lib/projects.js +217 -0
- package/dist/lib/relayInfo.d.ts +41 -3
- package/dist/lib/relayInfo.js +94 -0
- package/dist/lib/whoami.d.ts +11 -0
- package/dist/lib/whoami.js +38 -0
- package/dist/login/authBackend.d.ts +30 -0
- package/dist/login/authBackend.js +80 -0
- package/dist/login/guard.d.ts +13 -0
- package/dist/login/guard.js +17 -0
- package/dist/login/loginCommand.d.ts +39 -0
- package/dist/login/loginCommand.js +72 -0
- package/dist/login/pkce.d.ts +47 -0
- package/dist/login/pkce.js +106 -0
- package/dist/login/pkceLogin.d.ts +53 -0
- package/dist/login/pkceLogin.js +152 -0
- package/dist/mcp/server.js +47 -20
- package/dist/preview/api.d.ts +84 -0
- package/dist/preview/api.js +174 -0
- package/dist/preview/command.d.ts +96 -0
- package/dist/preview/command.js +276 -0
- package/dist/preview/copy.d.ts +209 -0
- package/dist/preview/copy.js +444 -0
- package/dist/preview/index.d.ts +2 -3
- package/dist/preview/index.js +5 -6
- package/dist/preview/proxy.d.ts +12 -1
- package/dist/preview/proxy.js +17 -7
- package/dist/preview/qr.d.ts +30 -0
- package/dist/preview/qr.js +89 -0
- package/dist/preview/runner.d.ts +155 -30
- package/dist/preview/runner.js +219 -68
- package/dist/preview/stream.d.ts +52 -0
- package/dist/preview/stream.js +171 -0
- package/dist/preview/viewers.d.ts +113 -0
- package/dist/preview/viewers.js +242 -0
- package/dist/relay/server.d.ts +32 -0
- package/dist/relay/server.js +80 -12
- package/dist/relay/store.d.ts +32 -3
- package/dist/relay/store.js +76 -22
- package/dist/relay/testers.d.ts +90 -0
- package/dist/relay/testers.js +144 -0
- package/dist/shared/authError.d.ts +1 -0
- package/dist/shared/authError.js +11 -0
- package/dist/shared/config.d.ts +73 -5
- package/dist/shared/config.js +133 -20
- package/package.json +3 -7
- package/dist/cli/deprecated/harness-mcp.d.ts +0 -2
- package/dist/cli/deprecated/harness-mcp.js +0 -7
- package/dist/cli/deprecated/harness-relay.d.ts +0 -2
- package/dist/cli/deprecated/harness-relay.js +0 -7
- package/dist/cli/deprecated/harness.d.ts +0 -2
- package/dist/cli/deprecated/harness.js +0 -7
- package/dist/login/login.d.ts +0 -34
- package/dist/login/login.js +0 -148
- package/dist/preview/cloudflared.d.ts +0 -13
- package/dist/preview/cloudflared.js +0 -46
- package/dist/preview/tunnel.d.ts +0 -14
- package/dist/preview/tunnel.js +0 -28
package/dist/relay/store.js
CHANGED
|
@@ -1,22 +1,52 @@
|
|
|
1
1
|
// The relay's memory: a bounded ring buffer of recent diagnostics events,
|
|
2
2
|
// queryable the way the MCP server asks ("network events since seq 40", "last
|
|
3
|
-
// 20 errors"). Pure and dependency-free so it is fully
|
|
4
|
-
// server in server.ts is a thin wrapper that feeds this
|
|
5
|
-
// from it.
|
|
3
|
+
// 20 errors", "just tester t2"). Pure and dependency-free so it is fully
|
|
4
|
+
// unit-testable; the WS server in server.ts is a thin wrapper that feeds this
|
|
5
|
+
// and answers queries from it.
|
|
6
|
+
//
|
|
7
|
+
// This buffer is where diagnostics content lives, and the ONLY place besides
|
|
8
|
+
// the phone's memory (B19). It is on the CREATOR'S OWN MACHINE.
|
|
6
9
|
export class EventStore {
|
|
7
10
|
capacity;
|
|
8
11
|
buf = [];
|
|
9
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Insertion-ordered, so the roster reads in arrival order rather than in
|
|
14
|
+
* whatever order a hash lands. Labels are never removed: a viewer that
|
|
15
|
+
* disconnected 20 seconds ago still has events in this buffer, and dropping
|
|
16
|
+
* its label would make them unfilterable.
|
|
17
|
+
*/
|
|
18
|
+
viewersSeen = new Set();
|
|
19
|
+
/**
|
|
20
|
+
* Same rule, same reason, for testers — and it matters MORE here, because
|
|
21
|
+
* `--tester t1` is what a creator runs at the moment the tester who hit the
|
|
22
|
+
* bug has already closed their tab.
|
|
23
|
+
*
|
|
24
|
+
* Note what is NOT here: a set of raw client session keys. The store used to
|
|
25
|
+
* hold one and return it in `RelayResult.sessions`, which put a
|
|
26
|
+
* viewer-controlled string on the terminal and in MCP tool output. The raw key
|
|
27
|
+
* never enters this file; the relay maps it to a tester id first.
|
|
28
|
+
*/
|
|
29
|
+
testersSeen = new Set();
|
|
10
30
|
latest = -1;
|
|
11
31
|
constructor(capacity = 5000) {
|
|
12
32
|
this.capacity = capacity;
|
|
13
33
|
}
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Ingest a batch from a client. Both the VIEWER and the TESTER come from the
|
|
36
|
+
* connection's own resolution, never from the payload — the device never
|
|
37
|
+
* proposes its own label (B22), and an identifier taken from the body would be
|
|
38
|
+
* a viewer-supplied string flowing to the terminal and to MCP tool output.
|
|
39
|
+
*/
|
|
40
|
+
ingest(events, tags = {}) {
|
|
41
|
+
const { viewer, tester } = tags;
|
|
42
|
+
if (viewer !== undefined)
|
|
43
|
+
this.viewersSeen.add(viewer);
|
|
44
|
+
if (tester !== undefined)
|
|
45
|
+
this.testersSeen.add(tester);
|
|
17
46
|
for (const e of events) {
|
|
18
|
-
// Tag ownership on the stored copy so
|
|
19
|
-
|
|
47
|
+
// Tag ownership on the stored copy so per-tester and per-viewer queries
|
|
48
|
+
// can filter.
|
|
49
|
+
this.buf.push(withTags(e, viewer, tester));
|
|
20
50
|
if (e.seq > this.latest)
|
|
21
51
|
this.latest = e.seq;
|
|
22
52
|
}
|
|
@@ -28,7 +58,12 @@ export class EventStore {
|
|
|
28
58
|
/** Answer an MCP query against the buffer. Newest-biased when limited. */
|
|
29
59
|
query(q = {}) {
|
|
30
60
|
let out = this.buf.filter((e) => {
|
|
31
|
-
|
|
61
|
+
// Filters INTERSECT. A caller passing both wants the events matching both;
|
|
62
|
+
// ORing them would answer a question nobody asked with more data than they
|
|
63
|
+
// requested, under a heading naming one.
|
|
64
|
+
if (q.tester && testerOf(e) !== q.tester)
|
|
65
|
+
return false;
|
|
66
|
+
if (q.viewer && viewerOf(e) !== q.viewer)
|
|
32
67
|
return false;
|
|
33
68
|
if (q.kind && e.kind !== q.kind)
|
|
34
69
|
return false;
|
|
@@ -40,29 +75,48 @@ export class EventStore {
|
|
|
40
75
|
out = out.slice(out.length - q.limit); // keep the newest `limit`
|
|
41
76
|
}
|
|
42
77
|
return {
|
|
43
|
-
events: out.map(
|
|
78
|
+
events: out.map(stripTags),
|
|
79
|
+
// Parallel to `events`, so a follower can print the label in column one
|
|
80
|
+
// without inventing a placeholder for an unattributed event.
|
|
81
|
+
eventViewers: out.map(viewerOf),
|
|
82
|
+
eventTesters: out.map(testerOf),
|
|
44
83
|
latestSeq: this.latest,
|
|
45
|
-
|
|
84
|
+
testers: [...this.testersSeen],
|
|
85
|
+
viewers: [...this.viewersSeen],
|
|
46
86
|
};
|
|
47
87
|
}
|
|
88
|
+
/** The label an event carries, for the live terminal stream. */
|
|
89
|
+
viewerFor(e) {
|
|
90
|
+
return viewerOf(e);
|
|
91
|
+
}
|
|
92
|
+
/** The tester an event carries, for the live terminal stream. */
|
|
93
|
+
testerFor(e) {
|
|
94
|
+
return testerOf(e);
|
|
95
|
+
}
|
|
48
96
|
size() {
|
|
49
97
|
return this.buf.length;
|
|
50
98
|
}
|
|
51
99
|
}
|
|
52
|
-
// The store tags each event with its owning
|
|
53
|
-
// so
|
|
54
|
-
//
|
|
55
|
-
const
|
|
56
|
-
|
|
100
|
+
// The store tags each event with its owning viewer and tester out-of-band
|
|
101
|
+
// (symbol keys) so they survive in the buffer without polluting the wire type.
|
|
102
|
+
// Queries strip them before returning.
|
|
103
|
+
const VIEWER = Symbol("viewer");
|
|
104
|
+
const TESTER = Symbol("tester");
|
|
105
|
+
function withTags(e, viewer, tester) {
|
|
57
106
|
return Object.assign(Object.create(Object.getPrototypeOf(e)), e, {
|
|
58
|
-
[
|
|
107
|
+
...(viewer === undefined ? {} : { [VIEWER]: viewer }),
|
|
108
|
+
...(tester === undefined ? {} : { [TESTER]: tester }),
|
|
59
109
|
});
|
|
60
110
|
}
|
|
61
|
-
function
|
|
62
|
-
return e[
|
|
111
|
+
function viewerOf(e) {
|
|
112
|
+
return e[VIEWER];
|
|
113
|
+
}
|
|
114
|
+
function testerOf(e) {
|
|
115
|
+
return e[TESTER];
|
|
63
116
|
}
|
|
64
|
-
function
|
|
117
|
+
function stripTags(e) {
|
|
65
118
|
const copy = { ...e };
|
|
66
|
-
delete copy[
|
|
119
|
+
delete copy[VIEWER];
|
|
120
|
+
delete copy[TESTER];
|
|
67
121
|
return copy;
|
|
68
122
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attributed testers per run. Justified against D-Many-viewers-are-normal's
|
|
3
|
+
* 40-person demo, NOT against the platform's MaxConcurrentStreams = 256 — those
|
|
4
|
+
* count different things. 256 is per-session CONCURRENT CONNECTIONS, and one tab
|
|
5
|
+
* opens 2-6 under HTTP/1.1, so 64 tabs already exceeds what the platform admits
|
|
6
|
+
* and the platform's cap binds first. Memory does not choose this number: 64
|
|
7
|
+
* entries is about 4 KB.
|
|
8
|
+
*
|
|
9
|
+
* That argument holds ONLY because minting requires a WebSocket the harness
|
|
10
|
+
* opened — see `resolve` versus `attribute`. A door that could mint without a
|
|
11
|
+
* stream would not be bounded by the platform's stream cap at all, and filling
|
|
12
|
+
* this registry is a denial of the creator's live terminal, not merely of an id.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MAX_TESTERS = 64;
|
|
15
|
+
/**
|
|
16
|
+
* Refused keys remembered, so the cap warning prints ONCE per refused session
|
|
17
|
+
* rather than once per batch.
|
|
18
|
+
*
|
|
19
|
+
* This set is what makes print-once bounded. The key is client-supplied, so
|
|
20
|
+
* without a cap here its size is a number a client chooses by cycling
|
|
21
|
+
* sessionStorage. Past this many, further sessions are still refused but no
|
|
22
|
+
* longer remembered — so a pathological client can re-trigger its own warning
|
|
23
|
+
* line. That is the correct failure: noisy, local and self-inflicted, versus
|
|
24
|
+
* unbounded memory on the creator's machine.
|
|
25
|
+
*/
|
|
26
|
+
export declare const MAX_REFUSED_TOMBSTONES = 256;
|
|
27
|
+
/** What the relay learned from one batch's key. */
|
|
28
|
+
export interface Attribution {
|
|
29
|
+
/** The tester id, or undefined when this key is not attributed. */
|
|
30
|
+
readonly tester?: string;
|
|
31
|
+
/**
|
|
32
|
+
* True exactly once per refused session — the batch that should print the cap
|
|
33
|
+
* warning. Every later batch from the same refused key reports false.
|
|
34
|
+
*/
|
|
35
|
+
readonly warnCapReached: boolean;
|
|
36
|
+
}
|
|
37
|
+
export declare class TesterRegistry {
|
|
38
|
+
private readonly maxTesters;
|
|
39
|
+
private readonly maxRefused;
|
|
40
|
+
/** Insertion-ordered, so ids read in arrival order. */
|
|
41
|
+
private readonly byKey;
|
|
42
|
+
/** Refused keys, for print-once. Holds no id — these were never assigned one. */
|
|
43
|
+
private readonly refused;
|
|
44
|
+
private next;
|
|
45
|
+
constructor(maxTesters?: number, maxRefused?: number);
|
|
46
|
+
/**
|
|
47
|
+
* Resolve a client-supplied key to a tester id, assigning one on first sight.
|
|
48
|
+
*
|
|
49
|
+
* The key is validated against the SAME grammar the client mints, with no
|
|
50
|
+
* sanitize-and-keep branch — the readViewerLabel discipline, applied to the
|
|
51
|
+
* field beside the label that never had it. A key that misses the grammar is
|
|
52
|
+
* un-attributed: its events are still ingested, only the tester tag is absent.
|
|
53
|
+
*
|
|
54
|
+
* Entries are NEVER removed for the life of the process. That mirrors
|
|
55
|
+
* viewersSeen in the event store and for its stated reason: the ring buffer
|
|
56
|
+
* holds events that outlive their producer, and dropping a tester's id would
|
|
57
|
+
* make its already-stored events unfilterable — worse than never having
|
|
58
|
+
* labelled them. `--tester t1` must keep answering for the tester who just hit
|
|
59
|
+
* the bug, because that is the moment the creator runs it.
|
|
60
|
+
*/
|
|
61
|
+
attribute(key: unknown): Attribution;
|
|
62
|
+
/**
|
|
63
|
+
* Resolve a key WITHOUT ever assigning one — the read-only door.
|
|
64
|
+
*
|
|
65
|
+
* The annotation POST uses this, and the distinction is load-bearing. That
|
|
66
|
+
* route is UNAUTHENTICATED and reachable by anyone holding the preview's
|
|
67
|
+
* capability URL, so if it could mint, 64 bare `fetch` calls would exhaust the
|
|
68
|
+
* registry before the creator's own phone ever connected. Entries are never
|
|
69
|
+
* evicted, so that state would be permanent for the run — and because the
|
|
70
|
+
* terminal only prints attributed events, the creator's live stream would go
|
|
71
|
+
* blank for every real tester.
|
|
72
|
+
*
|
|
73
|
+
* Minting requires a WebSocket the harness actually opened. An annotation can
|
|
74
|
+
* therefore be attributed to a tester that already exists, and is otherwise
|
|
75
|
+
* un-attributed, which is the correct failure: the creator still gets their
|
|
76
|
+
* bug report, it just carries no id.
|
|
77
|
+
*/
|
|
78
|
+
resolve(key: unknown): string | undefined;
|
|
79
|
+
/** Ids assigned so far, in arrival order. Never includes a refused session. */
|
|
80
|
+
testers(): string[];
|
|
81
|
+
/** How many keys hold an id. Bounded by maxTesters. */
|
|
82
|
+
get size(): number;
|
|
83
|
+
/** Whether this id was ever assigned — for `--tester` miss messages. */
|
|
84
|
+
has(tester: string): boolean;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Is this a well-formed tester id? Used at the query boundary so a malformed
|
|
88
|
+
* `--tester` filters to nothing rather than erroring or matching broadly.
|
|
89
|
+
*/
|
|
90
|
+
export declare function isTesterId(s: unknown): s is string;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// The tester registry: the one place a client-supplied session key becomes a
|
|
2
|
+
// tester id the creator can read.
|
|
3
|
+
//
|
|
4
|
+
// A TESTER is one browser tab on one preview, recognised across refreshes. It is
|
|
5
|
+
// deliberately NOT called a device: a tab is what the key can honestly
|
|
6
|
+
// distinguish, one person with two tabs is two testers, and over-claiming is how
|
|
7
|
+
// a debugging coordinate turns into an identity.
|
|
8
|
+
//
|
|
9
|
+
// The whole structure lives in the `vincentt preview` process and dies with it.
|
|
10
|
+
// It is NEVER persisted: a tester id surviving a restart would be exactly the
|
|
11
|
+
// cross-session correlator this design refuses (B22).
|
|
12
|
+
//
|
|
13
|
+
// Pure and dependency-free, like store.ts beside it, so every rule below is
|
|
14
|
+
// unit-testable without a socket.
|
|
15
|
+
import { CLIENT_SESSION_ID_RE, TESTER_ID_RE } from "@vincentt-xr/harness/events";
|
|
16
|
+
/**
|
|
17
|
+
* Attributed testers per run. Justified against D-Many-viewers-are-normal's
|
|
18
|
+
* 40-person demo, NOT against the platform's MaxConcurrentStreams = 256 — those
|
|
19
|
+
* count different things. 256 is per-session CONCURRENT CONNECTIONS, and one tab
|
|
20
|
+
* opens 2-6 under HTTP/1.1, so 64 tabs already exceeds what the platform admits
|
|
21
|
+
* and the platform's cap binds first. Memory does not choose this number: 64
|
|
22
|
+
* entries is about 4 KB.
|
|
23
|
+
*
|
|
24
|
+
* That argument holds ONLY because minting requires a WebSocket the harness
|
|
25
|
+
* opened — see `resolve` versus `attribute`. A door that could mint without a
|
|
26
|
+
* stream would not be bounded by the platform's stream cap at all, and filling
|
|
27
|
+
* this registry is a denial of the creator's live terminal, not merely of an id.
|
|
28
|
+
*/
|
|
29
|
+
export const MAX_TESTERS = 64;
|
|
30
|
+
/**
|
|
31
|
+
* Refused keys remembered, so the cap warning prints ONCE per refused session
|
|
32
|
+
* rather than once per batch.
|
|
33
|
+
*
|
|
34
|
+
* This set is what makes print-once bounded. The key is client-supplied, so
|
|
35
|
+
* without a cap here its size is a number a client chooses by cycling
|
|
36
|
+
* sessionStorage. Past this many, further sessions are still refused but no
|
|
37
|
+
* longer remembered — so a pathological client can re-trigger its own warning
|
|
38
|
+
* line. That is the correct failure: noisy, local and self-inflicted, versus
|
|
39
|
+
* unbounded memory on the creator's machine.
|
|
40
|
+
*/
|
|
41
|
+
export const MAX_REFUSED_TOMBSTONES = 256;
|
|
42
|
+
const UNATTRIBUTED = { warnCapReached: false };
|
|
43
|
+
export class TesterRegistry {
|
|
44
|
+
maxTesters;
|
|
45
|
+
maxRefused;
|
|
46
|
+
/** Insertion-ordered, so ids read in arrival order. */
|
|
47
|
+
byKey = new Map();
|
|
48
|
+
/** Refused keys, for print-once. Holds no id — these were never assigned one. */
|
|
49
|
+
refused = new Set();
|
|
50
|
+
next = 1;
|
|
51
|
+
constructor(maxTesters = MAX_TESTERS, maxRefused = MAX_REFUSED_TOMBSTONES) {
|
|
52
|
+
this.maxTesters = maxTesters;
|
|
53
|
+
this.maxRefused = maxRefused;
|
|
54
|
+
// TESTER_ID_RE tops out at t999, and `--tester` validates against it. A cap
|
|
55
|
+
// above that would mint ids the filter then refuses, making those testers
|
|
56
|
+
// permanently unselectable — a silent failure that would surface only to
|
|
57
|
+
// whoever raised the cap.
|
|
58
|
+
if (maxTesters > 999) {
|
|
59
|
+
throw new Error(`maxTesters ${maxTesters} exceeds the t999 id grammar`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Resolve a client-supplied key to a tester id, assigning one on first sight.
|
|
64
|
+
*
|
|
65
|
+
* The key is validated against the SAME grammar the client mints, with no
|
|
66
|
+
* sanitize-and-keep branch — the readViewerLabel discipline, applied to the
|
|
67
|
+
* field beside the label that never had it. A key that misses the grammar is
|
|
68
|
+
* un-attributed: its events are still ingested, only the tester tag is absent.
|
|
69
|
+
*
|
|
70
|
+
* Entries are NEVER removed for the life of the process. That mirrors
|
|
71
|
+
* viewersSeen in the event store and for its stated reason: the ring buffer
|
|
72
|
+
* holds events that outlive their producer, and dropping a tester's id would
|
|
73
|
+
* make its already-stored events unfilterable — worse than never having
|
|
74
|
+
* labelled them. `--tester t1` must keep answering for the tester who just hit
|
|
75
|
+
* the bug, because that is the moment the creator runs it.
|
|
76
|
+
*/
|
|
77
|
+
attribute(key) {
|
|
78
|
+
if (typeof key !== "string" || !CLIENT_SESSION_ID_RE.test(key))
|
|
79
|
+
return UNATTRIBUTED;
|
|
80
|
+
const existing = this.byKey.get(key);
|
|
81
|
+
if (existing !== undefined)
|
|
82
|
+
return { tester: existing, warnCapReached: false };
|
|
83
|
+
if (this.byKey.size >= this.maxTesters) {
|
|
84
|
+
// REFUSE. Never recycle.
|
|
85
|
+
//
|
|
86
|
+
// Every natural cap implementation merges or splits silently. `hash(key) %
|
|
87
|
+
// N` is the one a reader reaches for because it is stateless, and it
|
|
88
|
+
// deterministically collides the 65th tester with an existing one.
|
|
89
|
+
// LRU-evict is the same failure mirrored: one tester reappears as two.
|
|
90
|
+
// Refusing is the only branch whose failure is VISIBLE.
|
|
91
|
+
if (this.refused.has(key))
|
|
92
|
+
return UNATTRIBUTED;
|
|
93
|
+
if (this.refused.size < this.maxRefused)
|
|
94
|
+
this.refused.add(key);
|
|
95
|
+
return { warnCapReached: true };
|
|
96
|
+
}
|
|
97
|
+
const tester = `t${this.next++}`;
|
|
98
|
+
this.byKey.set(key, tester);
|
|
99
|
+
return { tester, warnCapReached: false };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Resolve a key WITHOUT ever assigning one — the read-only door.
|
|
103
|
+
*
|
|
104
|
+
* The annotation POST uses this, and the distinction is load-bearing. That
|
|
105
|
+
* route is UNAUTHENTICATED and reachable by anyone holding the preview's
|
|
106
|
+
* capability URL, so if it could mint, 64 bare `fetch` calls would exhaust the
|
|
107
|
+
* registry before the creator's own phone ever connected. Entries are never
|
|
108
|
+
* evicted, so that state would be permanent for the run — and because the
|
|
109
|
+
* terminal only prints attributed events, the creator's live stream would go
|
|
110
|
+
* blank for every real tester.
|
|
111
|
+
*
|
|
112
|
+
* Minting requires a WebSocket the harness actually opened. An annotation can
|
|
113
|
+
* therefore be attributed to a tester that already exists, and is otherwise
|
|
114
|
+
* un-attributed, which is the correct failure: the creator still gets their
|
|
115
|
+
* bug report, it just carries no id.
|
|
116
|
+
*/
|
|
117
|
+
resolve(key) {
|
|
118
|
+
if (typeof key !== "string")
|
|
119
|
+
return undefined;
|
|
120
|
+
return this.byKey.get(key);
|
|
121
|
+
}
|
|
122
|
+
/** Ids assigned so far, in arrival order. Never includes a refused session. */
|
|
123
|
+
testers() {
|
|
124
|
+
return [...this.byKey.values()];
|
|
125
|
+
}
|
|
126
|
+
/** How many keys hold an id. Bounded by maxTesters. */
|
|
127
|
+
get size() {
|
|
128
|
+
return this.byKey.size;
|
|
129
|
+
}
|
|
130
|
+
/** Whether this id was ever assigned — for `--tester` miss messages. */
|
|
131
|
+
has(tester) {
|
|
132
|
+
for (const t of this.byKey.values())
|
|
133
|
+
if (t === tester)
|
|
134
|
+
return true;
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Is this a well-formed tester id? Used at the query boundary so a malformed
|
|
140
|
+
* `--tester` filters to nothing rather than erroring or matching broadly.
|
|
141
|
+
*/
|
|
142
|
+
export function isTesterId(s) {
|
|
143
|
+
return typeof s === "string" && TESTER_ID_RE.test(s);
|
|
144
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function authFailureMessage(label: string, status: number, detail: string): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// A rejected PAT (401 from an authenticated call) is, after the auth cutover, the
|
|
2
|
+
// universal first-run state: every existing `vct_` token was invalidated, so the raw
|
|
3
|
+
// "(401): <body>" is unactionable noise for every user at once. Name the cause and give
|
|
4
|
+
// the one instruction that fixes it. Any other status keeps its existing "(<n>): <body>"
|
|
5
|
+
// shape — a 500/timeout is a different problem and must not be mislabeled a stale token.
|
|
6
|
+
export function authFailureMessage(label, status, detail) {
|
|
7
|
+
if (status === 401) {
|
|
8
|
+
return "Your Vincentt access token is no longer valid — run `vincentt login` to sign in again.";
|
|
9
|
+
}
|
|
10
|
+
return `${label} (${status})${detail ? `: ${detail}` : ""}`;
|
|
11
|
+
}
|
package/dist/shared/config.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export interface MachineConfig {
|
|
|
3
3
|
apiUrl: string;
|
|
4
4
|
/** Personal access token (bearer) for machine auth. */
|
|
5
5
|
pat: string;
|
|
6
|
+
/** Org slug commands act in when no --org flag or VINCENTT_ORG env is given. */
|
|
7
|
+
activeOrg: string;
|
|
6
8
|
}
|
|
7
9
|
export interface ProjectBinding {
|
|
8
10
|
/** The backend Project id this working tree publishes to. */
|
|
@@ -18,7 +20,8 @@ export declare function loadMachineConfig(): Promise<Partial<MachineConfig>>;
|
|
|
18
20
|
/**
|
|
19
21
|
* Merge a patch into ~/.vincentt/config.json (used by `harness login` to persist
|
|
20
22
|
* a freshly minted PAT). Preserves any other fields already present. The file
|
|
21
|
-
* holds a bearer credential, so the dir/file get owner-only perms
|
|
23
|
+
* holds a bearer credential, so the dir/file get owner-only perms (enforced on
|
|
24
|
+
* every write, even when the file pre-existed with looser perms).
|
|
22
25
|
*/
|
|
23
26
|
export declare function writeMachineConfig(patch: Partial<MachineConfig>): Promise<string>;
|
|
24
27
|
/**
|
|
@@ -30,6 +33,23 @@ export declare function clearMachineToken(): Promise<{
|
|
|
30
33
|
hadToken: boolean;
|
|
31
34
|
}>;
|
|
32
35
|
export declare function loadProjectBinding(cwd: string): Promise<ProjectBinding | undefined>;
|
|
36
|
+
export interface LocatedBinding {
|
|
37
|
+
binding: ProjectBinding;
|
|
38
|
+
/** The directory holding `.vincentt/project.json` (cwd, or an ancestor). */
|
|
39
|
+
dir: string;
|
|
40
|
+
/** Absolute path of the binding file, for messages that name it. */
|
|
41
|
+
path: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Locate the binding by PRECEDENCE, never inference: cwd, then each ancestor up
|
|
45
|
+
* to and including the git root. Nothing is guessed from the folder name and
|
|
46
|
+
* there is no "you only have one project" fallback — a wrong-but-plausible
|
|
47
|
+
* binding is silent retargeting, which B12 forbids.
|
|
48
|
+
*
|
|
49
|
+
* The walk stops at the git root so a binding in a parent repo cannot capture an
|
|
50
|
+
* unrelated sibling tree, and at the filesystem root when there is no repo.
|
|
51
|
+
*/
|
|
52
|
+
export declare function locateProjectBinding(cwd: string): Promise<LocatedBinding | undefined>;
|
|
33
53
|
/**
|
|
34
54
|
* Write the per-tree binding and make sure `.vincentt/` is gitignored (so the
|
|
35
55
|
* secret-free-but-tenant-scoped binding never rides a commit or `git archive`).
|
|
@@ -44,9 +64,57 @@ export interface ResolvedConfig {
|
|
|
44
64
|
apiUrl: string;
|
|
45
65
|
pat: string;
|
|
46
66
|
}
|
|
67
|
+
export declare const DEFAULT_API_URL = "http://localhost:5051";
|
|
68
|
+
export interface ResolveConfigOptions {
|
|
69
|
+
/** Honor a binding's apiUrl override (the `--allow-custom-api` flag). */
|
|
70
|
+
allowCustomApi?: boolean;
|
|
71
|
+
/** Where advisory lines go. Defaults to a no-op so library callers stay quiet. */
|
|
72
|
+
warn?: (line: string) => void;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Decide which API host a call goes to, given every source that can name one.
|
|
76
|
+
*
|
|
77
|
+
* The rule is PROVENANCE, not location. A machine config and an env var are set by
|
|
78
|
+
* the person running the command; `.vincentt/project.json` travels with a
|
|
79
|
+
* repository, so anyone who can land a commit can set it — and since the PAT comes
|
|
80
|
+
* from machine-global config, honoring the file's host by default sends the user's
|
|
81
|
+
* bearer token to a host chosen by whoever wrote the repo. So the binding's apiUrl
|
|
82
|
+
* is ignored unless `--allow-custom-api` states intent, INVERTING the precedence
|
|
83
|
+
* shipped in 0.1.x.
|
|
84
|
+
*/
|
|
85
|
+
export interface ApiUrlDecision {
|
|
86
|
+
/** The host the request actually goes to. */
|
|
87
|
+
apiUrl: string;
|
|
88
|
+
/**
|
|
89
|
+
* The host the caller's OWN configuration names, whether or not it won. The
|
|
90
|
+
* advisory compares against this, not against the decision — on the flagged
|
|
91
|
+
* path the decision IS the binding, so reading it back would print the
|
|
92
|
+
* override twice and claim it was the machine config.
|
|
93
|
+
*/
|
|
94
|
+
configuredUrl: string;
|
|
95
|
+
ignoredBindingApiUrl?: string;
|
|
96
|
+
usedBindingApiUrl?: string;
|
|
97
|
+
}
|
|
98
|
+
export declare function decideApiUrl(input: {
|
|
99
|
+
bindingApiUrl?: string;
|
|
100
|
+
envApiUrl?: string;
|
|
101
|
+
machineApiUrl?: string;
|
|
102
|
+
allowCustomApi?: boolean;
|
|
103
|
+
}): ApiUrlDecision;
|
|
104
|
+
/**
|
|
105
|
+
* Resolve the API URL + PAT for a machine call. Throws a clear, actionable error
|
|
106
|
+
* when no PAT is configured. When a binding's apiUrl diverges from the configured
|
|
107
|
+
* one, the safe branch has ALREADY been taken by the time the advisory prints —
|
|
108
|
+
* it reports a decision rather than asking for one.
|
|
109
|
+
*/
|
|
110
|
+
export declare function resolveConfig(cwd: string, opts?: ResolveConfigOptions): Promise<ResolvedConfig>;
|
|
47
111
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
112
|
+
* The Flow L12 transcripts. Ignoring an override is the loud case (both hosts on
|
|
113
|
+
* their own aligned lines, the mechanism named, and where to go look); honoring one
|
|
114
|
+
* on purpose is two quiet lines, because a creator who typed the flag has already
|
|
115
|
+
* stated intent and re-reading the lecture every run is how advisories get
|
|
116
|
+
* filtered out mentally — taking the un-flagged one with them.
|
|
51
117
|
*/
|
|
52
|
-
export declare function
|
|
118
|
+
export declare function apiUrlAdvisory(decided: ApiUrlDecision, ctx: {
|
|
119
|
+
bindingPath: string;
|
|
120
|
+
}): string[];
|