@ahpd/server 0.1.0 → 0.3.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 (170) hide show
  1. package/README.md +88 -81
  2. package/dist/config.d.ts +72 -0
  3. package/dist/config.js +75 -0
  4. package/dist/daemon.d.ts +31 -0
  5. package/dist/daemon.js +162 -0
  6. package/dist/main.d.ts +2 -0
  7. package/dist/main.js +335 -0
  8. package/dist/pty.d.ts +12 -0
  9. package/dist/pty.js +56 -0
  10. package/package.json +15 -25
  11. package/dist/automations.d.ts +0 -4
  12. package/dist/automations.d.ts.map +0 -1
  13. package/dist/automations.js +0 -191
  14. package/dist/automations.js.map +0 -1
  15. package/dist/catalog.d.ts +0 -48
  16. package/dist/catalog.d.ts.map +0 -1
  17. package/dist/catalog.js +0 -51
  18. package/dist/catalog.js.map +0 -1
  19. package/dist/changes.d.ts +0 -4
  20. package/dist/changes.d.ts.map +0 -1
  21. package/dist/changes.js +0 -601
  22. package/dist/changes.js.map +0 -1
  23. package/dist/cron.d.ts +0 -43
  24. package/dist/cron.d.ts.map +0 -1
  25. package/dist/cron.js +0 -211
  26. package/dist/cron.js.map +0 -1
  27. package/dist/git.d.ts +0 -16
  28. package/dist/git.d.ts.map +0 -1
  29. package/dist/git.js +0 -126
  30. package/dist/git.js.map +0 -1
  31. package/dist/host.d.ts +0 -24
  32. package/dist/host.d.ts.map +0 -1
  33. package/dist/host.js +0 -5850
  34. package/dist/host.js.map +0 -1
  35. package/dist/index.d.ts +0 -36
  36. package/dist/index.d.ts.map +0 -1
  37. package/dist/index.js +0 -34
  38. package/dist/index.js.map +0 -1
  39. package/dist/listen.d.ts +0 -3
  40. package/dist/listen.d.ts.map +0 -1
  41. package/dist/listen.js +0 -183
  42. package/dist/listen.js.map +0 -1
  43. package/dist/paging.d.ts +0 -38
  44. package/dist/paging.d.ts.map +0 -1
  45. package/dist/paging.js +0 -53
  46. package/dist/paging.js.map +0 -1
  47. package/dist/paths.d.ts +0 -15
  48. package/dist/paths.d.ts.map +0 -1
  49. package/dist/paths.js +0 -19
  50. package/dist/paths.js.map +0 -1
  51. package/dist/resources.d.ts +0 -122
  52. package/dist/resources.d.ts.map +0 -1
  53. package/dist/resources.js +0 -494
  54. package/dist/resources.js.map +0 -1
  55. package/dist/rpc.d.ts +0 -63
  56. package/dist/rpc.d.ts.map +0 -1
  57. package/dist/rpc.js +0 -208
  58. package/dist/rpc.js.map +0 -1
  59. package/dist/scheduled.d.ts +0 -48
  60. package/dist/scheduled.d.ts.map +0 -1
  61. package/dist/scheduled.js +0 -325
  62. package/dist/scheduled.js.map +0 -1
  63. package/dist/terminals.d.ts +0 -29
  64. package/dist/terminals.d.ts.map +0 -1
  65. package/dist/terminals.js +0 -346
  66. package/dist/terminals.js.map +0 -1
  67. package/dist/tools.d.ts +0 -18
  68. package/dist/tools.d.ts.map +0 -1
  69. package/dist/tools.js +0 -65
  70. package/dist/tools.js.map +0 -1
  71. package/dist/types/agent.d.ts +0 -245
  72. package/dist/types/agent.d.ts.map +0 -1
  73. package/dist/types/agent.js +0 -3
  74. package/dist/types/agent.js.map +0 -1
  75. package/dist/types/automations.d.ts +0 -164
  76. package/dist/types/automations.d.ts.map +0 -1
  77. package/dist/types/automations.js +0 -3
  78. package/dist/types/automations.js.map +0 -1
  79. package/dist/types/catalog.d.ts +0 -30
  80. package/dist/types/catalog.d.ts.map +0 -1
  81. package/dist/types/catalog.js +0 -3
  82. package/dist/types/catalog.js.map +0 -1
  83. package/dist/types/changes.d.ts +0 -272
  84. package/dist/types/changes.d.ts.map +0 -1
  85. package/dist/types/changes.js +0 -3
  86. package/dist/types/changes.js.map +0 -1
  87. package/dist/types/common.d.ts +0 -9
  88. package/dist/types/common.d.ts.map +0 -1
  89. package/dist/types/common.js +0 -3
  90. package/dist/types/common.js.map +0 -1
  91. package/dist/types/host.d.ts +0 -372
  92. package/dist/types/host.d.ts.map +0 -1
  93. package/dist/types/host.js +0 -3
  94. package/dist/types/host.js.map +0 -1
  95. package/dist/types/index.d.ts +0 -20
  96. package/dist/types/index.d.ts.map +0 -1
  97. package/dist/types/index.js +0 -8
  98. package/dist/types/index.js.map +0 -1
  99. package/dist/types/listen.d.ts +0 -48
  100. package/dist/types/listen.d.ts.map +0 -1
  101. package/dist/types/listen.js +0 -3
  102. package/dist/types/listen.js.map +0 -1
  103. package/dist/types/paging.d.ts +0 -10
  104. package/dist/types/paging.d.ts.map +0 -1
  105. package/dist/types/paging.js +0 -3
  106. package/dist/types/paging.js.map +0 -1
  107. package/dist/types/probe.d.ts +0 -46
  108. package/dist/types/probe.d.ts.map +0 -1
  109. package/dist/types/probe.js +0 -3
  110. package/dist/types/probe.js.map +0 -1
  111. package/dist/types/resources.d.ts +0 -83
  112. package/dist/types/resources.d.ts.map +0 -1
  113. package/dist/types/resources.js +0 -3
  114. package/dist/types/resources.js.map +0 -1
  115. package/dist/types/rpc.d.ts +0 -65
  116. package/dist/types/rpc.d.ts.map +0 -1
  117. package/dist/types/rpc.js +0 -3
  118. package/dist/types/rpc.js.map +0 -1
  119. package/dist/types/session.d.ts +0 -395
  120. package/dist/types/session.d.ts.map +0 -1
  121. package/dist/types/session.js +0 -3
  122. package/dist/types/session.js.map +0 -1
  123. package/dist/types/terminals.d.ts +0 -131
  124. package/dist/types/terminals.d.ts.map +0 -1
  125. package/dist/types/terminals.js +0 -3
  126. package/dist/types/terminals.js.map +0 -1
  127. package/dist/types/wire.d.ts +0 -62
  128. package/dist/types/wire.d.ts.map +0 -1
  129. package/dist/types/wire.js +0 -13
  130. package/dist/types/wire.js.map +0 -1
  131. package/dist/types/worktrees.d.ts +0 -86
  132. package/dist/types/worktrees.d.ts.map +0 -1
  133. package/dist/types/worktrees.js +0 -3
  134. package/dist/types/worktrees.js.map +0 -1
  135. package/dist/worktrees.d.ts +0 -27
  136. package/dist/worktrees.d.ts.map +0 -1
  137. package/dist/worktrees.js +0 -152
  138. package/dist/worktrees.js.map +0 -1
  139. package/src/automations.ts +0 -204
  140. package/src/catalog.ts +0 -53
  141. package/src/changes.ts +0 -629
  142. package/src/cron.ts +0 -251
  143. package/src/git.ts +0 -132
  144. package/src/host.ts +0 -5947
  145. package/src/index.ts +0 -40
  146. package/src/listen.ts +0 -240
  147. package/src/paging.ts +0 -56
  148. package/src/paths.ts +0 -19
  149. package/src/resources.ts +0 -513
  150. package/src/rpc.ts +0 -222
  151. package/src/scheduled.ts +0 -369
  152. package/src/terminals.ts +0 -353
  153. package/src/tools.ts +0 -64
  154. package/src/types/agent.ts +0 -255
  155. package/src/types/automations.ts +0 -164
  156. package/src/types/catalog.ts +0 -26
  157. package/src/types/changes.ts +0 -266
  158. package/src/types/common.ts +0 -9
  159. package/src/types/host.ts +0 -386
  160. package/src/types/index.ts +0 -27
  161. package/src/types/listen.ts +0 -53
  162. package/src/types/paging.ts +0 -11
  163. package/src/types/probe.ts +0 -42
  164. package/src/types/resources.ts +0 -107
  165. package/src/types/rpc.ts +0 -68
  166. package/src/types/session.ts +0 -415
  167. package/src/types/terminals.ts +0 -133
  168. package/src/types/wire.ts +0 -69
  169. package/src/types/worktrees.ts +0 -91
  170. package/src/worktrees.ts +0 -162
package/src/cron.ts DELETED
@@ -1,251 +0,0 @@
1
- /** Five-field AHP cron: what an expression means, and when it next comes round. */
2
-
3
- /**
4
- * A parsed expression, as the sets each field allows.
5
- *
6
- * Sets rather than the source text, because the question asked of this a
7
- * thousand times a day is "does this minute match" and not "what did somebody
8
- * type" - and because parsing once is what makes a bad expression a refusal at
9
- * the moment it is written rather than a schedule that silently never fires.
10
- */
11
- export interface Cron {
12
- minutes: ReadonlySet<number>;
13
- hours: ReadonlySet<number>;
14
- doms: ReadonlySet<number>;
15
- months: ReadonlySet<number>;
16
- dows: ReadonlySet<number>;
17
- /**
18
- * Whether the day fields were narrowed, which decides how they combine.
19
- *
20
- * Unix cron's one genuine oddity: when both day-of-month and day-of-week are
21
- * restricted, an occurrence matches when *either* does - `0 0 1 * 1` is the
22
- * first of the month **and** every Monday. When one is `*` the other simply
23
- * applies. Getting this backwards turns a weekly job into a yearly one.
24
- */
25
- domRestricted: boolean;
26
- dowRestricted: boolean;
27
- }
28
-
29
- const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
30
- const DAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
31
-
32
- /** One field's bounds, and the names it also accepts. */
33
- interface Field {
34
- name: string;
35
- min: number;
36
- max: number;
37
- names?: string[];
38
- }
39
-
40
- const FIELDS: Field[] = [
41
- { name: 'minute', min: 0, max: 59 },
42
- { name: 'hour', min: 0, max: 23 },
43
- { name: 'day of month', min: 1, max: 31 },
44
- { name: 'month', min: 1, max: 12, names: MONTHS },
45
- // 7 as well as 0, both meaning Sunday, which is what every crontab accepts.
46
- { name: 'day of week', min: 0, max: 7, names: DAYS },
47
- ];
48
-
49
- /** A single value, by number or by name. */
50
- function valueOf(text: string, field: Field): number {
51
- const named = field.names?.indexOf(text.toLowerCase());
52
- if (named !== undefined && named >= 0) return named + (field.names === MONTHS ? 1 : 0);
53
- if (!/^\d+$/.test(text)) throw new Error(`${text} is not a ${field.name}`);
54
- const value = Number(text);
55
- if (value < field.min || value > field.max) {
56
- throw new Error(`${value} is out of range for ${field.name} (${field.min}-${field.max})`);
57
- }
58
- return value;
59
- }
60
-
61
- /** One field: a comma-separated list of values, ranges and steps. */
62
- function parseField(text: string, field: Field): Set<number> {
63
- const out = new Set<number>();
64
- for (const term of text.split(',')) {
65
- if (term === '') throw new Error(`Empty ${field.name}`);
66
- const [range, step] = term.split('/') as [string, string | undefined];
67
- if (term.split('/').length > 2) throw new Error(`${term} has more than one step`);
68
- let by = 1;
69
- if (step !== undefined) {
70
- if (!/^\d+$/.test(step) || Number(step) === 0) {
71
- throw new Error(`${step} is not a step for ${field.name}; a step must be a positive integer`);
72
- }
73
- by = Number(step);
74
- }
75
- let from: number;
76
- let to: number;
77
- if (range === '*') {
78
- from = field.min;
79
- to = field.max;
80
- }
81
- else if (range.includes('-')) {
82
- const [a, b] = range.split('-') as [string, string];
83
- if (range.split('-').length > 2) throw new Error(`${range} is not a range`);
84
- from = valueOf(a, field);
85
- to = valueOf(b, field);
86
- if (to < from) throw new Error(`${range} runs backwards`);
87
- }
88
- else {
89
- from = valueOf(range, field);
90
- // A step on a single value means from there to the end of the field,
91
- // which is what crontab does with `5/10`.
92
- to = step === undefined ? from : field.max;
93
- }
94
- for (let at = from; at <= to; at += by) out.add(at);
95
- }
96
- return out;
97
- }
98
-
99
- /**
100
- * Read an expression, or say what is wrong with it.
101
- *
102
- * Throws rather than returning undefined: an unparseable schedule is a thing
103
- * somebody typed and must be told about, and the one place it is read is the
104
- * moment they write it.
105
- */
106
- export function parseCron(expression: string): Cron {
107
- const fields = expression.trim().split(/\s+/);
108
- if (fields.length !== 5) {
109
- throw new Error(`A schedule has five fields; ${expression.trim() === '' ? 'this one has none' : `this one has ${fields.length}`}`);
110
- }
111
- const [minute, hour, dom, month, dow] = fields as [string, string, string, string, string];
112
- const dows = parseField(dow, FIELDS[4] as Field);
113
- // Both spellings of Sunday collapse to one, so a weekday test is a lookup.
114
- if (dows.has(7)) { dows.delete(7); dows.add(0); }
115
- return {
116
- minutes: parseField(minute, FIELDS[0] as Field),
117
- hours: parseField(hour, FIELDS[1] as Field),
118
- doms: parseField(dom, FIELDS[2] as Field),
119
- months: parseField(month, FIELDS[3] as Field),
120
- dows,
121
- domRestricted: dom !== '*',
122
- dowRestricted: dow !== '*',
123
- };
124
- }
125
-
126
- // ------------------------------------------------------------- the time zone
127
-
128
- /** A wall-clock reading: what a clock on that wall says. */
129
- interface Wall {
130
- year: number;
131
- month: number;
132
- day: number;
133
- hour: number;
134
- minute: number;
135
- }
136
-
137
- const readers = new Map<string, Intl.DateTimeFormat>();
138
-
139
- /** Cached, because this is called once per candidate day and building one is not free. */
140
- function readerFor(timeZone: string): Intl.DateTimeFormat {
141
- let held = readers.get(timeZone);
142
- if (!held) {
143
- held = new Intl.DateTimeFormat('en-US', {
144
- timeZone,
145
- hourCycle: 'h23',
146
- year: 'numeric',
147
- month: '2-digit',
148
- day: '2-digit',
149
- hour: '2-digit',
150
- minute: '2-digit',
151
- });
152
- readers.set(timeZone, held);
153
- }
154
- return held;
155
- }
156
-
157
- /** What the clock in that zone said at that instant. */
158
- function wallAt(instant: number, timeZone: string): Wall {
159
- const parts = readerFor(timeZone).formatToParts(new Date(instant));
160
- const found: Record<string, number> = {};
161
- for (const part of parts) {
162
- if (part.type !== 'literal') found[part.type] = Number(part.value);
163
- }
164
- return {
165
- year: found.year as number,
166
- month: found.month as number,
167
- day: found.day as number,
168
- hour: found.hour as number,
169
- minute: found.minute as number,
170
- };
171
- }
172
-
173
- /** That zone's offset from UTC, in minutes, at that instant. */
174
- function offsetAt(instant: number, timeZone: string): number {
175
- const wall = wallAt(instant, timeZone);
176
- const asUtc = Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour, wall.minute);
177
- // To the minute: the reading has no seconds, so the instant must not either.
178
- return (asUtc - Math.floor(instant / 60000) * 60000) / 60000;
179
- }
180
-
181
- /**
182
- * The instant at which that zone's clock reads that.
183
- *
184
- * Two passes, because the offset depends on the answer: the first guess uses
185
- * the offset in force at roughly the right time and the second uses the one in
186
- * force at the instant the first produced, which is what gets the hour after a
187
- * DST change right. Returns undefined for a wall time that does not exist -
188
- * the hour a spring-forward skips - because a schedule pointing into the gap
189
- * has no occurrence that day rather than an occurrence at some other time.
190
- */
191
- function instantOf(wall: Wall, timeZone: string): number | undefined {
192
- const guess = Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour, wall.minute);
193
- let instant = guess - offsetAt(guess, timeZone) * 60000;
194
- const again = guess - offsetAt(instant, timeZone) * 60000;
195
- if (again !== instant) instant = again;
196
- const back = wallAt(instant, timeZone);
197
- if (back.hour !== wall.hour || back.minute !== wall.minute || back.day !== wall.day) return undefined;
198
- return instant;
199
- }
200
-
201
- // ------------------------------------------------------------- the next time
202
-
203
- /** How far ahead to look before calling an expression unreachable. `0 0 30 2 *` is. */
204
- const HORIZON_DAYS = 1500;
205
-
206
- /** Whether that calendar day is one this expression fires on. */
207
- function dayMatches(cron: Cron, year: number, month: number, day: number): boolean {
208
- if (!cron.months.has(month)) return false;
209
- const dow = new Date(Date.UTC(year, month - 1, day)).getUTCDay();
210
- const byDom = cron.doms.has(day);
211
- const byDow = cron.dows.has(dow);
212
- if (cron.domRestricted && cron.dowRestricted) return byDom || byDow;
213
- if (cron.domRestricted) return byDom;
214
- if (cron.dowRestricted) return byDow;
215
- return true;
216
- }
217
-
218
- /**
219
- * The first occurrence strictly after that instant, or undefined.
220
- *
221
- * Walked a day at a time rather than a minute at a time: a minute-by-minute
222
- * search over the horizon is half a million time-zone conversions to answer a
223
- * question about February.
224
- */
225
- export function nextOccurrence(cron: Cron, after: Date, timeZone: string): Date | undefined {
226
- const from = after.getTime();
227
- const start = wallAt(from, timeZone);
228
- const minutes = [...cron.minutes].sort((a, b) => a - b);
229
- const hours = [...cron.hours].sort((a, b) => a - b);
230
-
231
- for (let ahead = 0; ahead <= HORIZON_DAYS; ahead++) {
232
- // Calendar arithmetic in UTC on the *wall* date, which is date arithmetic
233
- // and not instant arithmetic - adding 24 hours to an instant lands on the
234
- // same date twice a year.
235
- const at = new Date(Date.UTC(start.year, start.month - 1, start.day + ahead));
236
- const year = at.getUTCFullYear();
237
- const month = at.getUTCMonth() + 1;
238
- const day = at.getUTCDate();
239
- if (!dayMatches(cron, year, month, day)) continue;
240
- for (const hour of hours) {
241
- for (const minute of minutes) {
242
- const instant = instantOf({ year, month, day, hour, minute }, timeZone);
243
- // Undefined is the spring-forward gap; earlier than `after` is the
244
- // hour a fall-back repeats, where the first pass is already behind us.
245
- if (instant === undefined || instant <= from) continue;
246
- return new Date(instant);
247
- }
248
- }
249
- }
250
- return undefined;
251
- }
package/src/git.ts DELETED
@@ -1,132 +0,0 @@
1
- /** What git can say about a directory the host serves. */
2
-
3
- import { execFile } from 'node:child_process';
4
- import type { DirectoryFacts } from './types/host.js';
5
-
6
- /**
7
- * The branch each served directory is on, as a host's `DirectoryFacts`.
8
- *
9
- * Deliberately not part of `createHost`: it spawns `git`, which is a binary
10
- * rather than a runtime API and may not be installed at all. The daemon wires
11
- * it in, a library caller opts in, and a host without it still names its
12
- * projects - so nothing here is load-bearing for the protocol.
13
- *
14
- * ```ts
15
- * createHost({ path, agents: [claude({ paths })], directories: gitBranches() });
16
- * ```
17
- */
18
- export function gitBranches(): DirectoryFacts {
19
- /*
20
- * The branch, per *directory* rather than per session.
21
- *
22
- * A host serving one repository with ninety-eight sessions in it would
23
- * otherwise ask git ninety-eight times for one answer.
24
- */
25
- const facts = new Map<string, Record<string, unknown>>();
26
-
27
- /** Run git in a directory and answer what it said, or nothing at all. */
28
- const git = (dir: string, args: string[]): Promise<string | undefined> =>
29
- new Promise((answer) => {
30
- execFile('git', ['-C', dir, ...args], { timeout: 2000 }, (error, out) => {
31
- answer(error ? undefined : out.toString().trim());
32
- });
33
- });
34
-
35
- /**
36
- * The owner and repository of a GitHub remote, if the remote is one.
37
- *
38
- * Both URL forms git writes: `git@github.com:owner/repo.git` and
39
- * `https://github.com/owner/repo`. Anything else is a remote this says
40
- * nothing about rather than one it guesses at.
41
- */
42
- const github = (url: string | undefined): { owner: string; repo: string } | undefined => {
43
- const found = /github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/.exec(url ?? '');
44
- return found ? { owner: found[1] as string, repo: found[2] as string } : undefined;
45
- };
46
-
47
- /**
48
- * Ask git, and answer whether what it said differs from what was held.
49
- *
50
- * A directory that is not a repository, or one whose HEAD is detached, has
51
- * no branch and is remembered as having none: `rev-parse --abbrev-ref` says
52
- * `HEAD` for a detached head, and reporting that would draw a branch called
53
- * HEAD.
54
- */
55
- const refresh = async (dir: string): Promise<boolean> => {
56
- const said = await git(dir, ['rev-parse', '--abbrev-ref', 'HEAD']);
57
- const branchName = said === undefined || said === '' || said === 'HEAD' ? undefined : said;
58
- if (branchName === undefined) {
59
- const had = facts.has(dir);
60
- facts.delete(dir);
61
- return had;
62
- }
63
- /*
64
- * The rest in two more calls, both cheap and both answering a question a
65
- * client draws something from.
66
- *
67
- * `status -sb --porcelain` names the upstream and the ahead/behind counts
68
- * on its first line and one file per line after it, so the count of
69
- * uncommitted changes comes out of the same call. `remote get-url` is the
70
- * only way to know whether this is a GitHub repository, which is what
71
- * turns a client's pull-request affordances on.
72
- */
73
- const [status, origin] = await Promise.all([
74
- git(dir, ['status', '-sb', '--porcelain']),
75
- git(dir, ['remote', 'get-url', 'origin']),
76
- ]);
77
- const lines = (status ?? '').split('\n');
78
- const head = lines[0] ?? '';
79
- const upstream = /^## [^.]*\.\.\.(\S+)/.exec(head)?.[1];
80
- const ahead = Number(/\[.*?ahead (\d+)/.exec(head)?.[1] ?? 0);
81
- const behind = Number(/\[.*?behind (\d+)/.exec(head)?.[1] ?? 0);
82
- const owner = github(origin);
83
- const now: Record<string, unknown> = {
84
- branchName,
85
- /*
86
- * The drift, and only where there is something to drift from.
87
- *
88
- * A branch with no upstream is not zero ahead and zero behind, it is
89
- * neither - and a client draws these as arrows beside the branch, so a
90
- * pair of zeroes is two arrows saying nothing. The reference host omits
91
- * them the same way.
92
- */
93
- ...(upstream === undefined ? {} : {
94
- upstreamBranchName: upstream,
95
- incomingChanges: behind,
96
- outgoingChanges: ahead,
97
- }),
98
- uncommittedChanges: lines.slice(1).filter((one) => one.trim() !== '').length,
99
- hasGitHubRemote: owner !== undefined,
100
- ...(owner ? { githubOwner: owner.owner, githubRepo: owner.repo } : {}),
101
- };
102
- const before = facts.get(dir);
103
- if (before !== undefined && JSON.stringify(before) === JSON.stringify(now)) return false;
104
- facts.set(dir, now);
105
- return true;
106
- };
107
-
108
- return {
109
- /*
110
- * `git` is the well-known key, and the field names are the reference host's.
111
- *
112
- * Not the protocol's: it declares no `_meta` keys at all. The names come
113
- * from a capture of the other implementation - `branchName`,
114
- * `upstreamBranchName`, `incomingChanges`, `outgoingChanges`,
115
- * `uncommittedChanges`, `hasGitHubRemote`, `githubOwner`, `githubRepo` -
116
- * because a client reads one spelling, and this host used to write `branch`
117
- * where that client looks for `branchName` and so said nothing at all.
118
- *
119
- * Written whole, and correct only because this is the only producer of
120
- * `_meta` here.
121
- *
122
- * Correct only because this is the only producer of `_meta` here:
123
- * `session/metaChanged` replaces the map entirely, so a host with two of
124
- * them would have to merge before dispatching rather than after.
125
- */
126
- meta: (dir) => {
127
- const held = facts.get(dir);
128
- return held === undefined ? undefined : { git: { ...held } };
129
- },
130
- refresh,
131
- };
132
- }