@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.
- package/README.md +88 -81
- package/dist/config.d.ts +72 -0
- package/dist/config.js +75 -0
- package/dist/daemon.d.ts +31 -0
- package/dist/daemon.js +162 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +335 -0
- package/dist/pty.d.ts +12 -0
- package/dist/pty.js +56 -0
- package/package.json +15 -25
- package/dist/automations.d.ts +0 -4
- package/dist/automations.d.ts.map +0 -1
- package/dist/automations.js +0 -191
- package/dist/automations.js.map +0 -1
- package/dist/catalog.d.ts +0 -48
- package/dist/catalog.d.ts.map +0 -1
- package/dist/catalog.js +0 -51
- package/dist/catalog.js.map +0 -1
- package/dist/changes.d.ts +0 -4
- package/dist/changes.d.ts.map +0 -1
- package/dist/changes.js +0 -601
- package/dist/changes.js.map +0 -1
- package/dist/cron.d.ts +0 -43
- package/dist/cron.d.ts.map +0 -1
- package/dist/cron.js +0 -211
- package/dist/cron.js.map +0 -1
- package/dist/git.d.ts +0 -16
- package/dist/git.d.ts.map +0 -1
- package/dist/git.js +0 -126
- package/dist/git.js.map +0 -1
- package/dist/host.d.ts +0 -24
- package/dist/host.d.ts.map +0 -1
- package/dist/host.js +0 -5850
- package/dist/host.js.map +0 -1
- package/dist/index.d.ts +0 -36
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -34
- package/dist/index.js.map +0 -1
- package/dist/listen.d.ts +0 -3
- package/dist/listen.d.ts.map +0 -1
- package/dist/listen.js +0 -183
- package/dist/listen.js.map +0 -1
- package/dist/paging.d.ts +0 -38
- package/dist/paging.d.ts.map +0 -1
- package/dist/paging.js +0 -53
- package/dist/paging.js.map +0 -1
- package/dist/paths.d.ts +0 -15
- package/dist/paths.d.ts.map +0 -1
- package/dist/paths.js +0 -19
- package/dist/paths.js.map +0 -1
- package/dist/resources.d.ts +0 -122
- package/dist/resources.d.ts.map +0 -1
- package/dist/resources.js +0 -494
- package/dist/resources.js.map +0 -1
- package/dist/rpc.d.ts +0 -63
- package/dist/rpc.d.ts.map +0 -1
- package/dist/rpc.js +0 -208
- package/dist/rpc.js.map +0 -1
- package/dist/scheduled.d.ts +0 -48
- package/dist/scheduled.d.ts.map +0 -1
- package/dist/scheduled.js +0 -325
- package/dist/scheduled.js.map +0 -1
- package/dist/terminals.d.ts +0 -29
- package/dist/terminals.d.ts.map +0 -1
- package/dist/terminals.js +0 -346
- package/dist/terminals.js.map +0 -1
- package/dist/tools.d.ts +0 -18
- package/dist/tools.d.ts.map +0 -1
- package/dist/tools.js +0 -65
- package/dist/tools.js.map +0 -1
- package/dist/types/agent.d.ts +0 -245
- package/dist/types/agent.d.ts.map +0 -1
- package/dist/types/agent.js +0 -3
- package/dist/types/agent.js.map +0 -1
- package/dist/types/automations.d.ts +0 -164
- package/dist/types/automations.d.ts.map +0 -1
- package/dist/types/automations.js +0 -3
- package/dist/types/automations.js.map +0 -1
- package/dist/types/catalog.d.ts +0 -30
- package/dist/types/catalog.d.ts.map +0 -1
- package/dist/types/catalog.js +0 -3
- package/dist/types/catalog.js.map +0 -1
- package/dist/types/changes.d.ts +0 -272
- package/dist/types/changes.d.ts.map +0 -1
- package/dist/types/changes.js +0 -3
- package/dist/types/changes.js.map +0 -1
- package/dist/types/common.d.ts +0 -9
- package/dist/types/common.d.ts.map +0 -1
- package/dist/types/common.js +0 -3
- package/dist/types/common.js.map +0 -1
- package/dist/types/host.d.ts +0 -372
- package/dist/types/host.d.ts.map +0 -1
- package/dist/types/host.js +0 -3
- package/dist/types/host.js.map +0 -1
- package/dist/types/index.d.ts +0 -20
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -8
- package/dist/types/index.js.map +0 -1
- package/dist/types/listen.d.ts +0 -48
- package/dist/types/listen.d.ts.map +0 -1
- package/dist/types/listen.js +0 -3
- package/dist/types/listen.js.map +0 -1
- package/dist/types/paging.d.ts +0 -10
- package/dist/types/paging.d.ts.map +0 -1
- package/dist/types/paging.js +0 -3
- package/dist/types/paging.js.map +0 -1
- package/dist/types/probe.d.ts +0 -46
- package/dist/types/probe.d.ts.map +0 -1
- package/dist/types/probe.js +0 -3
- package/dist/types/probe.js.map +0 -1
- package/dist/types/resources.d.ts +0 -83
- package/dist/types/resources.d.ts.map +0 -1
- package/dist/types/resources.js +0 -3
- package/dist/types/resources.js.map +0 -1
- package/dist/types/rpc.d.ts +0 -65
- package/dist/types/rpc.d.ts.map +0 -1
- package/dist/types/rpc.js +0 -3
- package/dist/types/rpc.js.map +0 -1
- package/dist/types/session.d.ts +0 -395
- package/dist/types/session.d.ts.map +0 -1
- package/dist/types/session.js +0 -3
- package/dist/types/session.js.map +0 -1
- package/dist/types/terminals.d.ts +0 -131
- package/dist/types/terminals.d.ts.map +0 -1
- package/dist/types/terminals.js +0 -3
- package/dist/types/terminals.js.map +0 -1
- package/dist/types/wire.d.ts +0 -62
- package/dist/types/wire.d.ts.map +0 -1
- package/dist/types/wire.js +0 -13
- package/dist/types/wire.js.map +0 -1
- package/dist/types/worktrees.d.ts +0 -86
- package/dist/types/worktrees.d.ts.map +0 -1
- package/dist/types/worktrees.js +0 -3
- package/dist/types/worktrees.js.map +0 -1
- package/dist/worktrees.d.ts +0 -27
- package/dist/worktrees.d.ts.map +0 -1
- package/dist/worktrees.js +0 -152
- package/dist/worktrees.js.map +0 -1
- package/src/automations.ts +0 -204
- package/src/catalog.ts +0 -53
- package/src/changes.ts +0 -629
- package/src/cron.ts +0 -251
- package/src/git.ts +0 -132
- package/src/host.ts +0 -5947
- package/src/index.ts +0 -40
- package/src/listen.ts +0 -240
- package/src/paging.ts +0 -56
- package/src/paths.ts +0 -19
- package/src/resources.ts +0 -513
- package/src/rpc.ts +0 -222
- package/src/scheduled.ts +0 -369
- package/src/terminals.ts +0 -353
- package/src/tools.ts +0 -64
- package/src/types/agent.ts +0 -255
- package/src/types/automations.ts +0 -164
- package/src/types/catalog.ts +0 -26
- package/src/types/changes.ts +0 -266
- package/src/types/common.ts +0 -9
- package/src/types/host.ts +0 -386
- package/src/types/index.ts +0 -27
- package/src/types/listen.ts +0 -53
- package/src/types/paging.ts +0 -11
- package/src/types/probe.ts +0 -42
- package/src/types/resources.ts +0 -107
- package/src/types/rpc.ts +0 -68
- package/src/types/session.ts +0 -415
- package/src/types/terminals.ts +0 -133
- package/src/types/wire.ts +0 -69
- package/src/types/worktrees.ts +0 -91
- 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
|
-
}
|