@vincentt-xr/cli 0.1.5 → 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.
Files changed (66) hide show
  1. package/README.md +94 -0
  2. package/dist/cli/args.d.ts +13 -0
  3. package/dist/cli/args.js +41 -0
  4. package/dist/cli/index.js +362 -92
  5. package/dist/lib/backend.d.ts +45 -5
  6. package/dist/lib/backend.js +99 -18
  7. package/dist/lib/diagnostics.d.ts +27 -2
  8. package/dist/lib/diagnostics.js +73 -8
  9. package/dist/lib/initCommand.d.ts +14 -0
  10. package/dist/lib/initCommand.js +75 -0
  11. package/dist/lib/lifecycle.d.ts +73 -15
  12. package/dist/lib/lifecycle.js +111 -114
  13. package/dist/lib/org.d.ts +29 -0
  14. package/dist/lib/org.js +45 -0
  15. package/dist/lib/projects.d.ts +82 -0
  16. package/dist/lib/projects.js +217 -0
  17. package/dist/lib/relayInfo.d.ts +41 -3
  18. package/dist/lib/relayInfo.js +94 -0
  19. package/dist/lib/whoami.d.ts +11 -0
  20. package/dist/lib/whoami.js +38 -0
  21. package/dist/login/authBackend.d.ts +30 -0
  22. package/dist/login/authBackend.js +80 -0
  23. package/dist/login/guard.d.ts +13 -0
  24. package/dist/login/guard.js +17 -0
  25. package/dist/login/loginCommand.d.ts +39 -0
  26. package/dist/login/loginCommand.js +72 -0
  27. package/dist/login/pkce.d.ts +47 -0
  28. package/dist/login/pkce.js +106 -0
  29. package/dist/login/pkceLogin.d.ts +53 -0
  30. package/dist/login/pkceLogin.js +152 -0
  31. package/dist/mcp/server.js +47 -20
  32. package/dist/preview/api.d.ts +84 -0
  33. package/dist/preview/api.js +174 -0
  34. package/dist/preview/command.d.ts +96 -0
  35. package/dist/preview/command.js +276 -0
  36. package/dist/preview/copy.d.ts +209 -0
  37. package/dist/preview/copy.js +444 -0
  38. package/dist/preview/index.d.ts +2 -3
  39. package/dist/preview/index.js +5 -6
  40. package/dist/preview/proxy.d.ts +12 -1
  41. package/dist/preview/proxy.js +17 -7
  42. package/dist/preview/qr.d.ts +30 -0
  43. package/dist/preview/qr.js +89 -0
  44. package/dist/preview/runner.d.ts +155 -30
  45. package/dist/preview/runner.js +219 -68
  46. package/dist/preview/stream.d.ts +52 -0
  47. package/dist/preview/stream.js +171 -0
  48. package/dist/preview/viewers.d.ts +113 -0
  49. package/dist/preview/viewers.js +242 -0
  50. package/dist/relay/server.d.ts +32 -0
  51. package/dist/relay/server.js +80 -12
  52. package/dist/relay/store.d.ts +32 -3
  53. package/dist/relay/store.js +76 -22
  54. package/dist/relay/testers.d.ts +90 -0
  55. package/dist/relay/testers.js +144 -0
  56. package/dist/shared/authError.d.ts +1 -0
  57. package/dist/shared/authError.js +11 -0
  58. package/dist/shared/config.d.ts +73 -5
  59. package/dist/shared/config.js +133 -20
  60. package/package.json +2 -3
  61. package/dist/login/login.d.ts +0 -34
  62. package/dist/login/login.js +0 -148
  63. package/dist/preview/cloudflared.d.ts +0 -13
  64. package/dist/preview/cloudflared.js +0 -46
  65. package/dist/preview/tunnel.d.ts +0 -14
  66. package/dist/preview/tunnel.js +0 -28
@@ -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 unit-testable; the WS
4
- // server in server.ts is a thin wrapper that feeds this and answers queries
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
- sessionsSeen = new Set();
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
- /** Ingest a batch from a client. Tracks the session and advances latestSeq. */
15
- ingest(sessionId, events) {
16
- this.sessionsSeen.add(sessionId);
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 cross-session queries can filter.
19
- this.buf.push(withSession(e, sessionId));
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
- if (q.sessionId && sessionOf(e) !== q.sessionId)
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(stripSession),
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
- sessions: [...this.sessionsSeen],
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 session out-of-band (a symbol key)
53
- // so it survives in the buffer without polluting the wire type. Queries strip
54
- // it before returning.
55
- const SESSION = Symbol("session");
56
- function withSession(e, sessionId) {
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
- [SESSION]: sessionId,
107
+ ...(viewer === undefined ? {} : { [VIEWER]: viewer }),
108
+ ...(tester === undefined ? {} : { [TESTER]: tester }),
59
109
  });
60
110
  }
61
- function sessionOf(e) {
62
- return e[SESSION];
111
+ function viewerOf(e) {
112
+ return e[VIEWER];
113
+ }
114
+ function testerOf(e) {
115
+ return e[TESTER];
63
116
  }
64
- function stripSession(e) {
117
+ function stripTags(e) {
65
118
  const copy = { ...e };
66
- delete copy[SESSION];
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
+ }
@@ -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
- * Resolve the API URL + PAT for a machine call. Precedence: a project binding's
49
- * apiUrl wins for the URL; env vars override the machine-config file; the file is
50
- * the base. Throws a clear, actionable error when no PAT is configured.
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 resolveConfig(cwd: string): Promise<ResolvedConfig>;
118
+ export declare function apiUrlAdvisory(decided: ApiUrlDecision, ctx: {
119
+ bindingPath: string;
120
+ }): string[];