@primitivedotdev/map 0.0.0-stage → 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/CONFORMANCE.md +116 -0
- package/LICENSE +21 -0
- package/README.md +47 -2
- package/SKILL.md +89 -0
- package/THIRD-PARTY-NOTICES +25 -0
- package/dist/map.mjs +3889 -0
- package/package.json +43 -4
package/dist/map.mjs
ADDED
|
@@ -0,0 +1,3889 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../core/src/brand.ts
|
|
4
|
+
var LEGACY_TAG = /^(\s*)\[gud /i;
|
|
5
|
+
function canonicalSubject(subject) {
|
|
6
|
+
return subject.replace(LEGACY_TAG, "$1[map ");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// ../core/src/subjects.ts
|
|
10
|
+
var WITH_ID = /* @__PURE__ */ new Set([
|
|
11
|
+
"commit",
|
|
12
|
+
"received",
|
|
13
|
+
"review",
|
|
14
|
+
"verdict",
|
|
15
|
+
"landed",
|
|
16
|
+
"conflict",
|
|
17
|
+
"objected",
|
|
18
|
+
"rejected",
|
|
19
|
+
"help",
|
|
20
|
+
"handle",
|
|
21
|
+
"repo",
|
|
22
|
+
"vetoed",
|
|
23
|
+
"owner",
|
|
24
|
+
"too-late",
|
|
25
|
+
"snapshot",
|
|
26
|
+
"check",
|
|
27
|
+
"check-failed",
|
|
28
|
+
"confirm",
|
|
29
|
+
"confirmed",
|
|
30
|
+
"created",
|
|
31
|
+
"event",
|
|
32
|
+
"tick",
|
|
33
|
+
"pending",
|
|
34
|
+
"stuck",
|
|
35
|
+
"consent",
|
|
36
|
+
"escalated"
|
|
37
|
+
]);
|
|
38
|
+
var NO_ID = /* @__PURE__ */ new Set(["config", "not-allowed", "settings", "log", "diff", "status", "create-failed"]);
|
|
39
|
+
var WITH_FIELDS = /* @__PURE__ */ new Set(["commit", "review", "handle", "repo", "check", "tick", "pending", "consent"]);
|
|
40
|
+
var SUBJECT = /^\[(?:map|gud) ([a-z][a-z-]*)(?: ([^\s\]]+))?(?: ([^\s\]]+))?\](?: (.*))?$/;
|
|
41
|
+
function parseSubject(subject) {
|
|
42
|
+
const m = SUBJECT.exec(subject.trim());
|
|
43
|
+
if (!m) return null;
|
|
44
|
+
const kind = m[1];
|
|
45
|
+
const id = m[2];
|
|
46
|
+
const extra = m[3];
|
|
47
|
+
const rest = (m[4] ?? "").trim();
|
|
48
|
+
if (NO_ID.has(kind)) {
|
|
49
|
+
if (id !== void 0) return null;
|
|
50
|
+
return { kind, fields: {}, rest };
|
|
51
|
+
}
|
|
52
|
+
if (!WITH_ID.has(kind) || id === void 0) return null;
|
|
53
|
+
const out = { kind, id, fields: {}, rest };
|
|
54
|
+
if (kind === "verdict") {
|
|
55
|
+
if (extra !== "approve" && extra !== "object") return null;
|
|
56
|
+
out.verdict = extra;
|
|
57
|
+
} else if (kind === "owner") {
|
|
58
|
+
if (extra !== "veto" && extra !== "approve") return null;
|
|
59
|
+
out.action = extra;
|
|
60
|
+
} else if (kind === "event" || kind === "tick") {
|
|
61
|
+
if (extra === void 0) return null;
|
|
62
|
+
out.ref = extra;
|
|
63
|
+
} else if (extra !== void 0) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (WITH_FIELDS.has(kind)) {
|
|
67
|
+
for (const tok of rest.split(/\s+/)) {
|
|
68
|
+
if (!tok) continue;
|
|
69
|
+
const eq = tok.indexOf("=");
|
|
70
|
+
if (eq <= 0) return null;
|
|
71
|
+
out.fields[tok.slice(0, eq)] = tok.slice(eq + 1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ../../node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/index.mjs
|
|
78
|
+
import { createRequire } from "module";
|
|
79
|
+
var require2 = createRequire("/");
|
|
80
|
+
var _a;
|
|
81
|
+
var Worker;
|
|
82
|
+
var isMarkedAsUntransferable;
|
|
83
|
+
try {
|
|
84
|
+
_a = require2("worker_threads"), Worker = _a.Worker, isMarkedAsUntransferable = _a.isMarkedAsUntransferable;
|
|
85
|
+
} catch (e) {
|
|
86
|
+
}
|
|
87
|
+
var u8 = Uint8Array;
|
|
88
|
+
var u16 = Uint16Array;
|
|
89
|
+
var i32 = Int32Array;
|
|
90
|
+
var fleb = new u8([
|
|
91
|
+
0,
|
|
92
|
+
0,
|
|
93
|
+
0,
|
|
94
|
+
0,
|
|
95
|
+
0,
|
|
96
|
+
0,
|
|
97
|
+
0,
|
|
98
|
+
0,
|
|
99
|
+
1,
|
|
100
|
+
1,
|
|
101
|
+
1,
|
|
102
|
+
1,
|
|
103
|
+
2,
|
|
104
|
+
2,
|
|
105
|
+
2,
|
|
106
|
+
2,
|
|
107
|
+
3,
|
|
108
|
+
3,
|
|
109
|
+
3,
|
|
110
|
+
3,
|
|
111
|
+
4,
|
|
112
|
+
4,
|
|
113
|
+
4,
|
|
114
|
+
4,
|
|
115
|
+
5,
|
|
116
|
+
5,
|
|
117
|
+
5,
|
|
118
|
+
5,
|
|
119
|
+
0,
|
|
120
|
+
/* unused */
|
|
121
|
+
0,
|
|
122
|
+
0,
|
|
123
|
+
/* impossible */
|
|
124
|
+
0
|
|
125
|
+
]);
|
|
126
|
+
var fdeb = new u8([
|
|
127
|
+
0,
|
|
128
|
+
0,
|
|
129
|
+
0,
|
|
130
|
+
0,
|
|
131
|
+
1,
|
|
132
|
+
1,
|
|
133
|
+
2,
|
|
134
|
+
2,
|
|
135
|
+
3,
|
|
136
|
+
3,
|
|
137
|
+
4,
|
|
138
|
+
4,
|
|
139
|
+
5,
|
|
140
|
+
5,
|
|
141
|
+
6,
|
|
142
|
+
6,
|
|
143
|
+
7,
|
|
144
|
+
7,
|
|
145
|
+
8,
|
|
146
|
+
8,
|
|
147
|
+
9,
|
|
148
|
+
9,
|
|
149
|
+
10,
|
|
150
|
+
10,
|
|
151
|
+
11,
|
|
152
|
+
11,
|
|
153
|
+
12,
|
|
154
|
+
12,
|
|
155
|
+
13,
|
|
156
|
+
13,
|
|
157
|
+
/* unused */
|
|
158
|
+
0,
|
|
159
|
+
0
|
|
160
|
+
]);
|
|
161
|
+
var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
|
|
162
|
+
var freb = function(eb, start) {
|
|
163
|
+
var b = new u16(31);
|
|
164
|
+
for (var i = 0; i < 31; ++i) {
|
|
165
|
+
b[i] = start += 1 << eb[i - 1];
|
|
166
|
+
}
|
|
167
|
+
var r = new i32(b[30]);
|
|
168
|
+
for (var i = 1; i < 30; ++i) {
|
|
169
|
+
for (var j = b[i]; j < b[i + 1]; ++j) {
|
|
170
|
+
r[j] = j - b[i] << 5 | i;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { b, r };
|
|
174
|
+
};
|
|
175
|
+
var _a = freb(fleb, 2);
|
|
176
|
+
var fl = _a.b;
|
|
177
|
+
var revfl = _a.r;
|
|
178
|
+
fl[28] = 258, revfl[258] = 28;
|
|
179
|
+
var _b = freb(fdeb, 0);
|
|
180
|
+
var fd = _b.b;
|
|
181
|
+
var revfd = _b.r;
|
|
182
|
+
var rev = new u16(32768);
|
|
183
|
+
for (i = 0; i < 32768; ++i) {
|
|
184
|
+
x = (i & 43690) >> 1 | (i & 21845) << 1;
|
|
185
|
+
x = (x & 52428) >> 2 | (x & 13107) << 2;
|
|
186
|
+
x = (x & 61680) >> 4 | (x & 3855) << 4;
|
|
187
|
+
rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1;
|
|
188
|
+
}
|
|
189
|
+
var x;
|
|
190
|
+
var i;
|
|
191
|
+
var hMap = (function(cd, mb, r) {
|
|
192
|
+
var s = cd.length;
|
|
193
|
+
var i = 0;
|
|
194
|
+
var l = new u16(mb);
|
|
195
|
+
for (; i < s; ++i) {
|
|
196
|
+
if (cd[i])
|
|
197
|
+
++l[cd[i] - 1];
|
|
198
|
+
}
|
|
199
|
+
var le = new u16(mb);
|
|
200
|
+
for (i = 1; i < mb; ++i) {
|
|
201
|
+
le[i] = le[i - 1] + l[i - 1] << 1;
|
|
202
|
+
}
|
|
203
|
+
var co;
|
|
204
|
+
if (r) {
|
|
205
|
+
co = new u16(1 << mb);
|
|
206
|
+
var rvb = 15 - mb;
|
|
207
|
+
for (i = 0; i < s; ++i) {
|
|
208
|
+
if (cd[i]) {
|
|
209
|
+
var sv = i << 4 | cd[i];
|
|
210
|
+
var r_1 = mb - cd[i];
|
|
211
|
+
var v = le[cd[i] - 1]++ << r_1;
|
|
212
|
+
for (var m = v | (1 << r_1) - 1; v <= m; ++v) {
|
|
213
|
+
co[rev[v] >> rvb] = sv;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
co = new u16(s);
|
|
219
|
+
for (i = 0; i < s; ++i) {
|
|
220
|
+
if (cd[i]) {
|
|
221
|
+
co[i] = rev[le[cd[i] - 1]++] >> 15 - cd[i];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return co;
|
|
226
|
+
});
|
|
227
|
+
var flt = new u8(288);
|
|
228
|
+
for (i = 0; i < 144; ++i)
|
|
229
|
+
flt[i] = 8;
|
|
230
|
+
var i;
|
|
231
|
+
for (i = 144; i < 256; ++i)
|
|
232
|
+
flt[i] = 9;
|
|
233
|
+
var i;
|
|
234
|
+
for (i = 256; i < 280; ++i)
|
|
235
|
+
flt[i] = 7;
|
|
236
|
+
var i;
|
|
237
|
+
for (i = 280; i < 288; ++i)
|
|
238
|
+
flt[i] = 8;
|
|
239
|
+
var i;
|
|
240
|
+
var fdt = new u8(32);
|
|
241
|
+
for (i = 0; i < 32; ++i)
|
|
242
|
+
fdt[i] = 5;
|
|
243
|
+
var i;
|
|
244
|
+
var flm = /* @__PURE__ */ hMap(flt, 9, 0);
|
|
245
|
+
var flrm = /* @__PURE__ */ hMap(flt, 9, 1);
|
|
246
|
+
var fdm = /* @__PURE__ */ hMap(fdt, 5, 0);
|
|
247
|
+
var fdrm = /* @__PURE__ */ hMap(fdt, 5, 1);
|
|
248
|
+
var max = function(a) {
|
|
249
|
+
var m = a[0];
|
|
250
|
+
for (var i = 1; i < a.length; ++i) {
|
|
251
|
+
if (a[i] > m)
|
|
252
|
+
m = a[i];
|
|
253
|
+
}
|
|
254
|
+
return m;
|
|
255
|
+
};
|
|
256
|
+
var bits = function(d, p, m) {
|
|
257
|
+
var o = p / 8 | 0;
|
|
258
|
+
return (d[o] | d[o + 1] << 8) >> (p & 7) & m;
|
|
259
|
+
};
|
|
260
|
+
var bits16 = function(d, p) {
|
|
261
|
+
var o = p / 8 | 0;
|
|
262
|
+
return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7);
|
|
263
|
+
};
|
|
264
|
+
var shft = function(p) {
|
|
265
|
+
return (p + 7) / 8 | 0;
|
|
266
|
+
};
|
|
267
|
+
var slc = function(v, s, e) {
|
|
268
|
+
if (s == null || s < 0)
|
|
269
|
+
s = 0;
|
|
270
|
+
if (e == null || e > v.length)
|
|
271
|
+
e = v.length;
|
|
272
|
+
return new u8(v.subarray(s, e));
|
|
273
|
+
};
|
|
274
|
+
var ec = [
|
|
275
|
+
"unexpected EOF",
|
|
276
|
+
"invalid block type",
|
|
277
|
+
"invalid length/literal",
|
|
278
|
+
"invalid distance",
|
|
279
|
+
"stream finished",
|
|
280
|
+
"no stream handler",
|
|
281
|
+
,
|
|
282
|
+
// determined by compression function
|
|
283
|
+
"no callback",
|
|
284
|
+
"invalid UTF-8 data",
|
|
285
|
+
"extra field too long",
|
|
286
|
+
"date not in range 1980-2099",
|
|
287
|
+
"filename too long",
|
|
288
|
+
"stream finishing",
|
|
289
|
+
"invalid zip data"
|
|
290
|
+
// determined by unknown compression method
|
|
291
|
+
];
|
|
292
|
+
var err = function(ind, msg, nt) {
|
|
293
|
+
var e = new Error(msg || ec[ind]);
|
|
294
|
+
e.code = ind;
|
|
295
|
+
if (Error.captureStackTrace)
|
|
296
|
+
Error.captureStackTrace(e, err);
|
|
297
|
+
if (!nt)
|
|
298
|
+
throw e;
|
|
299
|
+
return e;
|
|
300
|
+
};
|
|
301
|
+
var inflt = function(dat, st, buf, dict) {
|
|
302
|
+
var sl = dat.length, dl = dict ? dict.length : 0;
|
|
303
|
+
if (!sl || st.f && !st.l)
|
|
304
|
+
return buf || new u8(0);
|
|
305
|
+
var noBuf = !buf;
|
|
306
|
+
var resize = noBuf || st.i != 2;
|
|
307
|
+
var noSt = st.i;
|
|
308
|
+
if (noBuf)
|
|
309
|
+
buf = new u8(sl * 3);
|
|
310
|
+
var cbuf = function(l2) {
|
|
311
|
+
var bl = buf.length;
|
|
312
|
+
if (l2 > bl) {
|
|
313
|
+
var nbuf = new u8(Math.max(bl * 2, l2));
|
|
314
|
+
nbuf.set(buf);
|
|
315
|
+
buf = nbuf;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n;
|
|
319
|
+
var tbts = sl * 8;
|
|
320
|
+
do {
|
|
321
|
+
if (!lm) {
|
|
322
|
+
final = bits(dat, pos, 1);
|
|
323
|
+
var type = bits(dat, pos + 1, 3);
|
|
324
|
+
pos += 3;
|
|
325
|
+
if (!type) {
|
|
326
|
+
var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l;
|
|
327
|
+
if (t > sl) {
|
|
328
|
+
if (noSt)
|
|
329
|
+
err(0);
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
if (resize)
|
|
333
|
+
cbuf(bt + l);
|
|
334
|
+
buf.set(dat.subarray(s, t), bt);
|
|
335
|
+
st.b = bt += l, st.p = pos = t * 8, st.f = final;
|
|
336
|
+
continue;
|
|
337
|
+
} else if (type == 1)
|
|
338
|
+
lm = flrm, dm = fdrm, lbt = 9, dbt = 5;
|
|
339
|
+
else if (type == 2) {
|
|
340
|
+
var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4;
|
|
341
|
+
var tl = hLit + bits(dat, pos + 5, 31) + 1;
|
|
342
|
+
pos += 14;
|
|
343
|
+
var ldt = new u8(tl);
|
|
344
|
+
var clt = new u8(19);
|
|
345
|
+
for (var i = 0; i < hcLen; ++i) {
|
|
346
|
+
clt[clim[i]] = bits(dat, pos + i * 3, 7);
|
|
347
|
+
}
|
|
348
|
+
pos += hcLen * 3;
|
|
349
|
+
var clb = max(clt), clbmsk = (1 << clb) - 1;
|
|
350
|
+
var clm = hMap(clt, clb, 1);
|
|
351
|
+
for (var i = 0; i < tl; ) {
|
|
352
|
+
var r = clm[bits(dat, pos, clbmsk)];
|
|
353
|
+
pos += r & 15;
|
|
354
|
+
var s = r >> 4;
|
|
355
|
+
if (s < 16) {
|
|
356
|
+
ldt[i++] = s;
|
|
357
|
+
} else {
|
|
358
|
+
var c = 0, n = 0;
|
|
359
|
+
if (s == 16)
|
|
360
|
+
n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1];
|
|
361
|
+
else if (s == 17)
|
|
362
|
+
n = 3 + bits(dat, pos, 7), pos += 3;
|
|
363
|
+
else if (s == 18)
|
|
364
|
+
n = 11 + bits(dat, pos, 127), pos += 7;
|
|
365
|
+
while (n--)
|
|
366
|
+
ldt[i++] = c;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit);
|
|
370
|
+
lbt = max(lt);
|
|
371
|
+
dbt = max(dt);
|
|
372
|
+
lm = hMap(lt, lbt, 1);
|
|
373
|
+
dm = hMap(dt, dbt, 1);
|
|
374
|
+
} else
|
|
375
|
+
err(1);
|
|
376
|
+
if (pos > tbts) {
|
|
377
|
+
if (noSt)
|
|
378
|
+
err(0);
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (resize)
|
|
383
|
+
cbuf(bt + 131072);
|
|
384
|
+
var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1;
|
|
385
|
+
var lpos = pos;
|
|
386
|
+
for (; ; lpos = pos) {
|
|
387
|
+
var c = lm[bits16(dat, pos) & lms], sym = c >> 4;
|
|
388
|
+
pos += c & 15;
|
|
389
|
+
if (pos > tbts) {
|
|
390
|
+
if (noSt)
|
|
391
|
+
err(0);
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
if (!c)
|
|
395
|
+
err(2);
|
|
396
|
+
if (sym < 256)
|
|
397
|
+
buf[bt++] = sym;
|
|
398
|
+
else if (sym == 256) {
|
|
399
|
+
lpos = pos, lm = null;
|
|
400
|
+
break;
|
|
401
|
+
} else {
|
|
402
|
+
var add = sym - 254;
|
|
403
|
+
if (sym > 264) {
|
|
404
|
+
var i = sym - 257, b = fleb[i];
|
|
405
|
+
add = bits(dat, pos, (1 << b) - 1) + fl[i];
|
|
406
|
+
pos += b;
|
|
407
|
+
}
|
|
408
|
+
var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
|
|
409
|
+
if (!d)
|
|
410
|
+
err(3);
|
|
411
|
+
pos += d & 15;
|
|
412
|
+
var dt = fd[dsym];
|
|
413
|
+
if (dsym > 3) {
|
|
414
|
+
var b = fdeb[dsym];
|
|
415
|
+
dt += bits16(dat, pos) & (1 << b) - 1, pos += b;
|
|
416
|
+
}
|
|
417
|
+
if (pos > tbts) {
|
|
418
|
+
if (noSt)
|
|
419
|
+
err(0);
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
if (resize)
|
|
423
|
+
cbuf(bt + 131072);
|
|
424
|
+
var end = bt + add;
|
|
425
|
+
if (bt < dt) {
|
|
426
|
+
var shift = dl - dt, dend = Math.min(dt, end);
|
|
427
|
+
if (shift + bt < 0)
|
|
428
|
+
err(3);
|
|
429
|
+
for (; bt < dend; ++bt)
|
|
430
|
+
buf[bt] = dict[shift + bt];
|
|
431
|
+
}
|
|
432
|
+
for (; bt < end; ++bt)
|
|
433
|
+
buf[bt] = buf[bt - dt];
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
st.l = lm, st.p = lpos, st.b = bt, st.f = final;
|
|
437
|
+
if (lm)
|
|
438
|
+
final = 1, st.m = lbt, st.d = dm, st.n = dbt;
|
|
439
|
+
} while (!final);
|
|
440
|
+
return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt);
|
|
441
|
+
};
|
|
442
|
+
var wbits = function(d, p, v) {
|
|
443
|
+
v <<= p & 7;
|
|
444
|
+
var o = p / 8 | 0;
|
|
445
|
+
d[o] |= v;
|
|
446
|
+
d[o + 1] |= v >> 8;
|
|
447
|
+
};
|
|
448
|
+
var wbits16 = function(d, p, v) {
|
|
449
|
+
v <<= p & 7;
|
|
450
|
+
var o = p / 8 | 0;
|
|
451
|
+
d[o] |= v;
|
|
452
|
+
d[o + 1] |= v >> 8;
|
|
453
|
+
d[o + 2] |= v >> 16;
|
|
454
|
+
};
|
|
455
|
+
var hTree = function(d, mb) {
|
|
456
|
+
var t = [];
|
|
457
|
+
for (var i = 0; i < d.length; ++i) {
|
|
458
|
+
if (d[i])
|
|
459
|
+
t.push({ s: i, f: d[i] });
|
|
460
|
+
}
|
|
461
|
+
var s = t.length;
|
|
462
|
+
var t2 = t.slice();
|
|
463
|
+
if (!s)
|
|
464
|
+
return { t: et, l: 0 };
|
|
465
|
+
if (s == 1) {
|
|
466
|
+
var v = new u8(t[0].s + 1);
|
|
467
|
+
v[t[0].s] = 1;
|
|
468
|
+
return { t: v, l: 1 };
|
|
469
|
+
}
|
|
470
|
+
t.sort(function(a, b) {
|
|
471
|
+
return a.f - b.f;
|
|
472
|
+
});
|
|
473
|
+
t.push({ s: -1, f: 25001 });
|
|
474
|
+
var l = t[0], r = t[1], i0 = 0, i1 = 1, i2 = 2;
|
|
475
|
+
t[0] = { s: -1, f: l.f + r.f, l, r };
|
|
476
|
+
while (i1 != s - 1) {
|
|
477
|
+
l = t[t[i0].f < t[i2].f ? i0++ : i2++];
|
|
478
|
+
r = t[i0 != i1 && t[i0].f < t[i2].f ? i0++ : i2++];
|
|
479
|
+
t[i1++] = { s: -1, f: l.f + r.f, l, r };
|
|
480
|
+
}
|
|
481
|
+
var maxSym = t2[0].s;
|
|
482
|
+
for (var i = 1; i < s; ++i) {
|
|
483
|
+
if (t2[i].s > maxSym)
|
|
484
|
+
maxSym = t2[i].s;
|
|
485
|
+
}
|
|
486
|
+
var tr = new u16(maxSym + 1);
|
|
487
|
+
var mbt = ln(t[i1 - 1], tr, 0);
|
|
488
|
+
if (mbt > mb) {
|
|
489
|
+
var i = 0, dt = 0;
|
|
490
|
+
var lft = mbt - mb, cst = 1 << lft;
|
|
491
|
+
t2.sort(function(a, b) {
|
|
492
|
+
return tr[b.s] - tr[a.s] || a.f - b.f;
|
|
493
|
+
});
|
|
494
|
+
for (; i < s; ++i) {
|
|
495
|
+
var i2_1 = t2[i].s;
|
|
496
|
+
if (tr[i2_1] > mb) {
|
|
497
|
+
dt += cst - (1 << mbt - tr[i2_1]);
|
|
498
|
+
tr[i2_1] = mb;
|
|
499
|
+
} else
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
dt >>= lft;
|
|
503
|
+
while (dt > 0) {
|
|
504
|
+
var i2_2 = t2[i].s;
|
|
505
|
+
if (tr[i2_2] < mb)
|
|
506
|
+
dt -= 1 << mb - tr[i2_2]++ - 1;
|
|
507
|
+
else
|
|
508
|
+
++i;
|
|
509
|
+
}
|
|
510
|
+
for (; i >= 0 && dt; --i) {
|
|
511
|
+
var i2_3 = t2[i].s;
|
|
512
|
+
if (tr[i2_3] == mb) {
|
|
513
|
+
--tr[i2_3];
|
|
514
|
+
++dt;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
mbt = mb;
|
|
518
|
+
}
|
|
519
|
+
return { t: new u8(tr), l: mbt };
|
|
520
|
+
};
|
|
521
|
+
var ln = function(n, l, d) {
|
|
522
|
+
return n.s == -1 ? Math.max(ln(n.l, l, d + 1), ln(n.r, l, d + 1)) : l[n.s] = d;
|
|
523
|
+
};
|
|
524
|
+
var lc = function(c) {
|
|
525
|
+
var s = c.length;
|
|
526
|
+
while (s && !c[--s])
|
|
527
|
+
;
|
|
528
|
+
var cl = new u16(++s);
|
|
529
|
+
var cli2 = 0, cln = c[0], cls = 1;
|
|
530
|
+
var w = function(v) {
|
|
531
|
+
cl[cli2++] = v;
|
|
532
|
+
};
|
|
533
|
+
for (var i = 1; i <= s; ++i) {
|
|
534
|
+
if (c[i] == cln && i != s)
|
|
535
|
+
++cls;
|
|
536
|
+
else {
|
|
537
|
+
if (!cln && cls > 2) {
|
|
538
|
+
for (; cls > 138; cls -= 138)
|
|
539
|
+
w(32754);
|
|
540
|
+
if (cls > 2) {
|
|
541
|
+
w(cls > 10 ? cls - 11 << 5 | 28690 : cls - 3 << 5 | 12305);
|
|
542
|
+
cls = 0;
|
|
543
|
+
}
|
|
544
|
+
} else if (cls > 3) {
|
|
545
|
+
w(cln), --cls;
|
|
546
|
+
for (; cls > 6; cls -= 6)
|
|
547
|
+
w(8304);
|
|
548
|
+
if (cls > 2)
|
|
549
|
+
w(cls - 3 << 5 | 8208), cls = 0;
|
|
550
|
+
}
|
|
551
|
+
while (cls--)
|
|
552
|
+
w(cln);
|
|
553
|
+
cls = 1;
|
|
554
|
+
cln = c[i];
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return { c: cl.subarray(0, cli2), n: s };
|
|
558
|
+
};
|
|
559
|
+
var clen = function(cf, cl) {
|
|
560
|
+
var l = 0;
|
|
561
|
+
for (var i = 0; i < cl.length; ++i)
|
|
562
|
+
l += cf[i] * cl[i];
|
|
563
|
+
return l;
|
|
564
|
+
};
|
|
565
|
+
var wfblk = function(out, pos, dat) {
|
|
566
|
+
var s = dat.length;
|
|
567
|
+
var o = shft(pos + 2);
|
|
568
|
+
out[o] = s & 255;
|
|
569
|
+
out[o + 1] = s >> 8;
|
|
570
|
+
out[o + 2] = out[o] ^ 255;
|
|
571
|
+
out[o + 3] = out[o + 1] ^ 255;
|
|
572
|
+
for (var i = 0; i < s; ++i)
|
|
573
|
+
out[o + i + 4] = dat[i];
|
|
574
|
+
return (o + 4 + s) * 8;
|
|
575
|
+
};
|
|
576
|
+
var wblk = function(dat, out, final, syms, lf, df, eb, li, bs, bl, p) {
|
|
577
|
+
wbits(out, p++, final);
|
|
578
|
+
++lf[256];
|
|
579
|
+
var _a2 = hTree(lf, 15), dlt = _a2.t, mlb = _a2.l;
|
|
580
|
+
var _b2 = hTree(df, 15), ddt = _b2.t, mdb = _b2.l;
|
|
581
|
+
var _c = lc(dlt), lclt = _c.c, nlc = _c.n;
|
|
582
|
+
var _d = lc(ddt), lcdt = _d.c, ndc = _d.n;
|
|
583
|
+
var lcfreq = new u16(19);
|
|
584
|
+
for (var i = 0; i < lclt.length; ++i)
|
|
585
|
+
++lcfreq[lclt[i] & 31];
|
|
586
|
+
for (var i = 0; i < lcdt.length; ++i)
|
|
587
|
+
++lcfreq[lcdt[i] & 31];
|
|
588
|
+
var _e = hTree(lcfreq, 7), lct = _e.t, mlcb = _e.l;
|
|
589
|
+
var nlcc = 19;
|
|
590
|
+
for (; nlcc > 4 && !lct[clim[nlcc - 1]]; --nlcc)
|
|
591
|
+
;
|
|
592
|
+
var flen = bl + 5 << 3;
|
|
593
|
+
var ftlen = clen(lf, flt) + clen(df, fdt) + eb;
|
|
594
|
+
var dtlen = clen(lf, dlt) + clen(df, ddt) + eb + 14 + 3 * nlcc + clen(lcfreq, lct) + 2 * lcfreq[16] + 3 * lcfreq[17] + 7 * lcfreq[18];
|
|
595
|
+
if (bs >= 0 && flen <= ftlen && flen <= dtlen)
|
|
596
|
+
return wfblk(out, p, dat.subarray(bs, bs + bl));
|
|
597
|
+
var lm, ll, dm, dl;
|
|
598
|
+
wbits(out, p, 1 + (dtlen < ftlen)), p += 2;
|
|
599
|
+
if (dtlen < ftlen) {
|
|
600
|
+
lm = hMap(dlt, mlb, 0), ll = dlt, dm = hMap(ddt, mdb, 0), dl = ddt;
|
|
601
|
+
var llm = hMap(lct, mlcb, 0);
|
|
602
|
+
wbits(out, p, nlc - 257);
|
|
603
|
+
wbits(out, p + 5, ndc - 1);
|
|
604
|
+
wbits(out, p + 10, nlcc - 4);
|
|
605
|
+
p += 14;
|
|
606
|
+
for (var i = 0; i < nlcc; ++i)
|
|
607
|
+
wbits(out, p + 3 * i, lct[clim[i]]);
|
|
608
|
+
p += 3 * nlcc;
|
|
609
|
+
var lcts = [lclt, lcdt];
|
|
610
|
+
for (var it = 0; it < 2; ++it) {
|
|
611
|
+
var clct = lcts[it];
|
|
612
|
+
for (var i = 0; i < clct.length; ++i) {
|
|
613
|
+
var len = clct[i] & 31;
|
|
614
|
+
wbits(out, p, llm[len]), p += lct[len];
|
|
615
|
+
if (len > 15)
|
|
616
|
+
wbits(out, p, clct[i] >> 5 & 127), p += clct[i] >> 12;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
} else {
|
|
620
|
+
lm = flm, ll = flt, dm = fdm, dl = fdt;
|
|
621
|
+
}
|
|
622
|
+
for (var i = 0; i < li; ++i) {
|
|
623
|
+
var sym = syms[i];
|
|
624
|
+
if (sym > 255) {
|
|
625
|
+
var len = sym >> 18 & 31;
|
|
626
|
+
wbits16(out, p, lm[len + 257]), p += ll[len + 257];
|
|
627
|
+
if (len > 7)
|
|
628
|
+
wbits(out, p, sym >> 23 & 31), p += fleb[len];
|
|
629
|
+
var dst = sym & 31;
|
|
630
|
+
wbits16(out, p, dm[dst]), p += dl[dst];
|
|
631
|
+
if (dst > 3)
|
|
632
|
+
wbits16(out, p, sym >> 5 & 8191), p += fdeb[dst];
|
|
633
|
+
} else {
|
|
634
|
+
wbits16(out, p, lm[sym]), p += ll[sym];
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
wbits16(out, p, lm[256]);
|
|
638
|
+
return p + ll[256];
|
|
639
|
+
};
|
|
640
|
+
var deo = /* @__PURE__ */ new i32([65540, 131080, 131088, 131104, 262176, 1048704, 1048832, 2114560, 2117632]);
|
|
641
|
+
var et = /* @__PURE__ */ new u8(0);
|
|
642
|
+
var dflt = function(dat, lvl, plvl, pre, post, st) {
|
|
643
|
+
var s = st.z || dat.length;
|
|
644
|
+
var o = new u8(pre + s + 5 * (1 + Math.ceil(s / 7e3)) + post);
|
|
645
|
+
var w = o.subarray(pre, o.length - post);
|
|
646
|
+
var lst = st.l;
|
|
647
|
+
var pos = (st.r || 0) & 7;
|
|
648
|
+
if (lvl) {
|
|
649
|
+
if (pos)
|
|
650
|
+
w[0] = st.r >> 3;
|
|
651
|
+
var opt = deo[lvl - 1];
|
|
652
|
+
var n = opt >> 13, c = opt & 8191;
|
|
653
|
+
var msk_1 = (1 << plvl) - 1;
|
|
654
|
+
var prev = st.p || new u16(32768), head = st.h || new u16(msk_1 + 1);
|
|
655
|
+
var bs1_1 = Math.ceil(plvl / 3), bs2_1 = 2 * bs1_1;
|
|
656
|
+
var hsh = function(i2) {
|
|
657
|
+
return (dat[i2] ^ dat[i2 + 1] << bs1_1 ^ dat[i2 + 2] << bs2_1) & msk_1;
|
|
658
|
+
};
|
|
659
|
+
var syms = new i32(25e3);
|
|
660
|
+
var lf = new u16(288), df = new u16(32);
|
|
661
|
+
var lc_1 = 0, eb = 0, i = st.i || 0, li = 0, wi = st.w || 0, bs = 0;
|
|
662
|
+
for (; i + 2 < s; ++i) {
|
|
663
|
+
var hv = hsh(i);
|
|
664
|
+
var imod = i & 32767, pimod = head[hv];
|
|
665
|
+
prev[imod] = pimod;
|
|
666
|
+
head[hv] = imod;
|
|
667
|
+
if (wi <= i) {
|
|
668
|
+
var rem = s - i;
|
|
669
|
+
if ((lc_1 > 7e3 || li > 24576) && (rem > 423 || !lst)) {
|
|
670
|
+
pos = wblk(dat, w, 0, syms, lf, df, eb, li, bs, i - bs, pos);
|
|
671
|
+
li = lc_1 = eb = 0, bs = i;
|
|
672
|
+
for (var j = 0; j < 286; ++j)
|
|
673
|
+
lf[j] = 0;
|
|
674
|
+
for (var j = 0; j < 30; ++j)
|
|
675
|
+
df[j] = 0;
|
|
676
|
+
}
|
|
677
|
+
var l = 2, d = 0, ch_1 = c, dif = imod - pimod & 32767;
|
|
678
|
+
if (rem > 2 && hv == hsh(i - dif)) {
|
|
679
|
+
var maxn = Math.min(n, rem) - 1;
|
|
680
|
+
var maxd = Math.min(32767, i);
|
|
681
|
+
var ml = Math.min(258, rem);
|
|
682
|
+
while (dif <= maxd && --ch_1 && imod != pimod) {
|
|
683
|
+
if (dat[i + l] == dat[i + l - dif]) {
|
|
684
|
+
var nl2 = 0;
|
|
685
|
+
for (; nl2 < ml && dat[i + nl2] == dat[i + nl2 - dif]; ++nl2)
|
|
686
|
+
;
|
|
687
|
+
if (nl2 > l) {
|
|
688
|
+
l = nl2, d = dif;
|
|
689
|
+
if (nl2 > maxn)
|
|
690
|
+
break;
|
|
691
|
+
var mmd = Math.min(dif, nl2 - 2);
|
|
692
|
+
var md = 0;
|
|
693
|
+
for (var j = 0; j < mmd; ++j) {
|
|
694
|
+
var ti = i - dif + j & 32767;
|
|
695
|
+
var pti = prev[ti];
|
|
696
|
+
var cd = ti - pti & 32767;
|
|
697
|
+
if (cd > md)
|
|
698
|
+
md = cd, pimod = ti;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
imod = pimod, pimod = prev[imod];
|
|
703
|
+
dif += imod - pimod & 32767;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (d) {
|
|
707
|
+
syms[li++] = 268435456 | revfl[l] << 18 | revfd[d];
|
|
708
|
+
var lin = revfl[l] & 31, din = revfd[d] & 31;
|
|
709
|
+
eb += fleb[lin] + fdeb[din];
|
|
710
|
+
++lf[257 + lin];
|
|
711
|
+
++df[din];
|
|
712
|
+
wi = i + l;
|
|
713
|
+
++lc_1;
|
|
714
|
+
} else {
|
|
715
|
+
syms[li++] = dat[i];
|
|
716
|
+
++lf[dat[i]];
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
for (i = Math.max(i, wi); i < s; ++i) {
|
|
721
|
+
syms[li++] = dat[i];
|
|
722
|
+
++lf[dat[i]];
|
|
723
|
+
}
|
|
724
|
+
pos = wblk(dat, w, lst, syms, lf, df, eb, li, bs, i - bs, pos);
|
|
725
|
+
if (!lst) {
|
|
726
|
+
st.r = pos & 7 | w[pos / 8 | 0] << 3;
|
|
727
|
+
pos -= 7;
|
|
728
|
+
st.h = head, st.p = prev, st.i = i, st.w = wi;
|
|
729
|
+
}
|
|
730
|
+
} else {
|
|
731
|
+
for (var i = st.w || 0; i < s + lst; i += 65535) {
|
|
732
|
+
var e = i + 65535;
|
|
733
|
+
if (e >= s) {
|
|
734
|
+
w[pos / 8 | 0] = lst;
|
|
735
|
+
e = s;
|
|
736
|
+
}
|
|
737
|
+
pos = wfblk(w, pos + 1, dat.subarray(i, e));
|
|
738
|
+
}
|
|
739
|
+
st.i = s;
|
|
740
|
+
}
|
|
741
|
+
return slc(o, 0, pre + shft(pos) + post);
|
|
742
|
+
};
|
|
743
|
+
var crct = /* @__PURE__ */ (function() {
|
|
744
|
+
var t = new Int32Array(256);
|
|
745
|
+
for (var i = 0; i < 256; ++i) {
|
|
746
|
+
var c = i, k = 9;
|
|
747
|
+
while (--k)
|
|
748
|
+
c = (c & 1 && -306674912) ^ c >>> 1;
|
|
749
|
+
t[i] = c;
|
|
750
|
+
}
|
|
751
|
+
return t;
|
|
752
|
+
})();
|
|
753
|
+
var crc = function() {
|
|
754
|
+
var c = -1;
|
|
755
|
+
return {
|
|
756
|
+
p: function(d) {
|
|
757
|
+
var cr = c;
|
|
758
|
+
for (var i = 0; i < d.length; ++i)
|
|
759
|
+
cr = crct[cr & 255 ^ d[i]] ^ cr >>> 8;
|
|
760
|
+
c = cr;
|
|
761
|
+
},
|
|
762
|
+
d: function() {
|
|
763
|
+
return ~c;
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
};
|
|
767
|
+
var dopt = function(dat, opt, pre, post, st) {
|
|
768
|
+
if (!st) {
|
|
769
|
+
st = { l: 1 };
|
|
770
|
+
if (opt.dictionary) {
|
|
771
|
+
var dict = opt.dictionary.subarray(-32768);
|
|
772
|
+
var newDat = new u8(dict.length + dat.length);
|
|
773
|
+
newDat.set(dict);
|
|
774
|
+
newDat.set(dat, dict.length);
|
|
775
|
+
dat = newDat;
|
|
776
|
+
st.w = dict.length;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return dflt(dat, opt.level == null ? 6 : opt.level, opt.mem == null ? st.l ? Math.ceil(Math.max(8, Math.min(13, Math.log(dat.length))) * 1.5) : 20 : 12 + opt.mem, pre, post, st);
|
|
780
|
+
};
|
|
781
|
+
var wbytes = function(d, b, v) {
|
|
782
|
+
for (; v; ++b)
|
|
783
|
+
d[b] = v, v >>>= 8;
|
|
784
|
+
};
|
|
785
|
+
var gzh = function(c, o) {
|
|
786
|
+
var fn = o.filename;
|
|
787
|
+
c[0] = 31, c[1] = 139, c[2] = 8, c[8] = o.level < 2 ? 4 : o.level == 9 ? 2 : 0, c[9] = 3;
|
|
788
|
+
if (o.mtime != 0)
|
|
789
|
+
wbytes(c, 4, Math.floor(new Date(o.mtime || Date.now()) / 1e3));
|
|
790
|
+
if (fn) {
|
|
791
|
+
c[3] = 8;
|
|
792
|
+
for (var i = 0; i <= fn.length; ++i)
|
|
793
|
+
c[i + 10] = fn.charCodeAt(i);
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
var gzs = function(d) {
|
|
797
|
+
if (d[0] != 31 || d[1] != 139 || d[2] != 8)
|
|
798
|
+
err(6, "invalid gzip data");
|
|
799
|
+
var flg = d[3];
|
|
800
|
+
var st = 10;
|
|
801
|
+
if (flg & 4)
|
|
802
|
+
st += (d[10] | d[11] << 8) + 2;
|
|
803
|
+
for (var zs = (flg >> 3 & 1) + (flg >> 4 & 1); zs > 0; zs -= !d[st++])
|
|
804
|
+
;
|
|
805
|
+
return st + (flg & 2);
|
|
806
|
+
};
|
|
807
|
+
var gzhl = function(o) {
|
|
808
|
+
return 10 + (o.filename ? o.filename.length + 1 : 0);
|
|
809
|
+
};
|
|
810
|
+
var Inflate = /* @__PURE__ */ (function() {
|
|
811
|
+
function Inflate2(opts, cb) {
|
|
812
|
+
if (typeof opts == "function")
|
|
813
|
+
cb = opts, opts = {};
|
|
814
|
+
this.ondata = cb;
|
|
815
|
+
var dict = opts && opts.dictionary && opts.dictionary.subarray(-32768);
|
|
816
|
+
this.s = { i: 0, b: dict ? dict.length : 0 };
|
|
817
|
+
this.o = new u8(32768);
|
|
818
|
+
this.p = new u8(0);
|
|
819
|
+
if (dict)
|
|
820
|
+
this.o.set(dict);
|
|
821
|
+
}
|
|
822
|
+
Inflate2.prototype.e = function(c) {
|
|
823
|
+
if (!this.ondata)
|
|
824
|
+
err(5);
|
|
825
|
+
if (this.d)
|
|
826
|
+
err(4);
|
|
827
|
+
if (!this.p.length)
|
|
828
|
+
this.p = c;
|
|
829
|
+
else if (c.length) {
|
|
830
|
+
var n = new u8(this.p.length + c.length);
|
|
831
|
+
n.set(this.p), n.set(c, this.p.length), this.p = n;
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
Inflate2.prototype.c = function(final) {
|
|
835
|
+
this.s.i = +(this.d = final || false);
|
|
836
|
+
var bts = this.s.b;
|
|
837
|
+
var dt = inflt(this.p, this.s, this.o);
|
|
838
|
+
this.ondata(slc(dt, bts, this.s.b), this.d);
|
|
839
|
+
this.o = slc(dt, this.s.b - 32768), this.s.b = this.o.length;
|
|
840
|
+
this.p = slc(this.p, this.s.p / 8 | 0), this.s.p &= 7;
|
|
841
|
+
};
|
|
842
|
+
Inflate2.prototype.push = function(chunk, final) {
|
|
843
|
+
this.e(chunk), this.c(final);
|
|
844
|
+
};
|
|
845
|
+
return Inflate2;
|
|
846
|
+
})();
|
|
847
|
+
function gzipSync(data, opts) {
|
|
848
|
+
if (!opts)
|
|
849
|
+
opts = {};
|
|
850
|
+
var c = crc(), l = data.length;
|
|
851
|
+
c.p(data);
|
|
852
|
+
var d = dopt(data, opts, gzhl(opts), 8), s = d.length;
|
|
853
|
+
return gzh(d, opts), wbytes(d, s - 8, c.d()), wbytes(d, s - 4, l), d;
|
|
854
|
+
}
|
|
855
|
+
var Gunzip = /* @__PURE__ */ (function() {
|
|
856
|
+
function Gunzip2(opts, cb) {
|
|
857
|
+
this.v = 1;
|
|
858
|
+
this.r = 0;
|
|
859
|
+
Inflate.call(this, opts, cb);
|
|
860
|
+
}
|
|
861
|
+
Gunzip2.prototype.push = function(chunk, final) {
|
|
862
|
+
Inflate.prototype.e.call(this, chunk);
|
|
863
|
+
this.r += chunk.length;
|
|
864
|
+
if (this.v) {
|
|
865
|
+
var p = this.p.subarray(this.v - 1);
|
|
866
|
+
var s = p.length > 3 ? gzs(p) : 4;
|
|
867
|
+
if (s > p.length) {
|
|
868
|
+
if (!final)
|
|
869
|
+
return;
|
|
870
|
+
} else if (this.v > 1 && this.onmember) {
|
|
871
|
+
this.onmember(this.r - p.length);
|
|
872
|
+
}
|
|
873
|
+
this.p = p.subarray(s), this.v = 0;
|
|
874
|
+
}
|
|
875
|
+
Inflate.prototype.c.call(this, 0);
|
|
876
|
+
if (this.s.f && !this.s.l) {
|
|
877
|
+
this.v = shft(this.s.p) + 9;
|
|
878
|
+
this.s = { i: 0 };
|
|
879
|
+
this.o = new u8(0);
|
|
880
|
+
this.push(new u8(0), final);
|
|
881
|
+
} else if (final) {
|
|
882
|
+
Inflate.prototype.c.call(this, final);
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
return Gunzip2;
|
|
886
|
+
})();
|
|
887
|
+
var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder();
|
|
888
|
+
var tds = 0;
|
|
889
|
+
try {
|
|
890
|
+
td.decode(et, { stream: true });
|
|
891
|
+
tds = 1;
|
|
892
|
+
} catch (e) {
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// ../core/src/util.ts
|
|
896
|
+
var enc = new TextEncoder();
|
|
897
|
+
var dec = new TextDecoder();
|
|
898
|
+
var strictDec = new TextDecoder("utf-8", { fatal: true });
|
|
899
|
+
function utf8(s) {
|
|
900
|
+
return enc.encode(s);
|
|
901
|
+
}
|
|
902
|
+
function fromUtf8(b) {
|
|
903
|
+
return dec.decode(b);
|
|
904
|
+
}
|
|
905
|
+
function asText(b) {
|
|
906
|
+
if (b.includes(0)) return null;
|
|
907
|
+
try {
|
|
908
|
+
return strictDec.decode(b);
|
|
909
|
+
} catch {
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
function bytesEqual(a, b) {
|
|
914
|
+
if (a.length !== b.length) return false;
|
|
915
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
function compareBytes(a, b) {
|
|
919
|
+
const n = Math.min(a.length, b.length);
|
|
920
|
+
for (let i = 0; i < n; i++) {
|
|
921
|
+
const d = a[i] - b[i];
|
|
922
|
+
if (d !== 0) return d;
|
|
923
|
+
}
|
|
924
|
+
return a.length - b.length;
|
|
925
|
+
}
|
|
926
|
+
function sortPaths(paths) {
|
|
927
|
+
return [...paths].map((p) => ({ p, b: utf8(p) })).sort((x, y) => compareBytes(x.b, y.b)).map((x) => x.p);
|
|
928
|
+
}
|
|
929
|
+
function concat(parts) {
|
|
930
|
+
let n = 0;
|
|
931
|
+
for (const p of parts) n += p.length;
|
|
932
|
+
const out = new Uint8Array(n);
|
|
933
|
+
let o = 0;
|
|
934
|
+
for (const p of parts) {
|
|
935
|
+
out.set(p, o);
|
|
936
|
+
o += p.length;
|
|
937
|
+
}
|
|
938
|
+
return out;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// ../core/src/patch.ts
|
|
942
|
+
var PATCH_LIMITS = {
|
|
943
|
+
maxBytes: 5 * 1024 * 1024,
|
|
944
|
+
maxFiles: 500,
|
|
945
|
+
maxPathBytes: 400,
|
|
946
|
+
maxComponents: 30,
|
|
947
|
+
fuzz: 3,
|
|
948
|
+
context: 3
|
|
949
|
+
};
|
|
950
|
+
var DEVNULL = "/dev/null";
|
|
951
|
+
function foldKey(p) {
|
|
952
|
+
return p.normalize("NFC").toLowerCase();
|
|
953
|
+
}
|
|
954
|
+
var B85 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~";
|
|
955
|
+
var B85_VALUE = new Map([...B85].map((c, i) => [c, i]));
|
|
956
|
+
function tokens(s) {
|
|
957
|
+
const out = s.split(/(?<=\n)/);
|
|
958
|
+
if (out[out.length - 1] === "") out.pop();
|
|
959
|
+
return out;
|
|
960
|
+
}
|
|
961
|
+
var MAX_EDIT_DISTANCE = 2e3;
|
|
962
|
+
function myers(a, b) {
|
|
963
|
+
const n = a.length;
|
|
964
|
+
const m = b.length;
|
|
965
|
+
const max2 = n + m;
|
|
966
|
+
const off = max2 + 1;
|
|
967
|
+
const v = new Int32Array(2 * max2 + 3);
|
|
968
|
+
const snaps = [];
|
|
969
|
+
for (let d = 0; d <= Math.min(max2, MAX_EDIT_DISTANCE); d++) {
|
|
970
|
+
for (let k = -d; k <= d; k += 2) {
|
|
971
|
+
let x = k === -d || k !== d && v[off + k - 1] < v[off + k + 1] ? v[off + k + 1] : v[off + k - 1] + 1;
|
|
972
|
+
let y = x - k;
|
|
973
|
+
while (x < n && y < m && a[x] === b[y]) {
|
|
974
|
+
x++;
|
|
975
|
+
y++;
|
|
976
|
+
}
|
|
977
|
+
v[off + k] = x;
|
|
978
|
+
}
|
|
979
|
+
snaps.push(v.slice(off - d, off + d + 1));
|
|
980
|
+
const kEnd = n - m;
|
|
981
|
+
if (Math.abs(kEnd) <= d && (d - kEnd) % 2 === 0 && v[off + kEnd] >= n) {
|
|
982
|
+
const ops = [];
|
|
983
|
+
let x = n;
|
|
984
|
+
let y = m;
|
|
985
|
+
for (let dd = d; dd > 0; dd--) {
|
|
986
|
+
const prev = snaps[dd - 1];
|
|
987
|
+
const get = (kk) => prev[kk + dd - 1];
|
|
988
|
+
const k = x - y;
|
|
989
|
+
const down = k === -dd || k !== dd && get(k - 1) < get(k + 1);
|
|
990
|
+
const pk = down ? k + 1 : k - 1;
|
|
991
|
+
const px = get(pk);
|
|
992
|
+
const py = px - pk;
|
|
993
|
+
while (x > (down ? px : px + 1) && y > (down ? py + 1 : py)) {
|
|
994
|
+
ops.push([" ", a[x - 1]]);
|
|
995
|
+
x--;
|
|
996
|
+
y--;
|
|
997
|
+
}
|
|
998
|
+
if (down) {
|
|
999
|
+
ops.push(["+", b[y - 1]]);
|
|
1000
|
+
y--;
|
|
1001
|
+
} else {
|
|
1002
|
+
ops.push(["-", a[x - 1]]);
|
|
1003
|
+
x--;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
while (x > 0 && y > 0) {
|
|
1007
|
+
ops.push([" ", a[x - 1]]);
|
|
1008
|
+
x--;
|
|
1009
|
+
y--;
|
|
1010
|
+
}
|
|
1011
|
+
return ops.reverse();
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
function diffTokens(a, b) {
|
|
1017
|
+
let pre = 0;
|
|
1018
|
+
while (pre < a.length && pre < b.length && a[pre] === b[pre]) pre++;
|
|
1019
|
+
let suf = 0;
|
|
1020
|
+
while (suf < a.length - pre && suf < b.length - pre && a[a.length - 1 - suf] === b[b.length - 1 - suf]) suf++;
|
|
1021
|
+
const A = a.slice(pre, a.length - suf);
|
|
1022
|
+
const B = b.slice(pre, b.length - suf);
|
|
1023
|
+
const mid = myers(A, B) ?? [...A.map((t) => ["-", t]), ...B.map((t) => ["+", t])];
|
|
1024
|
+
return [
|
|
1025
|
+
...a.slice(0, pre).map((t) => [" ", t]),
|
|
1026
|
+
...mid,
|
|
1027
|
+
...a.slice(a.length - suf).map((t) => [" ", t])
|
|
1028
|
+
];
|
|
1029
|
+
}
|
|
1030
|
+
var lineTokens = tokens;
|
|
1031
|
+
var diffLines = diffTokens;
|
|
1032
|
+
function range(start, count) {
|
|
1033
|
+
const s = count === 0 ? start : start + 1;
|
|
1034
|
+
return count === 1 ? `${s}` : `${s},${count}`;
|
|
1035
|
+
}
|
|
1036
|
+
function unifiedHunks(a, b) {
|
|
1037
|
+
const ops = diffTokens(tokens(a), tokens(b));
|
|
1038
|
+
const ctx = PATCH_LIMITS.context;
|
|
1039
|
+
const out = [];
|
|
1040
|
+
let i = 0;
|
|
1041
|
+
let oldLine = 0;
|
|
1042
|
+
let newLine = 0;
|
|
1043
|
+
while (i < ops.length) {
|
|
1044
|
+
if (ops[i][0] === " ") {
|
|
1045
|
+
i++;
|
|
1046
|
+
oldLine++;
|
|
1047
|
+
newLine++;
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
const start = Math.max(0, i - ctx);
|
|
1051
|
+
let last = i;
|
|
1052
|
+
let j = i;
|
|
1053
|
+
while (j < ops.length) {
|
|
1054
|
+
if (ops[j][0] !== " ") last = j;
|
|
1055
|
+
else if (j - last > 2 * ctx) break;
|
|
1056
|
+
j++;
|
|
1057
|
+
}
|
|
1058
|
+
const end = Math.min(ops.length, last + ctx + 1);
|
|
1059
|
+
const lead = i - start;
|
|
1060
|
+
const oStart = oldLine - lead;
|
|
1061
|
+
const nStart = newLine - lead;
|
|
1062
|
+
let oc = 0;
|
|
1063
|
+
let nc = 0;
|
|
1064
|
+
const body = [];
|
|
1065
|
+
for (let k = start; k < end; k++) {
|
|
1066
|
+
const [op, t] = ops[k];
|
|
1067
|
+
if (op !== "+") oc++;
|
|
1068
|
+
if (op !== "-") nc++;
|
|
1069
|
+
body.push(op + (t.endsWith("\n") ? t.slice(0, -1) : t));
|
|
1070
|
+
if (!t.endsWith("\n")) body.push("\");
|
|
1071
|
+
}
|
|
1072
|
+
out.push(`@@ -${range(oStart, oc)} +${range(nStart, nc)} @@`, ...body);
|
|
1073
|
+
oldLine = oStart + oc;
|
|
1074
|
+
newLine = nStart + nc;
|
|
1075
|
+
i = end;
|
|
1076
|
+
}
|
|
1077
|
+
return out;
|
|
1078
|
+
}
|
|
1079
|
+
function side(prefix, p) {
|
|
1080
|
+
return `${prefix}${p}${p.includes(" ") ? " " : ""}`;
|
|
1081
|
+
}
|
|
1082
|
+
var MAX_LOSSY_BYTES = 256 * 1024;
|
|
1083
|
+
var looseDec = new TextDecoder("utf-8", { fatal: false });
|
|
1084
|
+
function lossyText(b) {
|
|
1085
|
+
return looseDec.decode(b).replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, (c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, "0")}`);
|
|
1086
|
+
}
|
|
1087
|
+
function binaryPaths(a, b) {
|
|
1088
|
+
const out = [];
|
|
1089
|
+
for (const p of sortPaths(/* @__PURE__ */ new Set([...a.keys(), ...b.keys()]))) {
|
|
1090
|
+
const A = a.get(p);
|
|
1091
|
+
const B = b.get(p);
|
|
1092
|
+
if (A && B && bytesEqual(A, B)) continue;
|
|
1093
|
+
if (A && asText(A) === null || B && asText(B) === null) out.push(p);
|
|
1094
|
+
}
|
|
1095
|
+
return out;
|
|
1096
|
+
}
|
|
1097
|
+
function renderDiff(a, b, o = {}) {
|
|
1098
|
+
const out = [];
|
|
1099
|
+
for (const p of sortPaths(/* @__PURE__ */ new Set([...a.keys(), ...b.keys()]))) {
|
|
1100
|
+
const A = a.get(p);
|
|
1101
|
+
const B = b.get(p);
|
|
1102
|
+
if (A && B && bytesEqual(A, B)) continue;
|
|
1103
|
+
out.push(`diff --git a/${p} b/${p}`);
|
|
1104
|
+
if (!A) out.push("new file mode 100644");
|
|
1105
|
+
if (!B) out.push("deleted file mode 100644");
|
|
1106
|
+
let ta = A ? asText(A) : "";
|
|
1107
|
+
let tb = B ? asText(B) : "";
|
|
1108
|
+
const minus = A ? `a/${p}` : DEVNULL;
|
|
1109
|
+
const plus = B ? `b/${p}` : DEVNULL;
|
|
1110
|
+
if (ta === null || tb === null) {
|
|
1111
|
+
const small = (A?.length ?? 0) <= MAX_LOSSY_BYTES && (B?.length ?? 0) <= MAX_LOSSY_BYTES;
|
|
1112
|
+
if (!o.lossy || !small) {
|
|
1113
|
+
out.push(`Binary files ${minus} and ${plus} differ`);
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
out.push(`map: ${p} is binary or not valid UTF-8 text; shown with undecodable bytes as U+FFFD and control bytes as \\xNN`);
|
|
1117
|
+
ta = A ? lossyText(A) : "";
|
|
1118
|
+
tb = B ? lossyText(B) : "";
|
|
1119
|
+
}
|
|
1120
|
+
if (ta === "" && tb === "") continue;
|
|
1121
|
+
out.push(`--- ${A ? side("a/", p) : DEVNULL}`, `+++ ${B ? side("b/", p) : DEVNULL}`, ...unifiedHunks(ta, tb));
|
|
1122
|
+
}
|
|
1123
|
+
return out.length ? out.join("\n") + "\n" : "";
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// ../core/src/merge.ts
|
|
1127
|
+
function changes(base, side2) {
|
|
1128
|
+
const out = [];
|
|
1129
|
+
let i = 0;
|
|
1130
|
+
let j = 0;
|
|
1131
|
+
let cur = null;
|
|
1132
|
+
for (const [op] of diffLines(base, side2)) {
|
|
1133
|
+
if (op === " ") {
|
|
1134
|
+
if (cur) out.push(cur);
|
|
1135
|
+
cur = null;
|
|
1136
|
+
i++;
|
|
1137
|
+
j++;
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
cur ??= { bs: i, be: i, ss: j, se: j };
|
|
1141
|
+
if (op === "-") cur.be = ++i;
|
|
1142
|
+
else cur.se = ++j;
|
|
1143
|
+
}
|
|
1144
|
+
if (cur) out.push(cur);
|
|
1145
|
+
return out;
|
|
1146
|
+
}
|
|
1147
|
+
var join = (lines2) => lines2.join("");
|
|
1148
|
+
var range2 = (start, end) => ({ start: start + 1, count: end - start });
|
|
1149
|
+
function isBinary(s) {
|
|
1150
|
+
return s.includes("\0");
|
|
1151
|
+
}
|
|
1152
|
+
var TOKEN = /\w+|[^\S\n]+|\n|[^\w\s]/g;
|
|
1153
|
+
function mergeInsertedTokens(base, ours, theirs) {
|
|
1154
|
+
if (!base) return null;
|
|
1155
|
+
const tok = (s) => s.match(TOKEN) ?? [];
|
|
1156
|
+
const B = tok(base);
|
|
1157
|
+
const inserts = (side2) => {
|
|
1158
|
+
const at = /* @__PURE__ */ new Map();
|
|
1159
|
+
let i = 0;
|
|
1160
|
+
for (const [op, t2] of diffLines(B, side2)) {
|
|
1161
|
+
if (op === " ") i++;
|
|
1162
|
+
else if (op === "-") return null;
|
|
1163
|
+
else at.set(i, [...at.get(i) ?? [], t2]);
|
|
1164
|
+
}
|
|
1165
|
+
return at;
|
|
1166
|
+
};
|
|
1167
|
+
const o = inserts(tok(ours));
|
|
1168
|
+
const t = inserts(tok(theirs));
|
|
1169
|
+
if (!o || !t) return null;
|
|
1170
|
+
let out = "";
|
|
1171
|
+
for (let i = 0; i <= B.length; i++) {
|
|
1172
|
+
const a = (o.get(i) ?? []).join("");
|
|
1173
|
+
const b = (t.get(i) ?? []).join("");
|
|
1174
|
+
out += a === b ? a : a + b;
|
|
1175
|
+
if (i < B.length) out += B[i];
|
|
1176
|
+
}
|
|
1177
|
+
return out;
|
|
1178
|
+
}
|
|
1179
|
+
function merge3(base, ours, theirs) {
|
|
1180
|
+
if (ours === theirs) return { ok: true, text: ours, unioned: false };
|
|
1181
|
+
if (ours === base) return { ok: true, text: theirs, unioned: false };
|
|
1182
|
+
if (theirs === base) return { ok: true, text: ours, unioned: false };
|
|
1183
|
+
const B = lineTokens(base);
|
|
1184
|
+
const O = lineTokens(ours);
|
|
1185
|
+
const T = lineTokens(theirs);
|
|
1186
|
+
if (isBinary(base) || isBinary(ours) || isBinary(theirs)) {
|
|
1187
|
+
return {
|
|
1188
|
+
ok: false,
|
|
1189
|
+
conflicts: [{ baseRange: range2(0, B.length), oursRange: range2(0, O.length), theirsRange: range2(0, T.length), base, ours, theirs }]
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
const all = [
|
|
1193
|
+
...changes(B, O).map((c) => ({ ...c, side: "o" })),
|
|
1194
|
+
...changes(B, T).map((c) => ({ ...c, side: "t" }))
|
|
1195
|
+
].sort((a, b) => a.bs - b.bs || a.be - b.be || (a.side < b.side ? -1 : 1));
|
|
1196
|
+
const out = [];
|
|
1197
|
+
const conflicts = [];
|
|
1198
|
+
let unioned = false;
|
|
1199
|
+
let pos = 0;
|
|
1200
|
+
let offO = 0;
|
|
1201
|
+
let offT = 0;
|
|
1202
|
+
for (let k = 0; k < all.length; ) {
|
|
1203
|
+
const first = all[k];
|
|
1204
|
+
const group = [first];
|
|
1205
|
+
let lo = first.bs;
|
|
1206
|
+
let hi = first.be;
|
|
1207
|
+
k++;
|
|
1208
|
+
while (k < all.length && all[k].bs <= hi) {
|
|
1209
|
+
const c = all[k];
|
|
1210
|
+
group.push(c);
|
|
1211
|
+
lo = Math.min(lo, c.bs);
|
|
1212
|
+
hi = Math.max(hi, c.be);
|
|
1213
|
+
k++;
|
|
1214
|
+
}
|
|
1215
|
+
out.push(...B.slice(pos, lo));
|
|
1216
|
+
pos = hi;
|
|
1217
|
+
const os = group.filter((c) => c.side === "o");
|
|
1218
|
+
const ts = group.filter((c) => c.side === "t");
|
|
1219
|
+
const delta = (cs) => cs.reduce((n, c) => n + (c.se - c.ss) - (c.be - c.bs), 0);
|
|
1220
|
+
const oStart = lo + offO;
|
|
1221
|
+
const oEnd = hi + offO + delta(os);
|
|
1222
|
+
const tStart = lo + offT;
|
|
1223
|
+
const tEnd = hi + offT + delta(ts);
|
|
1224
|
+
offO += delta(os);
|
|
1225
|
+
offT += delta(ts);
|
|
1226
|
+
const oLines = O.slice(oStart, oEnd);
|
|
1227
|
+
const tLines = T.slice(tStart, tEnd);
|
|
1228
|
+
if (!ts.length) {
|
|
1229
|
+
out.push(...oLines);
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
if (!os.length) {
|
|
1233
|
+
out.push(...tLines);
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
if (join(oLines) === join(tLines)) {
|
|
1237
|
+
out.push(...oLines);
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
if (group.every((c) => c.bs === c.be && c.bs === lo)) {
|
|
1241
|
+
out.push(...oLines, ...tLines);
|
|
1242
|
+
unioned = true;
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
const words = mergeInsertedTokens(join(B.slice(lo, hi)), join(oLines), join(tLines));
|
|
1246
|
+
if (words !== null) {
|
|
1247
|
+
out.push(words);
|
|
1248
|
+
unioned = true;
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
conflicts.push({
|
|
1252
|
+
baseRange: range2(lo, hi),
|
|
1253
|
+
oursRange: range2(oStart, oEnd),
|
|
1254
|
+
theirsRange: range2(tStart, tEnd),
|
|
1255
|
+
base: join(B.slice(lo, hi)),
|
|
1256
|
+
ours: join(oLines),
|
|
1257
|
+
theirs: join(tLines)
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
if (conflicts.length) return { ok: false, conflicts };
|
|
1261
|
+
out.push(...B.slice(pos));
|
|
1262
|
+
return { ok: true, text: join(out), unioned };
|
|
1263
|
+
}
|
|
1264
|
+
var same = (a, b) => a === void 0 || b === void 0 ? a === b : bytesEqual(a, b);
|
|
1265
|
+
function mergeTrees(base, ours, theirs) {
|
|
1266
|
+
const out = new Map(ours);
|
|
1267
|
+
const conflicts = [];
|
|
1268
|
+
const changed = [];
|
|
1269
|
+
let unioned = false;
|
|
1270
|
+
const set = (p, v) => {
|
|
1271
|
+
if (same(ours.get(p), v)) return;
|
|
1272
|
+
if (v === void 0) out.delete(p);
|
|
1273
|
+
else out.set(p, v);
|
|
1274
|
+
changed.push(p);
|
|
1275
|
+
};
|
|
1276
|
+
for (const p of sortPaths(/* @__PURE__ */ new Set([...base.keys(), ...ours.keys(), ...theirs.keys()]))) {
|
|
1277
|
+
const b = base.get(p);
|
|
1278
|
+
const o = ours.get(p);
|
|
1279
|
+
const t = theirs.get(p);
|
|
1280
|
+
if (same(o, t) || same(t, b)) continue;
|
|
1281
|
+
if (same(o, b)) {
|
|
1282
|
+
set(p, t);
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
const fail = (reason, lines2 = []) => conflicts.push({ path: p, reason, conflicts: lines2 });
|
|
1286
|
+
if (b === void 0) {
|
|
1287
|
+
fail(`${p} was added at the tip and by this push with different content`);
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
if (o === void 0) {
|
|
1291
|
+
fail(`${p} was deleted at the tip and changed by this push`);
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
if (t === void 0) {
|
|
1295
|
+
fail(`${p} was changed at the tip and deleted by this push`);
|
|
1296
|
+
continue;
|
|
1297
|
+
}
|
|
1298
|
+
const tb = asText(b);
|
|
1299
|
+
const to = asText(o);
|
|
1300
|
+
const tt = asText(t);
|
|
1301
|
+
if (tb === null || to === null || tt === null) {
|
|
1302
|
+
fail(`${p} is binary and was changed both at the tip and by this push`);
|
|
1303
|
+
continue;
|
|
1304
|
+
}
|
|
1305
|
+
const m = merge3(tb, to, tt);
|
|
1306
|
+
if (m.ok) {
|
|
1307
|
+
unioned ||= m.unioned;
|
|
1308
|
+
set(p, new TextEncoder().encode(m.text));
|
|
1309
|
+
} else fail(`${p} was changed at the tip and by this push in the same lines`, m.conflicts);
|
|
1310
|
+
}
|
|
1311
|
+
const folded = /* @__PURE__ */ new Map();
|
|
1312
|
+
for (const p of sortPaths(out.keys())) {
|
|
1313
|
+
const key = foldKey(p);
|
|
1314
|
+
const other = folded.get(key);
|
|
1315
|
+
if (other !== void 0 && (!ours.has(p) || !ours.has(other))) {
|
|
1316
|
+
conflicts.push({ path: p, reason: `${p} and ${other} differ only by case or Unicode normalisation`, conflicts: [] });
|
|
1317
|
+
}
|
|
1318
|
+
folded.set(key, p);
|
|
1319
|
+
}
|
|
1320
|
+
if (conflicts.length) return { ok: false, conflicts };
|
|
1321
|
+
return { ok: true, tree: out, changed: sortPaths(changed), unioned };
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// ../core/src/tree.ts
|
|
1325
|
+
var MAX_UNPACKED_BYTES = 50 * 1024 * 1024;
|
|
1326
|
+
var MAX_TAR_BYTES = MAX_UNPACKED_BYTES + 16 * 1024 * 1024;
|
|
1327
|
+
function octal(n, width) {
|
|
1328
|
+
return n.toString(8).padStart(width - 1, "0") + "\0";
|
|
1329
|
+
}
|
|
1330
|
+
function header(name, prefix, size, type) {
|
|
1331
|
+
const h = new Uint8Array(512);
|
|
1332
|
+
const put = (s, off) => h.set(typeof s === "string" ? utf8(s) : s, off);
|
|
1333
|
+
put(name, 0);
|
|
1334
|
+
put(octal(420, 8), 100);
|
|
1335
|
+
put(octal(0, 8), 108);
|
|
1336
|
+
put(octal(0, 8), 116);
|
|
1337
|
+
put(octal(size, 12), 124);
|
|
1338
|
+
put(octal(0, 12), 136);
|
|
1339
|
+
put(" ", 148);
|
|
1340
|
+
put(type, 156);
|
|
1341
|
+
put("ustar\0", 257);
|
|
1342
|
+
put("00", 263);
|
|
1343
|
+
put(prefix, 345);
|
|
1344
|
+
let sum = 0;
|
|
1345
|
+
for (const b of h) sum += b;
|
|
1346
|
+
put(sum.toString(8).padStart(6, "0") + "\0 ", 148);
|
|
1347
|
+
return h;
|
|
1348
|
+
}
|
|
1349
|
+
function pad(n) {
|
|
1350
|
+
return new Uint8Array((512 - n % 512) % 512);
|
|
1351
|
+
}
|
|
1352
|
+
function splitUstarName(path) {
|
|
1353
|
+
if (path.length <= 100) return [path, new Uint8Array(0)];
|
|
1354
|
+
for (let i = path.length - 1; i > 0; i--) {
|
|
1355
|
+
if (path[i] !== 47) continue;
|
|
1356
|
+
if (i <= 155 && path.length - i - 1 <= 100) return [path.subarray(i + 1), path.subarray(0, i)];
|
|
1357
|
+
}
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1360
|
+
function paxRecord(key, value) {
|
|
1361
|
+
const body = ` ${key}=${value}
|
|
1362
|
+
`;
|
|
1363
|
+
const bodyLen = utf8(body).length;
|
|
1364
|
+
let len = bodyLen + String(bodyLen).length;
|
|
1365
|
+
if (String(len).length !== String(bodyLen).length) len = bodyLen + String(len).length;
|
|
1366
|
+
return utf8(`${len}${body}`);
|
|
1367
|
+
}
|
|
1368
|
+
function packTarGz(t) {
|
|
1369
|
+
const parts = [];
|
|
1370
|
+
for (const path of sortPaths(t.keys())) {
|
|
1371
|
+
const data = t.get(path);
|
|
1372
|
+
const name = utf8(path);
|
|
1373
|
+
const split = splitUstarName(name);
|
|
1374
|
+
if (split) {
|
|
1375
|
+
parts.push(header(split[0], split[1], data.length, "0"));
|
|
1376
|
+
} else {
|
|
1377
|
+
const pax = paxRecord("path", path);
|
|
1378
|
+
parts.push(header(utf8("PaxHeader"), new Uint8Array(0), pax.length, "x"), pax, pad(pax.length));
|
|
1379
|
+
parts.push(header(name.subarray(name.length - 100), new Uint8Array(0), data.length, "0"));
|
|
1380
|
+
}
|
|
1381
|
+
parts.push(data, pad(data.length));
|
|
1382
|
+
}
|
|
1383
|
+
parts.push(new Uint8Array(1024));
|
|
1384
|
+
return gzipSync(concat(parts), { mtime: 0 });
|
|
1385
|
+
}
|
|
1386
|
+
var TarError = class extends Error {
|
|
1387
|
+
constructor(message) {
|
|
1388
|
+
super(message);
|
|
1389
|
+
this.name = "TarError";
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
function cstr(b) {
|
|
1393
|
+
const z = b.indexOf(0);
|
|
1394
|
+
return z === -1 ? b : b.subarray(0, z);
|
|
1395
|
+
}
|
|
1396
|
+
function readOctal(b) {
|
|
1397
|
+
const s = fromUtf8(cstr(b)).trim();
|
|
1398
|
+
if (!/^[0-7]*$/.test(s)) throw new TarError("bad number in tar header");
|
|
1399
|
+
return s ? parseInt(s, 8) : 0;
|
|
1400
|
+
}
|
|
1401
|
+
function parsePax(b) {
|
|
1402
|
+
const out = {};
|
|
1403
|
+
let i = 0;
|
|
1404
|
+
while (i < b.length) {
|
|
1405
|
+
const sp = b.indexOf(32, i);
|
|
1406
|
+
if (sp === -1) break;
|
|
1407
|
+
const len = parseInt(fromUtf8(b.subarray(i, sp)), 10);
|
|
1408
|
+
if (!(len > 0) || i + len > b.length) throw new TarError("bad pax record");
|
|
1409
|
+
const rec = fromUtf8(b.subarray(sp + 1, i + len - 1));
|
|
1410
|
+
const eq = rec.indexOf("=");
|
|
1411
|
+
if (eq > 0) out[rec.slice(0, eq)] = rec.slice(eq + 1);
|
|
1412
|
+
i += len;
|
|
1413
|
+
}
|
|
1414
|
+
return out;
|
|
1415
|
+
}
|
|
1416
|
+
function safePath(raw) {
|
|
1417
|
+
let p = raw;
|
|
1418
|
+
while (p.startsWith("./")) p = p.slice(2);
|
|
1419
|
+
if (!p || p.startsWith("/") || /[\x00-\x1f\x7f\\]/.test(p)) throw new TarError(`unsafe path in snapshot: ${JSON.stringify(raw)}`);
|
|
1420
|
+
const parts = p.split("/");
|
|
1421
|
+
if (parts.some((c) => c === "" || c === "." || c === "..")) throw new TarError(`unsafe path in snapshot: ${JSON.stringify(raw)}`);
|
|
1422
|
+
return p;
|
|
1423
|
+
}
|
|
1424
|
+
function gunzipCapped(b) {
|
|
1425
|
+
const chunks = [];
|
|
1426
|
+
let total = 0;
|
|
1427
|
+
const g = new Gunzip((chunk) => {
|
|
1428
|
+
total += chunk.length;
|
|
1429
|
+
if (total > MAX_TAR_BYTES) throw new TarError("snapshot is larger than 50 MB");
|
|
1430
|
+
chunks.push(chunk);
|
|
1431
|
+
});
|
|
1432
|
+
for (let i = 0; i < b.length; i += 65536) g.push(b.subarray(i, i + 65536), i + 65536 >= b.length);
|
|
1433
|
+
if (b.length === 0) throw new TarError("empty snapshot");
|
|
1434
|
+
return concat(chunks);
|
|
1435
|
+
}
|
|
1436
|
+
function unpackTarGz(b) {
|
|
1437
|
+
const tar = gunzipCapped(b);
|
|
1438
|
+
const tree = /* @__PURE__ */ new Map();
|
|
1439
|
+
let total = 0;
|
|
1440
|
+
let off = 0;
|
|
1441
|
+
let nextPath = null;
|
|
1442
|
+
while (off + 512 <= tar.length) {
|
|
1443
|
+
const h = tar.subarray(off, off + 512);
|
|
1444
|
+
if (h.every((x) => x === 0)) break;
|
|
1445
|
+
let sum = 0;
|
|
1446
|
+
for (let i = 0; i < 512; i++) sum += i >= 148 && i < 156 ? 32 : h[i];
|
|
1447
|
+
if (sum !== readOctal(h.subarray(148, 156))) throw new TarError("bad tar header checksum");
|
|
1448
|
+
const size = readOctal(h.subarray(124, 136));
|
|
1449
|
+
const type = String.fromCharCode(h[156]);
|
|
1450
|
+
const dataStart = off + 512;
|
|
1451
|
+
const dataEnd = dataStart + size;
|
|
1452
|
+
if (dataEnd > tar.length) throw new TarError("truncated tar");
|
|
1453
|
+
const data = tar.subarray(dataStart, dataEnd);
|
|
1454
|
+
off = dataStart + Math.ceil(size / 512) * 512;
|
|
1455
|
+
if (type === "x") {
|
|
1456
|
+
const pax = parsePax(data);
|
|
1457
|
+
if (pax.path !== void 0) nextPath = pax.path;
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
if (type === "g") continue;
|
|
1461
|
+
if (type === "L") {
|
|
1462
|
+
nextPath = fromUtf8(cstr(data));
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
let name = fromUtf8(cstr(h.subarray(0, 100)));
|
|
1466
|
+
const magic = fromUtf8(h.subarray(257, 262));
|
|
1467
|
+
const prefix = magic === "ustar" ? fromUtf8(cstr(h.subarray(345, 500))) : "";
|
|
1468
|
+
if (prefix) name = `${prefix}/${name}`;
|
|
1469
|
+
if (nextPath !== null) name = nextPath;
|
|
1470
|
+
nextPath = null;
|
|
1471
|
+
if (type === "5") {
|
|
1472
|
+
safePath(name.replace(/\/+$/, ""));
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
if (type === "1" || type === "2") throw new TarError(`links are not allowed in snapshots: ${name}`);
|
|
1476
|
+
if (type !== "0" && type !== "\0" && type !== "7") throw new TarError(`unsupported tar entry type ${JSON.stringify(type)}: ${name}`);
|
|
1477
|
+
const path = safePath(name);
|
|
1478
|
+
if (tree.has(path)) throw new TarError(`duplicate path in snapshot: ${path}`);
|
|
1479
|
+
total += size;
|
|
1480
|
+
if (total > MAX_UNPACKED_BYTES) throw new TarError("snapshot is larger than 50 MB");
|
|
1481
|
+
tree.set(path, data.slice());
|
|
1482
|
+
}
|
|
1483
|
+
return tree;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// ../core/src/settings.ts
|
|
1487
|
+
var CONFIRM_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1488
|
+
|
|
1489
|
+
// ../core/src/webhook.ts
|
|
1490
|
+
var enc2 = new TextEncoder();
|
|
1491
|
+
var MAX_AGE_SECONDS = 24 * 60 * 60;
|
|
1492
|
+
|
|
1493
|
+
// src/main.ts
|
|
1494
|
+
import { chmodSync, copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync5, realpathSync as realpathSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
1495
|
+
import { homedir as homedir3 } from "node:os";
|
|
1496
|
+
import { basename, delimiter, join as join9, resolve as resolve3 } from "node:path";
|
|
1497
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1498
|
+
|
|
1499
|
+
// src/address.ts
|
|
1500
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1501
|
+
import { homedir } from "node:os";
|
|
1502
|
+
import { dirname as dirname2, join as join5 } from "node:path";
|
|
1503
|
+
|
|
1504
|
+
// src/transport.ts
|
|
1505
|
+
import { pathToFileURL } from "node:url";
|
|
1506
|
+
|
|
1507
|
+
// ../transports/primitive/src/index.ts
|
|
1508
|
+
import { spawn } from "node:child_process";
|
|
1509
|
+
import { randomBytes } from "node:crypto";
|
|
1510
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
1511
|
+
import { tmpdir } from "node:os";
|
|
1512
|
+
import { join as join2 } from "node:path";
|
|
1513
|
+
import { userInfo } from "node:os";
|
|
1514
|
+
var TransportError = class extends Error {
|
|
1515
|
+
name = "TransportError";
|
|
1516
|
+
};
|
|
1517
|
+
var MapError = TransportError;
|
|
1518
|
+
function run(cmd, args, o = {}) {
|
|
1519
|
+
return new Promise((resolve4, reject) => {
|
|
1520
|
+
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1521
|
+
const out = [];
|
|
1522
|
+
const err2 = [];
|
|
1523
|
+
const timer = o.timeoutMs ? setTimeout(() => child.kill("SIGTERM"), o.timeoutMs) : void 0;
|
|
1524
|
+
child.stdout.on("data", (b) => out.push(b));
|
|
1525
|
+
child.stderr.on("data", (b) => err2.push(b));
|
|
1526
|
+
child.on("error", (e) => {
|
|
1527
|
+
if (timer) clearTimeout(timer);
|
|
1528
|
+
reject(e.code === "ENOENT" ? new TransportError(`${cmd} was not found on PATH (install it with npm install -g primitive)`) : e);
|
|
1529
|
+
});
|
|
1530
|
+
child.on("close", (code) => {
|
|
1531
|
+
if (timer) clearTimeout(timer);
|
|
1532
|
+
resolve4({ code: code ?? 1, out: Buffer.concat(out).toString("utf8"), err: Buffer.concat(err2).toString("utf8") });
|
|
1533
|
+
});
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
var scratch;
|
|
1537
|
+
function tempDir() {
|
|
1538
|
+
if (!scratch) {
|
|
1539
|
+
const dir = mkdtempSync(join2(tmpdir(), "map-primitive-"));
|
|
1540
|
+
scratch = dir;
|
|
1541
|
+
process.on("exit", () => rmSync(dir, { recursive: true, force: true }));
|
|
1542
|
+
}
|
|
1543
|
+
return scratch;
|
|
1544
|
+
}
|
|
1545
|
+
function bin() {
|
|
1546
|
+
return process.env.MAP_PRIMITIVE_BIN || "primitive";
|
|
1547
|
+
}
|
|
1548
|
+
function agentProfile() {
|
|
1549
|
+
return process.env.PRIMITIVE_AGENT_PROFILE || void 0;
|
|
1550
|
+
}
|
|
1551
|
+
function summary(x) {
|
|
1552
|
+
return {
|
|
1553
|
+
id: String(x?.id ?? ""),
|
|
1554
|
+
subject: String(x?.subject ?? ""),
|
|
1555
|
+
sender: String(x?.from_email ?? x?.sender ?? "").toLowerCase(),
|
|
1556
|
+
recipient: x?.recipient ? String(x.recipient).toLowerCase() : void 0,
|
|
1557
|
+
received_at: String(x?.received_at ?? x?.created_at ?? ""),
|
|
1558
|
+
awaiting: x?.awaiting ?? void 0,
|
|
1559
|
+
thread_id: x?.thread_id ?? void 0
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
function jsonLines(out) {
|
|
1563
|
+
const xs = [];
|
|
1564
|
+
for (const line of out.split("\n")) {
|
|
1565
|
+
const t = line.trim();
|
|
1566
|
+
if (!t.startsWith("{")) continue;
|
|
1567
|
+
try {
|
|
1568
|
+
xs.push(JSON.parse(t));
|
|
1569
|
+
} catch {
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
return xs;
|
|
1573
|
+
}
|
|
1574
|
+
var seq = 0;
|
|
1575
|
+
function bodyFile(body) {
|
|
1576
|
+
const f = join2(tempDir(), `body-${process.pid}-${++seq}.txt`);
|
|
1577
|
+
writeFileSync(f, body);
|
|
1578
|
+
return f;
|
|
1579
|
+
}
|
|
1580
|
+
async function cli(args, timeoutMs = 12e4) {
|
|
1581
|
+
const r = await run(bin(), args, { timeoutMs });
|
|
1582
|
+
if (r.code !== 0) {
|
|
1583
|
+
const why = (r.err.trim() || r.out.trim()).split("\n").slice(-6).join("\n");
|
|
1584
|
+
throw new MapError(`primitive ${args.slice(0, 2).join(" ")} failed: ${why}`);
|
|
1585
|
+
}
|
|
1586
|
+
return r.out;
|
|
1587
|
+
}
|
|
1588
|
+
var sentSearch = null;
|
|
1589
|
+
function searchesSentMail() {
|
|
1590
|
+
sentSearch ??= run(bin(), ["emails", "search", "--help"], { timeoutMs: 6e4 }).then((r) => r.code === 0 && /--direction/.test(r.out)).catch(() => false);
|
|
1591
|
+
return sentSearch;
|
|
1592
|
+
}
|
|
1593
|
+
function primitiveTransport() {
|
|
1594
|
+
return {
|
|
1595
|
+
name: "primitive",
|
|
1596
|
+
async send(m) {
|
|
1597
|
+
const from = m.fromName ? `"${m.fromName.replace(/["\\]/g, "\\$&")}" <${m.from}>` : m.from;
|
|
1598
|
+
const args = ["send", "--from", from, "--to", m.to, "--subject", m.subject, "--body-file", bodyFile(m.body)];
|
|
1599
|
+
for (const a of m.attachments ?? []) args.push("--attachment", a);
|
|
1600
|
+
args.push("--idempotency-key", `map-cli-${randomBytes(12).toString("hex")}`, "--json");
|
|
1601
|
+
const out = await cli(args);
|
|
1602
|
+
let d;
|
|
1603
|
+
try {
|
|
1604
|
+
d = JSON.parse(out);
|
|
1605
|
+
} catch {
|
|
1606
|
+
throw new MapError(`primitive send printed something unexpected: ${out.slice(0, 300)}`);
|
|
1607
|
+
}
|
|
1608
|
+
if (d?.error) throw new MapError(`primitive send failed: ${JSON.stringify(d.error).slice(0, 300)}`);
|
|
1609
|
+
const id = d?.sent_email_id ?? d?.sent?.id ?? d?.id;
|
|
1610
|
+
if (typeof id !== "string" || !id) throw new MapError(`primitive send did not report a sent email id: ${out.slice(0, 300)}`);
|
|
1611
|
+
return id;
|
|
1612
|
+
},
|
|
1613
|
+
async waitReply(sentId, o) {
|
|
1614
|
+
const args = ["emails", "wait", "--reply-to-sent-email-id", sentId, "--from", o.from, "--include-existing", "--timeout", String(Math.max(1, Math.round(o.timeoutSec)))];
|
|
1615
|
+
if (!agentProfile()) args.push("--to", o.me);
|
|
1616
|
+
if (o.since) args.push("--since", o.since);
|
|
1617
|
+
const r = await run(bin(), args, { timeoutMs: (o.timeoutSec + 60) * 1e3 });
|
|
1618
|
+
const hits = jsonLines(r.out).map(summary).filter((x) => x.id);
|
|
1619
|
+
if (hits.length) return hits[0];
|
|
1620
|
+
if (r.code === 0 || /timed out/i.test(r.err + r.out)) return null;
|
|
1621
|
+
throw new MapError(`primitive emails wait failed: ${(r.err.trim() || r.out.trim()).slice(-400)}`);
|
|
1622
|
+
},
|
|
1623
|
+
async get(id) {
|
|
1624
|
+
const d = JSON.parse(await cli(["emails", "get", "--id", id, "--json"]));
|
|
1625
|
+
const atts = d?.parsed?.attachments ?? d?.attachments ?? [];
|
|
1626
|
+
const detail = {
|
|
1627
|
+
...summary(d),
|
|
1628
|
+
body_text: String(d?.body_text ?? d?.parsed?.body_text ?? ""),
|
|
1629
|
+
attachments: atts.map((a, i) => ({ filename: String(a?.filename ?? ""), part_index: Number(a?.part_index ?? i) })),
|
|
1630
|
+
message_id: d?.message_id ?? void 0,
|
|
1631
|
+
in_reply_to: d?.parsed?.in_reply_to ?? void 0,
|
|
1632
|
+
references: d?.parsed?.references ?? void 0,
|
|
1633
|
+
from_header: d?.from_header ?? void 0
|
|
1634
|
+
};
|
|
1635
|
+
return detail;
|
|
1636
|
+
},
|
|
1637
|
+
async download(id, part, out) {
|
|
1638
|
+
await cli(["emails", "download-email-attachment-part", "--id", id, "--part-index", String(part), "--output", out]);
|
|
1639
|
+
return new Uint8Array(readFileSync(out));
|
|
1640
|
+
},
|
|
1641
|
+
async reply(id, m) {
|
|
1642
|
+
await cli(["reply", "--id", id, "--from", m.from, "--body-file", bodyFile(m.body)]);
|
|
1643
|
+
},
|
|
1644
|
+
async list(o) {
|
|
1645
|
+
const args = ["emails", "list", "--envelope", "--json", "--limit", String(o.limit ?? 100)];
|
|
1646
|
+
if (!agentProfile()) args.push("--recipient", o.me);
|
|
1647
|
+
if (o.after) args.push("--date-from", o.after);
|
|
1648
|
+
if (o.cursor) args.push("--cursor", o.cursor);
|
|
1649
|
+
const d = JSON.parse(await cli(args));
|
|
1650
|
+
const rows = Array.isArray(d) ? d : d?.data ?? [];
|
|
1651
|
+
return { data: rows.map(summary), cursor: Array.isArray(d) ? null : d?.meta?.cursor ?? null };
|
|
1652
|
+
},
|
|
1653
|
+
async tail(o) {
|
|
1654
|
+
const cursor = o.cursor ?? `${new Date(o.from ?? Date.now()).toISOString()}|00000000-0000-0000-0000-000000000000`;
|
|
1655
|
+
const args = ["emails", "list", "--envelope", "--json", "--limit", String(o.limit ?? 100), "--since", cursor];
|
|
1656
|
+
if (!agentProfile()) args.push("--recipient", o.me);
|
|
1657
|
+
if (o.wait) args.push("--wait", String(Math.min(60, Math.max(1, Math.round(o.wait)))));
|
|
1658
|
+
const d = JSON.parse(await cli(args, ((o.wait ?? 0) + 120) * 1e3));
|
|
1659
|
+
const rows = d?.data ?? [];
|
|
1660
|
+
return { data: rows.map(summary), cursor: d?.meta?.cursor ?? cursor };
|
|
1661
|
+
},
|
|
1662
|
+
async describe() {
|
|
1663
|
+
const r = await run(bin(), ["--version"], { timeoutMs: 3e4 }).catch(() => null);
|
|
1664
|
+
return { provider: "Primitive (primitive.dev)", version: r?.out.trim().split("\n")[0] || void 0 };
|
|
1665
|
+
},
|
|
1666
|
+
async setup() {
|
|
1667
|
+
const login = { why: "log in to Primitive (opens a browser, or pass your email for a code)", command: ["primitive", "login"], interactive: true };
|
|
1668
|
+
const version = await run(bin(), ["--version"], { timeoutMs: 3e4 }).catch(() => null);
|
|
1669
|
+
if (!version) {
|
|
1670
|
+
return { ready: false, steps: [{ why: "install the primitive CLI", command: ["npm", "install", "-g", "primitive"], interactive: false }, login] };
|
|
1671
|
+
}
|
|
1672
|
+
const profile = agentProfile();
|
|
1673
|
+
if (profile) {
|
|
1674
|
+
const address = await this.defaultAddress().catch(() => null);
|
|
1675
|
+
return address ? { ready: true, steps: [], account: `connected agent (profile ${profile})`, address } : { ready: false, steps: [{ why: `reconnect the agent profile ${profile}`, command: ["primitive", "agent", "connect", "--profile", profile, "--status"], interactive: false }] };
|
|
1676
|
+
}
|
|
1677
|
+
const who = await run(bin(), ["whoami", "--json"], { timeoutMs: 6e4 }).catch(() => null);
|
|
1678
|
+
let d = null;
|
|
1679
|
+
try {
|
|
1680
|
+
d = who && who.code === 0 ? JSON.parse(who.out) : null;
|
|
1681
|
+
} catch {
|
|
1682
|
+
d = null;
|
|
1683
|
+
}
|
|
1684
|
+
if (!d) return { ready: false, steps: [login] };
|
|
1685
|
+
const domain = typeof d.managed_inbox_domain === "string" ? d.managed_inbox_domain : "";
|
|
1686
|
+
const local = (userInfo().username || "agent").toLowerCase().replace(/[^a-z0-9._-]/g, "") || "agent";
|
|
1687
|
+
return { ready: true, steps: [], account: typeof d.email === "string" ? d.email : void 0, suggestedAddress: domain ? `${local}@${domain}` : void 0 };
|
|
1688
|
+
},
|
|
1689
|
+
async search(o) {
|
|
1690
|
+
if (o.direction && o.direction !== "inbound" && !await searchesSentMail()) return { data: [], cursor: null, unsupported: true };
|
|
1691
|
+
const shown2 = [];
|
|
1692
|
+
const add = (flag, v) => {
|
|
1693
|
+
if (v === void 0 || v === "") return;
|
|
1694
|
+
shown2.push(flag, String(v));
|
|
1695
|
+
};
|
|
1696
|
+
add("--q", o.q);
|
|
1697
|
+
add("--from", o.from);
|
|
1698
|
+
add("--to", o.to);
|
|
1699
|
+
add("--subject", o.subject);
|
|
1700
|
+
add("--body", o.body);
|
|
1701
|
+
add("--thread-id", o.threadId);
|
|
1702
|
+
if (o.direction && o.direction !== "inbound") add("--direction", o.direction);
|
|
1703
|
+
add("--sort", o.sort ?? "received_at_desc");
|
|
1704
|
+
add("--limit", o.limit ?? 50);
|
|
1705
|
+
const args = ["emails", "search", ...shown2, "--json", "--envelope", "--snippet", "false"];
|
|
1706
|
+
if (o.cursor) args.push("--cursor", o.cursor);
|
|
1707
|
+
const d = JSON.parse(await cli(args));
|
|
1708
|
+
const rows = Array.isArray(d) ? d : d?.data ?? [];
|
|
1709
|
+
const quote = (x) => /^[\w@.:=-]+$/.test(x) ? x : `'${x.replace(/'/g, "'\\''")}'`;
|
|
1710
|
+
return {
|
|
1711
|
+
data: rows.map((x) => ({ ...summary(x), snippet: typeof x?.snippet === "string" ? x.snippet : void 0 })),
|
|
1712
|
+
cursor: Array.isArray(d) ? null : d?.meta?.cursor ?? null,
|
|
1713
|
+
command: `primitive emails search ${shown2.map(quote).join(" ")}`
|
|
1714
|
+
};
|
|
1715
|
+
},
|
|
1716
|
+
// A connected agent can send as no other address. Primitive's session hook
|
|
1717
|
+
// exports it as PRIMITIVE_AGENT_ADDRESS; otherwise ask the selected profile.
|
|
1718
|
+
async boundAddress() {
|
|
1719
|
+
const exported = process.env.PRIMITIVE_AGENT_ADDRESS?.trim();
|
|
1720
|
+
if (exported && agentProfile()) return exported;
|
|
1721
|
+
return agentProfile() ? await this.defaultAddress() : null;
|
|
1722
|
+
},
|
|
1723
|
+
async defaultAddress() {
|
|
1724
|
+
const profile = agentProfile();
|
|
1725
|
+
if (!profile) return null;
|
|
1726
|
+
const r = await run(bin(), ["agent", "connect", "--profile", profile, "--status", "--json"], { timeoutMs: 6e4 }).catch(() => null);
|
|
1727
|
+
try {
|
|
1728
|
+
const d = JSON.parse(r?.out ?? "");
|
|
1729
|
+
const a = d?.identity?.agentAddress ?? d?.agentAddress ?? d?.address;
|
|
1730
|
+
if (typeof a === "string" && a) return a;
|
|
1731
|
+
} catch {
|
|
1732
|
+
}
|
|
1733
|
+
throw new MapError(`could not read the address of the primitive agent profile ${profile}. Pass --from <your address> or set MAP_FROM.`);
|
|
1734
|
+
}
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// src/util.ts
|
|
1739
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1740
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync2 } from "node:fs";
|
|
1741
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
1742
|
+
import { join as join3 } from "node:path";
|
|
1743
|
+
var MapError2 = class extends Error {
|
|
1744
|
+
constructor(message, code = 1) {
|
|
1745
|
+
super(message);
|
|
1746
|
+
this.code = code;
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
function adoptLegacyEnv(env = process.env) {
|
|
1750
|
+
for (const [k, v] of Object.entries(env)) {
|
|
1751
|
+
if (k.startsWith("GUD_") && !env[`MAP_${k.slice(4)}`]) env[`MAP_${k.slice(4)}`] = v;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
function run2(cmd, args, o = {}) {
|
|
1755
|
+
return new Promise((resolve4, reject) => {
|
|
1756
|
+
const child = spawn2(cmd, args, { env: o.env ?? process.env, cwd: o.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
1757
|
+
const out = [];
|
|
1758
|
+
const err2 = [];
|
|
1759
|
+
let timer;
|
|
1760
|
+
if (o.timeoutMs) timer = setTimeout(() => child.kill("SIGTERM"), o.timeoutMs);
|
|
1761
|
+
child.stdout.on("data", (b) => out.push(b));
|
|
1762
|
+
child.stderr.on("data", (b) => err2.push(b));
|
|
1763
|
+
child.on("error", (e) => {
|
|
1764
|
+
if (timer) clearTimeout(timer);
|
|
1765
|
+
if (e.code === "ENOENT") reject(new MapError2(`${cmd} was not found on PATH`));
|
|
1766
|
+
else reject(e);
|
|
1767
|
+
});
|
|
1768
|
+
child.on("close", (code) => {
|
|
1769
|
+
if (timer) clearTimeout(timer);
|
|
1770
|
+
const bytes = Buffer.concat(out);
|
|
1771
|
+
resolve4({ code: code ?? 1, out: bytes.toString("utf8"), err: Buffer.concat(err2).toString("utf8"), bytes });
|
|
1772
|
+
});
|
|
1773
|
+
child.stdin.on("error", () => {
|
|
1774
|
+
});
|
|
1775
|
+
child.stdin.end(o.input ?? "");
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
var scratch2;
|
|
1779
|
+
function tempDir2() {
|
|
1780
|
+
if (!scratch2) {
|
|
1781
|
+
scratch2 = mkdtempSync2(join3(tmpdir2(), "map-"));
|
|
1782
|
+
const dir = scratch2;
|
|
1783
|
+
process.on("exit", () => rmSync2(dir, { recursive: true, force: true }));
|
|
1784
|
+
}
|
|
1785
|
+
return scratch2;
|
|
1786
|
+
}
|
|
1787
|
+
function say(line) {
|
|
1788
|
+
process.stderr.write(`${line}
|
|
1789
|
+
`);
|
|
1790
|
+
}
|
|
1791
|
+
function oneLine(s, max2 = 150) {
|
|
1792
|
+
const t = s.replace(/\s+/g, " ").trim();
|
|
1793
|
+
return t.length > max2 ? `${t.slice(0, max2 - 3)}...` : t;
|
|
1794
|
+
}
|
|
1795
|
+
function indent(text2, pad2 = " ") {
|
|
1796
|
+
return text2.replace(/\n+$/, "").split("\n").map((l) => l ? pad2 + l : l).join("\n");
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
// src/transport.ts
|
|
1800
|
+
var BUILT_IN = { primitive: primitiveTransport };
|
|
1801
|
+
var current;
|
|
1802
|
+
function transport() {
|
|
1803
|
+
return current ??= loadTransport(process.env.MAP_TRANSPORT || "primitive");
|
|
1804
|
+
}
|
|
1805
|
+
async function loadTransport(name) {
|
|
1806
|
+
const built = BUILT_IN[name];
|
|
1807
|
+
if (built) return built();
|
|
1808
|
+
if (/[\\/]|\.m?js$/.test(name)) {
|
|
1809
|
+
const mod = await import(pathToFileURL(name).href).catch((e) => {
|
|
1810
|
+
throw new MapError2(`could not load the transport module ${name}: ${e.message}`);
|
|
1811
|
+
});
|
|
1812
|
+
const f = mod?.default ?? mod?.transport;
|
|
1813
|
+
if (typeof f !== "function") throw new MapError2(`${name} must export a default function that returns a transport`);
|
|
1814
|
+
return f();
|
|
1815
|
+
}
|
|
1816
|
+
throw new MapError2(`unknown MAP_TRANSPORT "${name}": use ${Object.keys(BUILT_IN).join(", ")}, or the path of an adapter module`);
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
// src/workdir.ts
|
|
1820
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync as readFileSync2, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1821
|
+
import { dirname, join as join4, relative, resolve, sep } from "node:path";
|
|
1822
|
+
var META_DIR = ".map";
|
|
1823
|
+
var LEGACY_META_DIR = ".gud";
|
|
1824
|
+
function metaDirOf(dir) {
|
|
1825
|
+
if (!existsSync(join4(dir, META_DIR, "meta.json")) && existsSync(join4(dir, LEGACY_META_DIR, "meta.json"))) return LEGACY_META_DIR;
|
|
1826
|
+
return META_DIR;
|
|
1827
|
+
}
|
|
1828
|
+
function metaPath(dir) {
|
|
1829
|
+
return join4(dir, metaDirOf(dir), "meta.json");
|
|
1830
|
+
}
|
|
1831
|
+
function findWorkdir(start = process.cwd()) {
|
|
1832
|
+
let d = resolve(start);
|
|
1833
|
+
for (; ; ) {
|
|
1834
|
+
if (existsSync(metaPath(d))) return d;
|
|
1835
|
+
const up = dirname(d);
|
|
1836
|
+
if (up === d) return null;
|
|
1837
|
+
d = up;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
function readMeta(dir) {
|
|
1841
|
+
try {
|
|
1842
|
+
return JSON.parse(readFileSync2(metaPath(dir), "utf8"));
|
|
1843
|
+
} catch {
|
|
1844
|
+
throw new MapError2(`${dir} is not a map working copy (no ${META_DIR}/meta.json). Make one with: map get <repo> [dir]`);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
function writeMeta(dir, m) {
|
|
1848
|
+
mkdirSync(join4(dir, metaDirOf(dir)), { recursive: true });
|
|
1849
|
+
writeFileSync2(metaPath(dir), `${JSON.stringify(m, null, 2)}
|
|
1850
|
+
`);
|
|
1851
|
+
}
|
|
1852
|
+
function readBase(dir) {
|
|
1853
|
+
return unpackTarGz(new Uint8Array(readFileSync2(join4(dir, metaDirOf(dir), "base.tar.gz"))));
|
|
1854
|
+
}
|
|
1855
|
+
function writeBase(dir, t) {
|
|
1856
|
+
mkdirSync(join4(dir, metaDirOf(dir)), { recursive: true });
|
|
1857
|
+
writeFileSync2(join4(dir, metaDirOf(dir), "base.tar.gz"), packTarGz(t));
|
|
1858
|
+
}
|
|
1859
|
+
function writeSent(dir, sent, t) {
|
|
1860
|
+
mkdirSync(join4(dir, metaDirOf(dir), "sent"), { recursive: true });
|
|
1861
|
+
writeFileSync2(join4(dir, metaDirOf(dir), "sent", `${sent}.tar.gz`), packTarGz(t));
|
|
1862
|
+
}
|
|
1863
|
+
function readSent(dir, sent) {
|
|
1864
|
+
const f = join4(dir, metaDirOf(dir), "sent", `${sent}.tar.gz`);
|
|
1865
|
+
return existsSync(f) ? unpackTarGz(new Uint8Array(readFileSync2(f))) : null;
|
|
1866
|
+
}
|
|
1867
|
+
function dropSent(dir, sent) {
|
|
1868
|
+
rmSync3(join4(dir, metaDirOf(dir), "sent", `${sent}.tar.gz`), { force: true });
|
|
1869
|
+
}
|
|
1870
|
+
function ignoreMatcher(text2) {
|
|
1871
|
+
const rules = [];
|
|
1872
|
+
for (const raw of text2.split(/\r?\n/)) {
|
|
1873
|
+
let p = raw.trim();
|
|
1874
|
+
if (!p || p.startsWith("#") || p.startsWith("!")) continue;
|
|
1875
|
+
const dirOnly = p.endsWith("/");
|
|
1876
|
+
if (dirOnly) p = p.slice(0, -1);
|
|
1877
|
+
const anchored = p.startsWith("/") || p.includes("/");
|
|
1878
|
+
p = p.replace(/^\//, "");
|
|
1879
|
+
const re = p.split("").map((c, i, a) => c === "*" ? a[i + 1] === "*" ? "\0" : a[i - 1] === "*" ? "" : "[^/]*" : c === "?" ? "[^/]" : /[.+^${}()|[\]\\]/.test(c) ? `\\${c}` : c).join("").replace(/\u0000\/?/g, "(?:.*/)?");
|
|
1880
|
+
rules.push({ re: new RegExp(`^${re}$`), dirOnly, anchored });
|
|
1881
|
+
}
|
|
1882
|
+
return (path, isDir) => {
|
|
1883
|
+
const parts = path.split("/");
|
|
1884
|
+
return rules.some((r) => {
|
|
1885
|
+
if (r.dirOnly && !isDir) return false;
|
|
1886
|
+
return r.anchored ? r.re.test(path) : r.re.test(parts[parts.length - 1]);
|
|
1887
|
+
});
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
function readWork(dir, base) {
|
|
1891
|
+
const tree = /* @__PURE__ */ new Map();
|
|
1892
|
+
const skipped = [];
|
|
1893
|
+
const gi = join4(dir, ".gitignore");
|
|
1894
|
+
const ignored = ignoreMatcher(existsSync(gi) ? readFileSync2(gi, "utf8") : "");
|
|
1895
|
+
const tracked = (p) => base.has(p) || [...base.keys()].some((k) => k.startsWith(`${p}/`));
|
|
1896
|
+
const walk = (abs) => {
|
|
1897
|
+
for (const name of readdirSync(abs).sort()) {
|
|
1898
|
+
const full = join4(abs, name);
|
|
1899
|
+
const rel = relative(dir, full).split(sep).join("/");
|
|
1900
|
+
if (rel === META_DIR || rel === LEGACY_META_DIR || rel === ".git") continue;
|
|
1901
|
+
const st = lstatSync(full);
|
|
1902
|
+
if (st.isSymbolicLink()) {
|
|
1903
|
+
skipped.push(`${rel} (symlink)`);
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
if (st.isDirectory()) {
|
|
1907
|
+
if (ignored(rel, true) && !tracked(rel)) continue;
|
|
1908
|
+
walk(full);
|
|
1909
|
+
} else if (st.isFile()) {
|
|
1910
|
+
if (ignored(rel, false) && !base.has(rel)) continue;
|
|
1911
|
+
tree.set(rel, new Uint8Array(readFileSync2(full)));
|
|
1912
|
+
} else skipped.push(`${rel} (not a regular file)`);
|
|
1913
|
+
}
|
|
1914
|
+
};
|
|
1915
|
+
walk(dir);
|
|
1916
|
+
return { tree, skipped };
|
|
1917
|
+
}
|
|
1918
|
+
function writeWork(dir, prev, next) {
|
|
1919
|
+
for (const [p, b] of next) {
|
|
1920
|
+
const cur = prev.get(p);
|
|
1921
|
+
if (cur && cur.length === b.length && Buffer.compare(Buffer.from(cur), Buffer.from(b)) === 0 && existsSync(join4(dir, p))) continue;
|
|
1922
|
+
const full = join4(dir, ...p.split("/"));
|
|
1923
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
1924
|
+
writeFileSync2(full, b);
|
|
1925
|
+
}
|
|
1926
|
+
for (const p of prev.keys()) {
|
|
1927
|
+
if (next.has(p)) continue;
|
|
1928
|
+
rmSync3(join4(dir, ...p.split("/")), { force: true });
|
|
1929
|
+
let d = dirname(join4(dir, ...p.split("/")));
|
|
1930
|
+
while (d.startsWith(dir) && d !== dir) {
|
|
1931
|
+
try {
|
|
1932
|
+
if (readdirSync(d).length) break;
|
|
1933
|
+
rmSync3(d, { recursive: true });
|
|
1934
|
+
} catch {
|
|
1935
|
+
break;
|
|
1936
|
+
}
|
|
1937
|
+
d = dirname(d);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// src/address.ts
|
|
1943
|
+
var REPO_RE = /^[a-z][a-z0-9]{1,30}-[a-z][a-z0-9]{1,30}@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
1944
|
+
function parseRepo(s) {
|
|
1945
|
+
const a = s.trim().replace(/^(?:map|gud):\/\//i, "").replace(/\/+$/, "").toLowerCase();
|
|
1946
|
+
if (!REPO_RE.test(a)) throw new MapError2(`"${s}" is not a map repo address (like owner-repo@map.dev)`);
|
|
1947
|
+
return a;
|
|
1948
|
+
}
|
|
1949
|
+
function isRepoAddress(s) {
|
|
1950
|
+
return REPO_RE.test(s.toLowerCase());
|
|
1951
|
+
}
|
|
1952
|
+
var ADDRESS = /^[^\s@<>"]+@[^\s@<>"]+\.[^\s@<>"]+$/;
|
|
1953
|
+
async function resolveRepo(arg) {
|
|
1954
|
+
if (arg) return parseRepo(arg);
|
|
1955
|
+
const dir = findWorkdir();
|
|
1956
|
+
if (dir) return readMeta(dir).repo;
|
|
1957
|
+
throw new MapError2("no repo given and this folder is not a map working copy. Pass the repo address, like owner-repo@map.dev, or run map get <repo> first.");
|
|
1958
|
+
}
|
|
1959
|
+
function configPath() {
|
|
1960
|
+
return process.env.MAP_CONFIG || join5(process.env.XDG_CONFIG_HOME || join5(homedir(), ".config"), "map", "config.json");
|
|
1961
|
+
}
|
|
1962
|
+
function legacyConfigPath() {
|
|
1963
|
+
return join5(process.env.XDG_CONFIG_HOME || join5(homedir(), ".config"), "gud", "config.json");
|
|
1964
|
+
}
|
|
1965
|
+
function readConfig() {
|
|
1966
|
+
for (const p of process.env.MAP_CONFIG ? [configPath()] : [configPath(), legacyConfigPath()]) {
|
|
1967
|
+
try {
|
|
1968
|
+
return JSON.parse(readFileSync3(p, "utf8"));
|
|
1969
|
+
} catch {
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
return {};
|
|
1973
|
+
}
|
|
1974
|
+
var DEFAULT_HOST = "map.dev";
|
|
1975
|
+
function defaultHost() {
|
|
1976
|
+
return (process.env.MAP_HOST || readConfig().host || DEFAULT_HOST).trim().toLowerCase();
|
|
1977
|
+
}
|
|
1978
|
+
function writeConfig(c) {
|
|
1979
|
+
const p = configPath();
|
|
1980
|
+
mkdirSync2(dirname2(p), { recursive: true });
|
|
1981
|
+
writeFileSync3(p, `${JSON.stringify({ ...readConfig(), ...c }, null, 2)}
|
|
1982
|
+
`);
|
|
1983
|
+
}
|
|
1984
|
+
async function resolveMe(flag) {
|
|
1985
|
+
const pick = (s, where) => {
|
|
1986
|
+
const a = s.trim().toLowerCase();
|
|
1987
|
+
if (!ADDRESS.test(a)) throw new MapError2(`${where} is "${s}", which is not an email address`);
|
|
1988
|
+
return a;
|
|
1989
|
+
};
|
|
1990
|
+
if (flag) return pick(flag, "--from");
|
|
1991
|
+
if (process.env.MAP_FROM) return pick(process.env.MAP_FROM, "MAP_FROM");
|
|
1992
|
+
const t = await transport();
|
|
1993
|
+
const bound = await t.boundAddress?.();
|
|
1994
|
+
if (bound) return pick(bound, `the ${t.name} agent's address`);
|
|
1995
|
+
const dir = findWorkdir();
|
|
1996
|
+
if (dir) {
|
|
1997
|
+
const f = readMeta(dir).from;
|
|
1998
|
+
if (f) return pick(f, `${dir}/.map/meta.json`);
|
|
1999
|
+
}
|
|
2000
|
+
const c = readConfig().from;
|
|
2001
|
+
if (c) return pick(c, configPath());
|
|
2002
|
+
const d = await t.defaultAddress();
|
|
2003
|
+
if (d) return pick(d, `the address of the ${t.name} login`);
|
|
2004
|
+
throw new MapError2(`map does not know your email address. Run map init, pass --from <address>, or set MAP_FROM.`);
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
// src/mail.ts
|
|
2008
|
+
function after(iso) {
|
|
2009
|
+
const t = Date.parse(iso);
|
|
2010
|
+
return Number.isNaN(t) ? iso : new Date(t + 1).toISOString();
|
|
2011
|
+
}
|
|
2012
|
+
var Mail = class {
|
|
2013
|
+
constructor(me, t) {
|
|
2014
|
+
this.me = me;
|
|
2015
|
+
this.t = t ? Promise.resolve(t) : transport();
|
|
2016
|
+
}
|
|
2017
|
+
t;
|
|
2018
|
+
async send(o) {
|
|
2019
|
+
return (await this.t).send({ from: this.me, fromName: o.fromName, to: o.to, subject: o.subject, body: o.body, attachments: o.attachment ? [o.attachment] : [] });
|
|
2020
|
+
}
|
|
2021
|
+
async waitReply(sentId, from, o) {
|
|
2022
|
+
return (await this.t).waitReply(sentId, { me: this.me, from, ...o });
|
|
2023
|
+
}
|
|
2024
|
+
async get(id) {
|
|
2025
|
+
return (await this.t).get(id);
|
|
2026
|
+
}
|
|
2027
|
+
async download(id, part, out) {
|
|
2028
|
+
return (await this.t).download(id, part, out);
|
|
2029
|
+
}
|
|
2030
|
+
async reply(id, body) {
|
|
2031
|
+
return (await this.t).reply(id, { from: this.me, body });
|
|
2032
|
+
}
|
|
2033
|
+
async list(o = {}) {
|
|
2034
|
+
return (await this.t).list({ me: this.me, ...o });
|
|
2035
|
+
}
|
|
2036
|
+
async tail(o = {}) {
|
|
2037
|
+
return (await this.t).tail({ me: this.me, ...o });
|
|
2038
|
+
}
|
|
2039
|
+
};
|
|
2040
|
+
|
|
2041
|
+
// src/replies.ts
|
|
2042
|
+
function parseLog(body) {
|
|
2043
|
+
const tip = /up to the tip (c\d+-[0-9a-f]{8})/.exec(body)?.[1] ?? null;
|
|
2044
|
+
const lines2 = body.replace(/\r\n?/g, "\n").split("\n");
|
|
2045
|
+
const entries = [];
|
|
2046
|
+
for (let i = 0; i < lines2.length; i++) {
|
|
2047
|
+
const m = /^(c\d+-[0-9a-f]{8}) {2}(\d{4}-\d\d-\d\d \d\d:\d\d) {2}(\S+) {2}approved by (.*)$/.exec(lines2[i]);
|
|
2048
|
+
if (!m) continue;
|
|
2049
|
+
const next = lines2[i + 1] ?? "";
|
|
2050
|
+
const mm = /^ {4}(.*?)(?: \(push (p-[0-9a-f]{8})\))?$/.exec(next);
|
|
2051
|
+
const message = mm ? mm[1] : "";
|
|
2052
|
+
entries.push({ id: m[1], at: m[2], author: m[3], approvedBy: m[4], message: message === "(no message)" ? "" : message, pid: mm?.[2] ?? null });
|
|
2053
|
+
}
|
|
2054
|
+
return { tip: tip ?? entries[0]?.id ?? null, entries };
|
|
2055
|
+
}
|
|
2056
|
+
function parseStatus(body) {
|
|
2057
|
+
const tip = /^Status\. Tip: (\S+)/m.exec(body)?.[1] ?? null;
|
|
2058
|
+
const lines2 = body.replace(/\r\n?/g, "\n").split("\n");
|
|
2059
|
+
const pushes = [];
|
|
2060
|
+
for (let i = 0; i < lines2.length; i++) {
|
|
2061
|
+
const m = /^ {2}(p-[0-9a-f]{8}) {2}(.+?) {2}(\S+@\S+) {2}(.+?) old$/.exec(lines2[i]);
|
|
2062
|
+
if (!m) continue;
|
|
2063
|
+
const subject = /^ {4}(.*)$/.exec(lines2[i + 1] ?? "")?.[1] ?? "";
|
|
2064
|
+
pushes.push({ pid: m[1], state: m[2], author: m[3], age: m[4], subject: subject === "(no subject)" ? "" : subject });
|
|
2065
|
+
}
|
|
2066
|
+
return { tip, pushes };
|
|
2067
|
+
}
|
|
2068
|
+
function stripReadHelp(body) {
|
|
2069
|
+
const i = body.indexOf("Read commands: email the repo");
|
|
2070
|
+
return (i === -1 ? body : body.slice(0, i)).replace(/\s+$/, "");
|
|
2071
|
+
}
|
|
2072
|
+
function quotedBlock(body) {
|
|
2073
|
+
const lines2 = body.replace(/\r\n?/g, "\n").split("\n");
|
|
2074
|
+
const out = [];
|
|
2075
|
+
let started = false;
|
|
2076
|
+
for (const l of lines2) {
|
|
2077
|
+
if (l.startsWith(">")) {
|
|
2078
|
+
started = true;
|
|
2079
|
+
out.push(l.replace(/^> ?/, ""));
|
|
2080
|
+
} else if (started) break;
|
|
2081
|
+
}
|
|
2082
|
+
return out.join("\n").trim();
|
|
2083
|
+
}
|
|
2084
|
+
function quotedBlocks(body) {
|
|
2085
|
+
const lines2 = body.replace(/\r\n?/g, "\n").split("\n");
|
|
2086
|
+
const out = [];
|
|
2087
|
+
for (let i = 0; i < lines2.length; i++) {
|
|
2088
|
+
if (!lines2[i].startsWith(">")) continue;
|
|
2089
|
+
const buf = [];
|
|
2090
|
+
let j = i;
|
|
2091
|
+
for (; j < lines2.length && lines2[j].startsWith(">"); j++) buf.push(lines2[j].replace(/^> ?/, ""));
|
|
2092
|
+
let k = i - 1;
|
|
2093
|
+
while (k >= 0 && !lines2[k].trim()) k--;
|
|
2094
|
+
out.push({ label: (lines2[k] ?? "").trim().replace(/:$/, ""), text: buf.join("\n").trim() });
|
|
2095
|
+
i = j;
|
|
2096
|
+
}
|
|
2097
|
+
return out;
|
|
2098
|
+
}
|
|
2099
|
+
function parseVerdict(subject, body) {
|
|
2100
|
+
const s = parseSubject(subject);
|
|
2101
|
+
const b = body.replace(/\r\n?/g, "\n");
|
|
2102
|
+
const pidInBody = /\b(p-[0-9a-f]{8})\b/.exec(b)?.[1];
|
|
2103
|
+
if (!s) return { kind: "other", final: false };
|
|
2104
|
+
switch (s.kind) {
|
|
2105
|
+
case "landed":
|
|
2106
|
+
return { kind: "landed", final: true, cid: s.id, pid: /(?:map|gud) landed push (p-[0-9a-f]{8})/.exec(b)?.[1] ?? pidInBody, merged: /merged your change onto/.test(b) };
|
|
2107
|
+
case "conflict": {
|
|
2108
|
+
const tip = /^Tip commit: (\S+)/m.exec(b)?.[1];
|
|
2109
|
+
let end = b.indexOf("If you still have the repo directory");
|
|
2110
|
+
if (end === -1) end = b.length;
|
|
2111
|
+
const overlaps = b.slice(0, end).split("\n").filter((l) => !/snapshot\.tar\.gz/.test(l) && !/^Nothing was landed\.?$/.test(l.trim())).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
2112
|
+
return { kind: "conflict", final: true, pid: s.id, tip, overlaps };
|
|
2113
|
+
}
|
|
2114
|
+
case "objected": {
|
|
2115
|
+
const lastRound = s.rest === "final" || /the last one/.test(b) || /round 3 of 3/.test(b);
|
|
2116
|
+
const round = Number(/round (\d+) of \d+/.exec(b)?.[1] ?? 0) || void 0;
|
|
2117
|
+
let who = "the reviewer";
|
|
2118
|
+
const owner = /^(\S+@\S+) wrote lines that push/m.exec(b)?.[1];
|
|
2119
|
+
const standing = /the objection of (.+?) still stands/s.exec(b)?.[1];
|
|
2120
|
+
const reviewer = /^The reviewer of (\S+) objected/m.exec(b);
|
|
2121
|
+
if (owner) who = `line owner ${owner}`;
|
|
2122
|
+
else if (standing) who = `line owner${standing.includes(",") ? "s" : ""} ${standing.replace(/\s+/g, " ")}`;
|
|
2123
|
+
else if (reviewer) who = "the reviewer";
|
|
2124
|
+
const blocks = quotedBlocks(b);
|
|
2125
|
+
const reason = blocks.length > 1 && standing ? blocks.map((x) => `${x.label}: ${x.text}`).join("\n") : blocks[0]?.text ?? "";
|
|
2126
|
+
return { kind: "objected", final: true, pid: s.id, round, lastRound: lastRound || s.rest === "final", who, reason: s.rest === "final" && !reason ? "the review is closed after round 3" : reason };
|
|
2127
|
+
}
|
|
2128
|
+
case "rejected":
|
|
2129
|
+
return { kind: "rejected", final: true, pid: s.id, reason: /^Reason: (.*)$/m.exec(b)?.[1] ?? oneLineOf(b) };
|
|
2130
|
+
case "vetoed":
|
|
2131
|
+
return { kind: "vetoed", final: true, pid: s.id, who: /, (\S+@\S+), vetoed push/.exec(b)?.[1], reason: quotedBlock(b) };
|
|
2132
|
+
case "check-failed":
|
|
2133
|
+
return { kind: "check-failed", final: true, pid: s.id, who: /checker of \S+, (\S+@\S+), reported/.exec(b)?.[1], reason: quotedBlock(b) };
|
|
2134
|
+
case "escalated":
|
|
2135
|
+
return { kind: "escalated", final: true, pid: s.id, who: /the objection of (.+?) still stands/s.exec(b)?.[1]?.replace(/\s+/g, " ") };
|
|
2136
|
+
case "not-allowed":
|
|
2137
|
+
return { kind: "not-allowed", final: true, reason: oneLineOf(b) };
|
|
2138
|
+
case "stuck":
|
|
2139
|
+
return { kind: "stuck", final: false, pid: s.id, reason: /^It is waiting on: (.*)$/m.exec(b)?.[1] };
|
|
2140
|
+
default:
|
|
2141
|
+
return { kind: "other", final: false, pid: s.id };
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
function oneLineOf(b) {
|
|
2145
|
+
return b.split("\n").find((l) => l.trim())?.trim() ?? "";
|
|
2146
|
+
}
|
|
2147
|
+
var ACTION_KINDS = {
|
|
2148
|
+
consent: 'a line owner ask: read it with map show <pid>, then map approve <pid> or map object <pid> "<reason>"',
|
|
2149
|
+
objected: 'your push was objected to: read why with map show <pid>, then map argue <pid> "<why>", or change the files and map send again',
|
|
2150
|
+
escalated: 'escalated to the repo owners: owners run map approve <pid> or map veto <pid> "<reason>"',
|
|
2151
|
+
pending: 'owner review window: map veto <pid> "<reason>" to stop it, map approve <pid> to land it now',
|
|
2152
|
+
confirm: "a confirmation request: reply confirm to accept (see the mail)"
|
|
2153
|
+
};
|
|
2154
|
+
var VERDICT_KINDS = /* @__PURE__ */ new Set(["landed", "conflict", "rejected", "vetoed", "check-failed", "stuck", "too-late", "not-allowed", "confirmed"]);
|
|
2155
|
+
function inboxItem(m) {
|
|
2156
|
+
const s = parseSubject(m.subject);
|
|
2157
|
+
if (!s) return null;
|
|
2158
|
+
const ref = s.kind === "confirm" || s.kind === "confirmed" ? null : s.kind === "event" ? s.ref ?? null : s.id ?? null;
|
|
2159
|
+
const base = { id: m.id, kind: s.kind, pid: ref, repo: m.sender, subject: m.subject, at: m.received_at };
|
|
2160
|
+
if (s.kind in ACTION_KINDS) {
|
|
2161
|
+
const final = s.kind === "objected" && s.rest === "final";
|
|
2162
|
+
const replied = m.awaiting === "them";
|
|
2163
|
+
if (final) return { ...base, action: false, hint: "objected, final: change the files and map send again, or ask a repo owner" };
|
|
2164
|
+
return { ...base, action: !replied, hint: replied ? "you already replied" : ACTION_KINDS[s.kind] };
|
|
2165
|
+
}
|
|
2166
|
+
if (VERDICT_KINDS.has(s.kind)) return { ...base, action: false, hint: s.kind === "landed" ? "landed: map update to get it" : s.kind };
|
|
2167
|
+
return null;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
// src/actions.ts
|
|
2171
|
+
async function mapMail(mail, o = {}) {
|
|
2172
|
+
const out = [];
|
|
2173
|
+
let cursor;
|
|
2174
|
+
for (let i = 0; i < (o.pages ?? 3); i++) {
|
|
2175
|
+
const r = await mail.list({ limit: 100, cursor });
|
|
2176
|
+
for (const m of r.data) if (isRepoAddress(m.sender) && (!o.repo || m.sender === o.repo) && parseSubject(m.subject)) out.push(m);
|
|
2177
|
+
if (!r.cursor || !r.data.length) break;
|
|
2178
|
+
cursor = r.cursor;
|
|
2179
|
+
}
|
|
2180
|
+
return out;
|
|
2181
|
+
}
|
|
2182
|
+
function inboxItems(ms, o = {}) {
|
|
2183
|
+
const items = [];
|
|
2184
|
+
for (const m of ms) {
|
|
2185
|
+
const it = inboxItem(m);
|
|
2186
|
+
if (!it) continue;
|
|
2187
|
+
if (!o.all && !it.action) {
|
|
2188
|
+
const t = Date.parse(it.at);
|
|
2189
|
+
if (!(t >= (o.sinceMs ?? Date.now() - 24 * 36e5))) continue;
|
|
2190
|
+
}
|
|
2191
|
+
items.push(it);
|
|
2192
|
+
}
|
|
2193
|
+
return items;
|
|
2194
|
+
}
|
|
2195
|
+
async function waitInbox(mail, timeoutSec, repo, since) {
|
|
2196
|
+
const until = Date.now() + timeoutSec * 1e3;
|
|
2197
|
+
let cursor;
|
|
2198
|
+
const from = since ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2199
|
+
for (; ; ) {
|
|
2200
|
+
const left = Math.round((until - Date.now()) / 1e3);
|
|
2201
|
+
if (left <= 0) return [];
|
|
2202
|
+
const r = await mail.tail({ cursor, from, limit: 100, wait: Math.min(30, Math.max(1, left)) });
|
|
2203
|
+
cursor = r.cursor;
|
|
2204
|
+
const fresh = r.data.filter((m) => isRepoAddress(m.sender) && (!repo || m.sender === repo));
|
|
2205
|
+
const items = fresh.map(inboxItem).filter((x) => !!x && (x.action || x.kind !== "stuck"));
|
|
2206
|
+
if (items.length) return items;
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
var TARGETS = {
|
|
2210
|
+
approve: ["consent", "escalated", "pending", "objected", "conflict", "stuck", "check-failed", "vetoed", "received"],
|
|
2211
|
+
object: ["consent"],
|
|
2212
|
+
argue: ["objected"],
|
|
2213
|
+
veto: ["escalated", "pending", "consent", "objected", "stuck", "check-failed", "received"]
|
|
2214
|
+
};
|
|
2215
|
+
function pickTarget(action, pid, ms) {
|
|
2216
|
+
const mine = ms.filter((m) => parseSubject(m.subject)?.id === pid);
|
|
2217
|
+
if (!mine.length) {
|
|
2218
|
+
throw new MapError2(`no map mail about ${pid} in your mailbox. Check the push id (map inbox lists them) and that you are using the address map wrote to.`);
|
|
2219
|
+
}
|
|
2220
|
+
for (const kind of TARGETS[action]) {
|
|
2221
|
+
const hit = mine.find((m) => parseSubject(m.subject)?.kind === kind);
|
|
2222
|
+
if (!hit) continue;
|
|
2223
|
+
if (action === "argue" && parseSubject(hit.subject)?.rest === "final") {
|
|
2224
|
+
throw new MapError2(`${pid} was objected to in the last round: arguing will not reopen it. Change the files and map send again, or ask a repo owner to approve it.`);
|
|
2225
|
+
}
|
|
2226
|
+
return hit;
|
|
2227
|
+
}
|
|
2228
|
+
const kinds = [...new Set(mine.map((m) => parseSubject(m.subject)?.kind))].join(", ");
|
|
2229
|
+
if (action === "object") throw new MapError2(`map did not ask you to consent to ${pid} (you have: ${kinds}). Only line owners asked for consent can object; repo owners use map veto.`);
|
|
2230
|
+
if (action === "argue") throw new MapError2(`${pid} has no objection to argue with in your mailbox (you have: ${kinds}).`);
|
|
2231
|
+
throw new MapError2(`no map mail about ${pid} that ${action} can answer (you have: ${kinds}).`);
|
|
2232
|
+
}
|
|
2233
|
+
var WORDS = /^(approve[sd]?|object(s|ed|ion)?|veto(ed|es)?|confirm(ed)?)\b/i;
|
|
2234
|
+
function actionBody(action, text2, pid) {
|
|
2235
|
+
const t = text2.trim();
|
|
2236
|
+
switch (action) {
|
|
2237
|
+
case "approve":
|
|
2238
|
+
return `approve${t ? ` ${t}` : ""}
|
|
2239
|
+
`;
|
|
2240
|
+
case "object":
|
|
2241
|
+
if (!t) throw new MapError2('say why you object: map object <pid> "<reason>"');
|
|
2242
|
+
return `object: ${t}
|
|
2243
|
+
`;
|
|
2244
|
+
case "veto":
|
|
2245
|
+
return `veto${t ? ` ${t}` : ""}
|
|
2246
|
+
`;
|
|
2247
|
+
case "argue": {
|
|
2248
|
+
if (!t) throw new MapError2('give your argument: map argue <pid> "<why the objection is wrong>"');
|
|
2249
|
+
const first = t.split("\n").find((l) => l.trim() && !l.trimStart().startsWith(">")) ?? "";
|
|
2250
|
+
return WORDS.test(first.trim()) ? `Argument for ${pid}:
|
|
2251
|
+
|
|
2252
|
+
${t}
|
|
2253
|
+
` : `${t}
|
|
2254
|
+
`;
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
// src/args.ts
|
|
2260
|
+
var BOOLEAN = /* @__PURE__ */ new Set(["json", "wait", "all", "help", "h", "no-wait", "version", "yes", "y", "no-skill", "force", "strict"]);
|
|
2261
|
+
var VALUE = /* @__PURE__ */ new Set(["from", "repo", "n", "timeout", "bin-dir", "o", "output", "depth", "body-file", "since", "handle", "allow", "host", "adapter", "a", "b", "repo-host", "out", "provider", "seconds", "skills-home", "commit"]);
|
|
2262
|
+
var ALIAS = { h: "help", o: "output", y: "yes" };
|
|
2263
|
+
function parseArgs(argv) {
|
|
2264
|
+
const pos = [];
|
|
2265
|
+
const flags = {};
|
|
2266
|
+
let ended = false;
|
|
2267
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2268
|
+
const a = argv[i];
|
|
2269
|
+
if (ended || a === "-" || !a.startsWith("-")) {
|
|
2270
|
+
pos.push(a);
|
|
2271
|
+
continue;
|
|
2272
|
+
}
|
|
2273
|
+
if (a === "--") {
|
|
2274
|
+
ended = true;
|
|
2275
|
+
continue;
|
|
2276
|
+
}
|
|
2277
|
+
const m = /^--?([A-Za-z][A-Za-z-]*)(?:=(.*))?$/s.exec(a);
|
|
2278
|
+
if (!m) {
|
|
2279
|
+
pos.push(a);
|
|
2280
|
+
continue;
|
|
2281
|
+
}
|
|
2282
|
+
const raw = m[1];
|
|
2283
|
+
const key = ALIAS[raw] ?? raw;
|
|
2284
|
+
if (BOOLEAN.has(raw)) {
|
|
2285
|
+
if (m[2] !== void 0) throw new MapError2(`--${raw} takes no value`);
|
|
2286
|
+
flags[key] = true;
|
|
2287
|
+
} else if (VALUE.has(raw)) {
|
|
2288
|
+
const v = m[2] ?? argv[++i];
|
|
2289
|
+
if (v === void 0) throw new MapError2(`--${raw} needs a value`);
|
|
2290
|
+
flags[key] = v;
|
|
2291
|
+
} else {
|
|
2292
|
+
throw new MapError2(`unknown option ${a} (see map help)`);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
const cmd = pos.shift() ?? "";
|
|
2296
|
+
return { cmd, pos, flags };
|
|
2297
|
+
}
|
|
2298
|
+
function str(p, key) {
|
|
2299
|
+
const v = p.flags[key];
|
|
2300
|
+
return typeof v === "string" ? v : void 0;
|
|
2301
|
+
}
|
|
2302
|
+
function num(p, key, dflt2) {
|
|
2303
|
+
const v = str(p, key);
|
|
2304
|
+
if (v === void 0) return dflt2;
|
|
2305
|
+
const n = Number(v);
|
|
2306
|
+
if (!Number.isInteger(n) || n <= 0) throw new MapError2(`--${key} must be a positive whole number`);
|
|
2307
|
+
return n;
|
|
2308
|
+
}
|
|
2309
|
+
function bool(p, key) {
|
|
2310
|
+
return p.flags[key] === true;
|
|
2311
|
+
}
|
|
2312
|
+
var PID = /^p-[0-9a-f]{8}$/;
|
|
2313
|
+
var CID = /^c\d+(?:-[0-9a-f]{8})?$/;
|
|
2314
|
+
function pidArg(s) {
|
|
2315
|
+
const v = (s ?? "").trim().toLowerCase();
|
|
2316
|
+
if (!PID.test(v)) throw new MapError2(`"${s ?? ""}" is not a push id (like p-1a2b3c4d)`);
|
|
2317
|
+
return v;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
// src/conformance.ts
|
|
2321
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
2322
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, realpathSync, renameSync, statSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2323
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
2324
|
+
import { isAbsolute, join as join6, resolve as resolve2 } from "node:path";
|
|
2325
|
+
import { fileURLToPath } from "node:url";
|
|
2326
|
+
import { crc32, deflateSync } from "node:zlib";
|
|
2327
|
+
var CONFORMANCE_VERSION = "1";
|
|
2328
|
+
var Skip = class extends Error {
|
|
2329
|
+
};
|
|
2330
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2331
|
+
function png() {
|
|
2332
|
+
const chunk = (type, data) => {
|
|
2333
|
+
const len = Buffer.alloc(4);
|
|
2334
|
+
len.writeUInt32BE(data.length);
|
|
2335
|
+
const td2 = Buffer.concat([Buffer.from(type, "ascii"), data]);
|
|
2336
|
+
const crc2 = Buffer.alloc(4);
|
|
2337
|
+
crc2.writeUInt32BE(crc32(td2) >>> 0);
|
|
2338
|
+
return Buffer.concat([len, td2, crc2]);
|
|
2339
|
+
};
|
|
2340
|
+
const w = 8;
|
|
2341
|
+
const h = 8;
|
|
2342
|
+
const ihdr = Buffer.alloc(13);
|
|
2343
|
+
ihdr.writeUInt32BE(w, 0);
|
|
2344
|
+
ihdr.writeUInt32BE(h, 4);
|
|
2345
|
+
ihdr[8] = 8;
|
|
2346
|
+
ihdr[9] = 6;
|
|
2347
|
+
const rows = [];
|
|
2348
|
+
for (let y = 0; y < h; y++) rows.push(Buffer.concat([Buffer.from([0]), randomBytes2(w * 4)]));
|
|
2349
|
+
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
2350
|
+
return new Uint8Array(Buffer.concat([sig, chunk("IHDR", ihdr), chunk("IDAT", deflateSync(Buffer.concat(rows))), chunk("IEND", Buffer.alloc(0))]));
|
|
2351
|
+
}
|
|
2352
|
+
function listFiles(dir, rel = "") {
|
|
2353
|
+
const out = [];
|
|
2354
|
+
for (const n of readdirSync2(join6(dir, rel)).sort()) {
|
|
2355
|
+
const r = rel ? `${rel}/${n}` : n;
|
|
2356
|
+
if (r === ".map" || r === ".gud") continue;
|
|
2357
|
+
if (statSync(join6(dir, r)).isDirectory()) out.push(...listFiles(dir, r));
|
|
2358
|
+
else out.push(r);
|
|
2359
|
+
}
|
|
2360
|
+
return out;
|
|
2361
|
+
}
|
|
2362
|
+
function equalBytes(a, b) {
|
|
2363
|
+
return a.length === b.length && Buffer.compare(Buffer.from(a), Buffer.from(b)) === 0;
|
|
2364
|
+
}
|
|
2365
|
+
async function runConformance(o) {
|
|
2366
|
+
const log2 = o.log ?? ((s) => say(s));
|
|
2367
|
+
const adapterRef = /[\\/]|\.m?js$/.test(o.adapter) ? resolve2(o.adapter) : o.adapter;
|
|
2368
|
+
process.env.MAP_TRANSPORT = adapterRef;
|
|
2369
|
+
const t = await loadTransport(adapterRef);
|
|
2370
|
+
const desc = await t.describe?.().catch(() => null) ?? { provider: t.name };
|
|
2371
|
+
const A = new Mail(o.a, t);
|
|
2372
|
+
const B = new Mail(o.b, t);
|
|
2373
|
+
const tag = randomBytes2(3).toString("hex");
|
|
2374
|
+
const work = mkdtempSync3(join6(tmpdir3(), `map-conformance-${tag}-`));
|
|
2375
|
+
const checks = [];
|
|
2376
|
+
const report = {
|
|
2377
|
+
suite: `map transport conformance v${CONFORMANCE_VERSION}`,
|
|
2378
|
+
adapter: t.name,
|
|
2379
|
+
provider: desc.provider,
|
|
2380
|
+
providerVersion: desc.version ?? "unknown",
|
|
2381
|
+
mapCli: o.cliVersion,
|
|
2382
|
+
node: process.version,
|
|
2383
|
+
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2384
|
+
a: o.a,
|
|
2385
|
+
b: o.b,
|
|
2386
|
+
repo: o.repo ?? null,
|
|
2387
|
+
checks,
|
|
2388
|
+
passed: false
|
|
2389
|
+
};
|
|
2390
|
+
const check = async (id, part, name, fn, needs) => {
|
|
2391
|
+
const missing = needs?.();
|
|
2392
|
+
if (missing) {
|
|
2393
|
+
checks.push({ id, part, name, status: "SKIP", seconds: 0, reason: missing });
|
|
2394
|
+
log2(`SKIP ${id} ${name}: ${missing}`);
|
|
2395
|
+
return false;
|
|
2396
|
+
}
|
|
2397
|
+
log2(`.... ${id} ${name}`);
|
|
2398
|
+
const t0 = Date.now();
|
|
2399
|
+
try {
|
|
2400
|
+
const reason = await fn();
|
|
2401
|
+
checks.push({ id, part, name, status: "PASS", seconds: (Date.now() - t0) / 1e3, reason });
|
|
2402
|
+
log2(`PASS ${id} ${name}: ${reason}`);
|
|
2403
|
+
return true;
|
|
2404
|
+
} catch (e) {
|
|
2405
|
+
const skip = e instanceof Skip;
|
|
2406
|
+
const reason = e.message.split("\n")[0].slice(0, 300);
|
|
2407
|
+
checks.push({ id, part, name, status: skip ? "SKIP" : "FAIL", seconds: (Date.now() - t0) / 1e3, reason });
|
|
2408
|
+
log2(`${skip ? "SKIP" : "FAIL"} ${id} ${name}: ${reason}`);
|
|
2409
|
+
return false;
|
|
2410
|
+
}
|
|
2411
|
+
};
|
|
2412
|
+
const fail = (m) => {
|
|
2413
|
+
throw new Error(m);
|
|
2414
|
+
};
|
|
2415
|
+
const inbound = async (box, subject, timeoutSec = 120) => {
|
|
2416
|
+
const until = Date.now() + timeoutSec * 1e3;
|
|
2417
|
+
for (; ; ) {
|
|
2418
|
+
const { data } = await box.list({ limit: 50 });
|
|
2419
|
+
const hit = data.find((m) => m.subject === subject);
|
|
2420
|
+
if (hit) return hit;
|
|
2421
|
+
if (Date.now() > until) fail(`"${subject}" did not reach ${box.me} within ${timeoutSec} s`);
|
|
2422
|
+
await sleep(2e3);
|
|
2423
|
+
}
|
|
2424
|
+
};
|
|
2425
|
+
const P1 = "1. Interface";
|
|
2426
|
+
const start = new Date(Date.now() - 2e3).toISOString();
|
|
2427
|
+
await check("1.1", P1, "send returns a stable id", async () => {
|
|
2428
|
+
const s1 = `map conformance ${tag} send 1`;
|
|
2429
|
+
const s2 = `map conformance ${tag} send 2`;
|
|
2430
|
+
const id1 = await A.send({ to: o.b, subject: s1, body: "first\n" });
|
|
2431
|
+
const id2 = await A.send({ to: o.b, subject: s2, body: "second\n" });
|
|
2432
|
+
if (typeof id1 !== "string" || !id1 || typeof id2 !== "string" || !id2) fail("send did not return an id");
|
|
2433
|
+
if (id1 === id2) fail(`two sends returned the same id ${id1}`);
|
|
2434
|
+
await inbound(B, s1);
|
|
2435
|
+
await inbound(B, s2);
|
|
2436
|
+
return `two sends gave distinct ids (${id1.slice(0, 8)}..., ${id2.slice(0, 8)}...), both delivered to B; 1.3 waits on such an id`;
|
|
2437
|
+
});
|
|
2438
|
+
await check("1.2", P1, "an attachment round-trips byte for byte with its filename", async () => {
|
|
2439
|
+
const bytes = new Uint8Array(Buffer.concat([Buffer.from([0, 255, 13, 10, 31, 139]), randomBytes2(4090)]));
|
|
2440
|
+
const name = "conformance-blob.bin";
|
|
2441
|
+
const dir = join6(work, "attach");
|
|
2442
|
+
mkdirSync3(dir, { recursive: true });
|
|
2443
|
+
writeFileSync4(join6(dir, name), bytes);
|
|
2444
|
+
const subject = `map conformance ${tag} attachment`;
|
|
2445
|
+
await A.send({ to: o.b, subject, body: "binary attached\n", attachment: join6(dir, name) });
|
|
2446
|
+
const m = await inbound(B, subject);
|
|
2447
|
+
const d = await B.get(m.id);
|
|
2448
|
+
const att = d.attachments.find((x) => x.filename === name);
|
|
2449
|
+
if (!att) fail(`B sees attachments [${d.attachments.map((x) => x.filename).join(", ")}], not ${name}`);
|
|
2450
|
+
const got = await B.download(d.id, att.part_index, join6(dir, "received.bin"));
|
|
2451
|
+
if (!equalBytes(got, bytes)) fail(`B read ${got.length} bytes that differ from the ${bytes.length} sent`);
|
|
2452
|
+
return `${bytes.length} random bytes (with NUL, 0xff, CRLF) arrived as ${name}, identical`;
|
|
2453
|
+
});
|
|
2454
|
+
let threadA = null;
|
|
2455
|
+
await check("1.3", P1, "waitReply returns each reply once, in order, including one that came before the wait", async () => {
|
|
2456
|
+
const subject = `map conformance ${tag} replies`;
|
|
2457
|
+
const sent = await A.send({ to: o.b, subject, body: "reply twice please\n" });
|
|
2458
|
+
const m = await inbound(B, subject);
|
|
2459
|
+
await B.reply(m.id, `reply one ${tag}
|
|
2460
|
+
`);
|
|
2461
|
+
await sleep(1500);
|
|
2462
|
+
await B.reply(m.id, `reply two ${tag}
|
|
2463
|
+
`);
|
|
2464
|
+
const until = Date.now() + 12e4;
|
|
2465
|
+
for (; ; ) {
|
|
2466
|
+
const { data } = await A.list({ after: start, limit: 50 });
|
|
2467
|
+
if (data.filter((x) => x.sender === o.b && x.subject.includes(`${tag} replies`)).length >= 2) break;
|
|
2468
|
+
if (Date.now() > until) fail("B's two replies did not reach A within 120 s");
|
|
2469
|
+
await sleep(2e3);
|
|
2470
|
+
}
|
|
2471
|
+
const r1 = await A.waitReply(sent, o.b, { timeoutSec: 60 });
|
|
2472
|
+
if (!r1) fail("the first wait found no reply");
|
|
2473
|
+
const r2 = await A.waitReply(sent, o.b, { timeoutSec: 60, since: after(r1.received_at) });
|
|
2474
|
+
if (!r2) fail("the second wait found no second reply");
|
|
2475
|
+
const r3 = await A.waitReply(sent, o.b, { timeoutSec: 5, since: after(r2.received_at) });
|
|
2476
|
+
if (r3) fail(`a third wait returned ${r3.id}, a reply already seen or one never sent`);
|
|
2477
|
+
if (r1.id === r2.id) fail("both waits returned the same reply");
|
|
2478
|
+
const b1 = (await A.get(r1.id)).body_text;
|
|
2479
|
+
const b2 = (await A.get(r2.id)).body_text;
|
|
2480
|
+
if (!b1.includes(`reply one ${tag}`) || !b2.includes(`reply two ${tag}`)) fail("the replies came back out of order");
|
|
2481
|
+
threadA = { sent, copyB: m.id, first: r1 };
|
|
2482
|
+
return "reply one, then reply two, then a timeout; both were already delivered when the waits began";
|
|
2483
|
+
});
|
|
2484
|
+
await check(
|
|
2485
|
+
"1.4",
|
|
2486
|
+
P1,
|
|
2487
|
+
"a reply threads to the message it answers",
|
|
2488
|
+
async () => {
|
|
2489
|
+
const original = await B.get(threadA.copyB);
|
|
2490
|
+
const reply = await A.get(threadA.first.id);
|
|
2491
|
+
if (!original.message_id) fail("the adapter does not report Message-ID, so threading cannot be checked");
|
|
2492
|
+
if (!reply.in_reply_to) fail("the adapter does not report In-Reply-To");
|
|
2493
|
+
const irt = reply.in_reply_to.map((x) => x.trim());
|
|
2494
|
+
if (!irt.includes(original.message_id.trim())) fail(`In-Reply-To is ${irt.join(" ")}, not ${original.message_id}`);
|
|
2495
|
+
if (reply.references && !reply.references.map((x) => x.trim()).includes(original.message_id.trim())) fail("References does not name the original");
|
|
2496
|
+
return `In-Reply-To and References name ${original.message_id}; 2.4 and 2.5 show map matching such replies to their push`;
|
|
2497
|
+
},
|
|
2498
|
+
() => threadA ? null : "needs 1.3"
|
|
2499
|
+
);
|
|
2500
|
+
await check("1.5", P1, "the inbox (list and tail) holds only mail to my own address", async () => {
|
|
2501
|
+
const lists = [
|
|
2502
|
+
["list A", (await A.list({ after: start, limit: 100 })).data],
|
|
2503
|
+
["tail A", (await A.tail({ from: start, limit: 100 })).data],
|
|
2504
|
+
["list B", (await B.list({ after: start, limit: 100 })).data]
|
|
2505
|
+
];
|
|
2506
|
+
for (const [what, rows] of lists) {
|
|
2507
|
+
const me = what.endsWith("A") ? o.a : o.b;
|
|
2508
|
+
if (!rows.length) fail(`${what} is empty, though mail was delivered`);
|
|
2509
|
+
const missing = rows.filter((r) => !r.recipient);
|
|
2510
|
+
if (missing.length) fail(`${what}: the adapter does not report recipients`);
|
|
2511
|
+
const other = rows.find((r) => r.recipient !== me);
|
|
2512
|
+
if (other) fail(`${what} holds mail to ${other.recipient}`);
|
|
2513
|
+
}
|
|
2514
|
+
if (lists[0][1].some((r) => /send 1$/.test(r.subject))) fail("A's inbox shows the mail A sent to B");
|
|
2515
|
+
return `${lists.map(([w, r]) => `${w}: ${r.length}`).join(", ")}; every row is to its own address`;
|
|
2516
|
+
});
|
|
2517
|
+
await check("1.6", P1, "a wait that times out is a timeout, not an error", async () => {
|
|
2518
|
+
const sent = await A.send({ to: o.b, subject: `map conformance ${tag} nobody answers`, body: "no reply expected\n" });
|
|
2519
|
+
const t0 = Date.now();
|
|
2520
|
+
const r = await A.waitReply(sent, o.b, { timeoutSec: 5 });
|
|
2521
|
+
if (r) fail(`the wait returned ${r.id}`);
|
|
2522
|
+
return `waitReply returned null after ${((Date.now() - t0) / 1e3).toFixed(1)} s with a 5 s timeout`;
|
|
2523
|
+
});
|
|
2524
|
+
await check("1.7", P1, "search finds mail by its words, only in my own mailbox", async () => {
|
|
2525
|
+
const word = `zq${tag}word`;
|
|
2526
|
+
const subject = `map conformance ${tag} search`;
|
|
2527
|
+
await A.send({ to: o.b, subject, body: `The marker ${word} is in this body.
|
|
2528
|
+
` });
|
|
2529
|
+
const m = await inbound(B, subject);
|
|
2530
|
+
const t0 = Date.now();
|
|
2531
|
+
let hits = [];
|
|
2532
|
+
let command = "";
|
|
2533
|
+
for (; ; ) {
|
|
2534
|
+
const r = await t.search({ me: o.b, q: word, to: o.b, limit: 10 });
|
|
2535
|
+
hits = r.data;
|
|
2536
|
+
command = r.command ?? "";
|
|
2537
|
+
if (hits.length || Date.now() - t0 > 6e4) break;
|
|
2538
|
+
await sleep(3e3);
|
|
2539
|
+
}
|
|
2540
|
+
if (!hits.some((h) => h.id === m.id)) fail(`a search for ${word} did not return B's mail`);
|
|
2541
|
+
if (hits.some((h) => h.recipient && h.recipient !== o.b)) fail("the search returned mail to another address");
|
|
2542
|
+
const bySubject = await t.search({ me: o.b, subject: `${tag} search`, to: o.b, limit: 10 });
|
|
2543
|
+
if (!bySubject.data.some((h) => h.id === m.id)) fail("a subject search did not return it");
|
|
2544
|
+
return `found by a body word and by subject${command ? ` (${command.slice(0, 80)})` : ""}`;
|
|
2545
|
+
});
|
|
2546
|
+
const P2 = "2. Flow";
|
|
2547
|
+
const self = realpathSync(fileURLToPath(import.meta.url));
|
|
2548
|
+
const bin2 = join6(work, "bin");
|
|
2549
|
+
mkdirSync3(bin2);
|
|
2550
|
+
writeFileSync4(join6(bin2, "map"), `#!/bin/sh
|
|
2551
|
+
exec "${process.execPath}" "${self}" "$@"
|
|
2552
|
+
`, { mode: 493 });
|
|
2553
|
+
const baseEnv = { ...process.env, PATH: `${bin2}:${process.env.PATH}`, MAP_WAIT_SECONDS: "900" };
|
|
2554
|
+
delete baseEnv.MAP_NO_WAIT;
|
|
2555
|
+
const sh = async (who, cwd, cmd, ...args) => {
|
|
2556
|
+
const r = await run2(cmd, args, { cwd, env: { ...baseEnv, MAP_FROM: who } });
|
|
2557
|
+
return { code: r.code, out: r.out, err: r.err, all: `${r.out}
|
|
2558
|
+
${r.err}` };
|
|
2559
|
+
};
|
|
2560
|
+
const must = async (who, cwd, cmd, ...args) => {
|
|
2561
|
+
const r = await sh(who, cwd, cmd, ...args);
|
|
2562
|
+
if (r.code !== 0) fail(`${cmd} ${args.join(" ")} failed: ${r.all.trim().split("\n").slice(-3).join(" / ")}`);
|
|
2563
|
+
return r;
|
|
2564
|
+
};
|
|
2565
|
+
const landed = (all) => /LANDED (p-[0-9a-f]{8}) as (c\d+-[0-9a-f]{8})/.exec(all);
|
|
2566
|
+
const sendLanded = async (who, cwd, subject) => {
|
|
2567
|
+
const r = await sh(who, cwd, "map", "send", subject);
|
|
2568
|
+
const m = landed(r.all);
|
|
2569
|
+
if (r.code !== 0 || !m) fail(`map send did not land: ${r.all.trim().split("\n").filter((l) => l.startsWith("map:")).slice(-3).join(" / ")}`);
|
|
2570
|
+
return { pid: m[1], cid: m[2] };
|
|
2571
|
+
};
|
|
2572
|
+
const caughtUp = async (who, cwd) => {
|
|
2573
|
+
await must(who, cwd, "map", "update");
|
|
2574
|
+
const d = await must(who, cwd, "map", "diff");
|
|
2575
|
+
if (d.out.trim()) fail(`map diff still shows changes after map update: ${d.out.slice(0, 120)}`);
|
|
2576
|
+
};
|
|
2577
|
+
const da = join6(work, "a");
|
|
2578
|
+
const db = join6(work, "b");
|
|
2579
|
+
let repo = o.repo ?? "";
|
|
2580
|
+
await check("2.0", P2, "create a test repo by email (B owns it, A may push)", async () => {
|
|
2581
|
+
if (o.repo) return `using the given repo ${o.repo} (B must own it and A must be allowed)`;
|
|
2582
|
+
const name = `conf${tag}`;
|
|
2583
|
+
const args = ["create", name, "--allow", o.a, "--host", o.host, "--json"];
|
|
2584
|
+
if (o.handle) args.push("--handle", o.handle);
|
|
2585
|
+
else args.push("--handle", `gc${randomBytes2(3).toString("hex")}`);
|
|
2586
|
+
const r = await must(o.b, work, "map", ...args);
|
|
2587
|
+
repo = JSON.parse(r.out).address;
|
|
2588
|
+
report.repo = repo;
|
|
2589
|
+
return `created ${repo} through new@${o.host}`;
|
|
2590
|
+
});
|
|
2591
|
+
const needRepo = () => repo ? null : "needs a repo (2.0)";
|
|
2592
|
+
let notes = "";
|
|
2593
|
+
const ok21 = await check(
|
|
2594
|
+
"2.1",
|
|
2595
|
+
P2,
|
|
2596
|
+
"A gets the code, edits, sends: landed; map update leaves nothing to send",
|
|
2597
|
+
async () => {
|
|
2598
|
+
await must(o.a, work, "map", "get", repo, "a");
|
|
2599
|
+
notes = ["line one by A", "line two by A", "line three by A", "line four by A", "line five by A", ""].join("\n");
|
|
2600
|
+
mkdirSync3(join6(da, "docs"), { recursive: true });
|
|
2601
|
+
writeFileSync4(join6(da, "notes.txt"), notes);
|
|
2602
|
+
writeFileSync4(join6(da, "docs", "old.md"), "# Notes\n\nThis file is renamed by check 2.3.\n");
|
|
2603
|
+
const l = await sendLanded(o.a, da, "Add notes and docs");
|
|
2604
|
+
await caughtUp(o.a, da);
|
|
2605
|
+
return `landed ${l.pid} as ${l.cid}; after map update, map diff is empty`;
|
|
2606
|
+
},
|
|
2607
|
+
needRepo
|
|
2608
|
+
);
|
|
2609
|
+
const ok22 = await check(
|
|
2610
|
+
"2.2",
|
|
2611
|
+
P2,
|
|
2612
|
+
"B sends a stale change: conflict; map update marks the overlap; resolved and sent: landed",
|
|
2613
|
+
async () => {
|
|
2614
|
+
await must(o.b, work, "map", "get", repo, "b");
|
|
2615
|
+
writeFileSync4(join6(da, "notes.txt"), notes.replace("line one by A", "line one, edited by A"));
|
|
2616
|
+
await sendLanded(o.a, da, "Edit line one");
|
|
2617
|
+
await caughtUp(o.a, da);
|
|
2618
|
+
writeFileSync4(join6(db, "notes.txt"), notes.replace("line one by A", "line one, edited by B"));
|
|
2619
|
+
const c = await sh(o.b, db, "map", "send", "Edit line one differently");
|
|
2620
|
+
if (c.code !== 1 || !/CONFLICT on (p-[0-9a-f]{8})/.test(c.all) || !c.all.includes("notes.txt")) fail(`expected a conflict naming notes.txt, got exit ${c.code}`);
|
|
2621
|
+
const pid = /CONFLICT on (p-[0-9a-f]{8})/.exec(c.all)[1];
|
|
2622
|
+
const u = await sh(o.b, db, "map", "update");
|
|
2623
|
+
const marked = readFileSync4(join6(db, "notes.txt"), "utf8");
|
|
2624
|
+
if (u.code === 0 || !/^<<<<<<< /m.test(marked) || !marked.includes("line one, edited by A") || !marked.includes("line one, edited by B")) fail("map update did not mark the overlap in notes.txt");
|
|
2625
|
+
notes = notes.replace("line one by A", "line one, edited by A and B");
|
|
2626
|
+
writeFileSync4(join6(db, "notes.txt"), notes);
|
|
2627
|
+
const l = await sendLanded(o.b, db, "Edit line one, merged");
|
|
2628
|
+
await caughtUp(o.b, db);
|
|
2629
|
+
return `${pid} got a conflict naming notes.txt; map update wrote markers; after resolving, ${l.pid} landed as ${l.cid}`;
|
|
2630
|
+
},
|
|
2631
|
+
() => needRepo() ?? (ok21 ? null : "needs 2.1")
|
|
2632
|
+
);
|
|
2633
|
+
const pixel = png();
|
|
2634
|
+
await check(
|
|
2635
|
+
"2.3",
|
|
2636
|
+
P2,
|
|
2637
|
+
"a binary file and a rename land byte for byte (checked in a fresh map get)",
|
|
2638
|
+
async () => {
|
|
2639
|
+
await must(o.a, da, "map", "update");
|
|
2640
|
+
mkdirSync3(join6(da, "assets"), { recursive: true });
|
|
2641
|
+
writeFileSync4(join6(da, "assets", "pixel.png"), pixel);
|
|
2642
|
+
renameSync(join6(da, "docs", "old.md"), join6(da, "docs", "new.md"));
|
|
2643
|
+
const l = await sendLanded(o.a, da, "Add a pixel and rename the docs");
|
|
2644
|
+
await caughtUp(o.a, da);
|
|
2645
|
+
await must(o.a, work, "map", "get", repo, "c");
|
|
2646
|
+
const dc = join6(work, "c");
|
|
2647
|
+
if (!equalBytes(new Uint8Array(readFileSync4(join6(dc, "assets", "pixel.png"))), pixel)) fail("assets/pixel.png differs in the fresh copy");
|
|
2648
|
+
if (existsSync2(join6(dc, "docs", "old.md")) || !existsSync2(join6(dc, "docs", "new.md"))) fail("the rename did not land");
|
|
2649
|
+
const files = listFiles(da);
|
|
2650
|
+
if (files.join() !== listFiles(dc).join()) fail("A's folder and a fresh copy hold different files");
|
|
2651
|
+
for (const f of files) if (!equalBytes(new Uint8Array(readFileSync4(join6(da, f))), new Uint8Array(readFileSync4(join6(dc, f))))) fail(`${f} differs between A's folder and a fresh copy`);
|
|
2652
|
+
return `${l.pid} landed as ${l.cid}; a fresh map get has the same ${files.length} files byte for byte (pixel.png ${pixel.length} bytes)`;
|
|
2653
|
+
},
|
|
2654
|
+
() => needRepo() ?? (ok22 ? null : "needs 2.2")
|
|
2655
|
+
);
|
|
2656
|
+
const window = Math.max(5, o.consentWindow ?? 5);
|
|
2657
|
+
const consentOn = await check(
|
|
2658
|
+
"2.4a",
|
|
2659
|
+
P2,
|
|
2660
|
+
`B, the owner, turns on line owner consent (${window} min window) with map settings`,
|
|
2661
|
+
async () => {
|
|
2662
|
+
const r = await must(o.b, db, "map", "settings", repo, "Map-Consent: owners", `Map-Consent-Window: ${window}`);
|
|
2663
|
+
if (!/Line owner consent:\s*\n\s*owners/.test(r.out)) fail(`the settings ack does not show consent on: ${r.out.slice(0, 200)}`);
|
|
2664
|
+
return "the settings ack shows line owner consent: owners";
|
|
2665
|
+
},
|
|
2666
|
+
() => needRepo() ?? (ok22 ? null : "needs 2.2")
|
|
2667
|
+
);
|
|
2668
|
+
const consentAsk = async (seen, round, timeoutSec = 900) => {
|
|
2669
|
+
const until = Date.now() + timeoutSec * 1e3;
|
|
2670
|
+
for (; ; ) {
|
|
2671
|
+
const r = await sh(o.a, da, "map", "inbox", "--json", "--repo", repo);
|
|
2672
|
+
if (r.code === 0) {
|
|
2673
|
+
const items = JSON.parse(r.out).items;
|
|
2674
|
+
const hit = items.find((i) => i.kind === "consent" && i.action && !seen.includes(i.pid) && (!round || parseSubject(i.subject)?.fields.round === round));
|
|
2675
|
+
if (hit) return { pid: hit.pid, id: hit.id };
|
|
2676
|
+
}
|
|
2677
|
+
if (Date.now() > until) fail(`no consent ask reached A within ${timeoutSec} s`);
|
|
2678
|
+
await sleep(5e3);
|
|
2679
|
+
}
|
|
2680
|
+
};
|
|
2681
|
+
let firstAsk = "";
|
|
2682
|
+
const ok24 = await check(
|
|
2683
|
+
"2.4",
|
|
2684
|
+
P2,
|
|
2685
|
+
"B changes a line A wrote: A gets a consent ask, and map approve lands it",
|
|
2686
|
+
async () => {
|
|
2687
|
+
await must(o.b, db, "map", "update");
|
|
2688
|
+
notes = notes.replace("line three by A", "line three, edited by B");
|
|
2689
|
+
writeFileSync4(join6(db, "notes.txt"), notes);
|
|
2690
|
+
const send = sh(o.b, db, "map", "send", "Edit line three");
|
|
2691
|
+
const ask = await consentAsk([]);
|
|
2692
|
+
firstAsk = ask.pid;
|
|
2693
|
+
await must(o.a, da, "map", "approve", ask.pid, "--repo", repo);
|
|
2694
|
+
const r = await send;
|
|
2695
|
+
const l = landed(r.all);
|
|
2696
|
+
if (r.code !== 0 || !l) fail(`B's send did not land after the approve: exit ${r.code}`);
|
|
2697
|
+
if (l[1] !== ask.pid) fail(`A was asked about ${ask.pid} but ${l[1]} landed`);
|
|
2698
|
+
await caughtUp(o.b, db);
|
|
2699
|
+
return `A got [map consent ${ask.pid}]; map approve landed it as ${l[2]}`;
|
|
2700
|
+
},
|
|
2701
|
+
() => consentOn ? null : "needs 2.4a"
|
|
2702
|
+
);
|
|
2703
|
+
let objected = "";
|
|
2704
|
+
await check(
|
|
2705
|
+
"2.5",
|
|
2706
|
+
P2,
|
|
2707
|
+
"A objects to a later B change with map object, B argues with map argue, the argument reaches that push",
|
|
2708
|
+
async () => {
|
|
2709
|
+
notes = notes.replace("line four by A", "line four, edited by B");
|
|
2710
|
+
writeFileSync4(join6(db, "notes.txt"), notes);
|
|
2711
|
+
const send = sh(o.b, db, "map", "send", "Edit line four");
|
|
2712
|
+
const ask = await consentAsk([firstAsk]);
|
|
2713
|
+
await must(o.a, da, "map", "object", ask.pid, `conformance objection ${tag}`, "--repo", repo);
|
|
2714
|
+
const r = await send;
|
|
2715
|
+
if (r.code !== 1 || !r.all.includes(`OBJECTED: line owner ${o.a} objected to ${ask.pid}`)) fail(`B's send did not report A's objection: exit ${r.code}`);
|
|
2716
|
+
if (!r.all.includes(`conformance objection ${tag}`)) fail("B's send did not show A's reason");
|
|
2717
|
+
const argument = `conformance argument ${tag}: line four reads better this way`;
|
|
2718
|
+
await must(o.b, db, "map", "argue", ask.pid, argument, "--repo", repo);
|
|
2719
|
+
const again = await consentAsk([], "2");
|
|
2720
|
+
if (again.pid !== ask.pid) fail(`the round 2 ask is about ${again.pid}, not ${ask.pid}`);
|
|
2721
|
+
const body = (await A.get(again.id)).body_text;
|
|
2722
|
+
if (!body.includes(`conformance argument ${tag}`)) fail("the round 2 ask does not quote B's argument");
|
|
2723
|
+
objected = ask.pid;
|
|
2724
|
+
await must(o.a, da, "map", "approve", ask.pid, "--repo", repo);
|
|
2725
|
+
const w = await sh(o.b, db, "map", "wait", ask.pid, "--seconds", "600");
|
|
2726
|
+
if (w.code !== 0 || !landed(w.all)) fail(`map wait did not return the landing after A's approve: exit ${w.code}`);
|
|
2727
|
+
return `B saw A's objection to ${ask.pid}; B's argument came back to A in [map consent ${ask.pid}] round=2; after A's approve, map wait returned LANDED`;
|
|
2728
|
+
},
|
|
2729
|
+
() => ok24 ? null : "needs 2.4"
|
|
2730
|
+
);
|
|
2731
|
+
await check(
|
|
2732
|
+
"2.6",
|
|
2733
|
+
P2,
|
|
2734
|
+
"history is mail: map history finds the objection, map log lists the commits",
|
|
2735
|
+
async () => {
|
|
2736
|
+
const h = await must(o.b, db, "map", "history", `conformance objection ${tag}`, "--json");
|
|
2737
|
+
const items = JSON.parse(h.out).items;
|
|
2738
|
+
if (!items.some((i) => i.kind === "objected" && i.what.includes(objected))) fail(`map history did not find the objected mail about ${objected}`);
|
|
2739
|
+
const lg = await must(o.a, da, "map", "log", "--json", "-n", "20");
|
|
2740
|
+
const j = JSON.parse(lg.out);
|
|
2741
|
+
if (j.entries.length < 5) fail(`map log listed ${j.entries.length} commits, expected at least 5`);
|
|
2742
|
+
return `history found [map objected ${objected}] by searching B's mailbox; log listed ${j.entries.length} commits (${j.source === "mailbox" ? "from commit mail in this account" : "from the repo's reply to a Map-Log mail"})`;
|
|
2743
|
+
},
|
|
2744
|
+
() => objected ? null : "needs 2.5"
|
|
2745
|
+
);
|
|
2746
|
+
const P3 = "3. Security";
|
|
2747
|
+
await check(
|
|
2748
|
+
"3.1",
|
|
2749
|
+
P3,
|
|
2750
|
+
"a From display name naming the owner (B) does not make A an owner",
|
|
2751
|
+
async () => {
|
|
2752
|
+
const rule = `*@forged-${tag}.example`;
|
|
2753
|
+
const subject = `map conformance ${tag} forged settings`;
|
|
2754
|
+
const body = `Map-Allow: ${rule}
|
|
2755
|
+
`;
|
|
2756
|
+
await A.send({ fromName: o.b, to: o.b, subject, body });
|
|
2757
|
+
const copy = await B.get((await inbound(B, subject)).id);
|
|
2758
|
+
const shown2 = copy.from_header ? `From: ${copy.from_header}` : "the adapter does not report the From header";
|
|
2759
|
+
if (copy.from_header && !copy.from_header.includes(o.b)) throw new Skip(`the adapter did not send the display name (${shown2})`);
|
|
2760
|
+
const sentAt = new Date(Date.now() - 2e3).toISOString();
|
|
2761
|
+
await A.send({ fromName: o.b, to: repo, subject, body });
|
|
2762
|
+
await sleep(45e3);
|
|
2763
|
+
const s = await must(o.b, db, "map", "settings", repo);
|
|
2764
|
+
if (!/Contributor rules:/.test(s.out)) fail("the owner could not read the settings to compare");
|
|
2765
|
+
if (s.out.includes(rule)) fail(`the repo applied ${rule} from the forged mail`);
|
|
2766
|
+
const answers = (await A.list({ after: sentAt, limit: 50 })).data.filter((m) => m.sender === repo && m.subject.endsWith(subject));
|
|
2767
|
+
const ack = answers.length ? (await A.get(answers[0].id)).body_text : "";
|
|
2768
|
+
if (/settings of .* changed\./.test(ack)) fail("the repo acknowledged the forged settings as an owner's");
|
|
2769
|
+
return `${shown2}; ${rule} was not applied (${answers.length ? "A was answered as a non-owner" : "the mail was dropped"})`;
|
|
2770
|
+
},
|
|
2771
|
+
() => needRepo() ?? (ok22 ? null : "needs 2.2")
|
|
2772
|
+
);
|
|
2773
|
+
report.passed = checks.every((c) => c.status === "PASS");
|
|
2774
|
+
return report;
|
|
2775
|
+
}
|
|
2776
|
+
function markdown(r) {
|
|
2777
|
+
const rows = r.checks.map((c) => `| ${c.id} | ${c.name} | ${c.status} | ${c.seconds.toFixed(1)} s | ${c.reason.replace(/\|/g, "\\|")} |`);
|
|
2778
|
+
const counts = (s) => r.checks.filter((c) => c.status === s).length;
|
|
2779
|
+
return `# map transport conformance: ${r.adapter}
|
|
2780
|
+
|
|
2781
|
+
- Suite: ${r.suite}
|
|
2782
|
+
- Adapter: \`${r.adapter}\`
|
|
2783
|
+
- Provider: ${r.provider}, version ${r.providerVersion}
|
|
2784
|
+
- map CLI: ${r.mapCli}; Node ${r.node}
|
|
2785
|
+
- Date: ${r.date}
|
|
2786
|
+
- Mailboxes: A ${r.a}, B ${r.b}
|
|
2787
|
+
- Repo: ${r.repo ?? "(none)"}
|
|
2788
|
+
- Result: ${r.passed ? "PASS" : "FAIL"} (${counts("PASS")} passed, ${counts("FAIL")} failed, ${counts("SKIP")} skipped)
|
|
2789
|
+
|
|
2790
|
+
| # | Check | Result | Time | Reason |
|
|
2791
|
+
| --- | --- | --- | --- | --- |
|
|
2792
|
+
${rows.join("\n")}
|
|
2793
|
+
`;
|
|
2794
|
+
}
|
|
2795
|
+
async function conformanceMain(argv, cliVersion) {
|
|
2796
|
+
const s = (k) => typeof argv.flags[k] === "string" ? argv.flags[k] : void 0;
|
|
2797
|
+
if (argv.pos[0] !== "transport") throw new MapError2("usage: map-conformance transport --adapter <primitive|path> --a <address> --b <address> [--repo-host map.dev] [--repo <address>] [--json] [--out <file>]");
|
|
2798
|
+
const adapter = s("adapter") ?? "primitive";
|
|
2799
|
+
const a = s("a")?.toLowerCase();
|
|
2800
|
+
const b = s("b")?.toLowerCase();
|
|
2801
|
+
if (!a || !b || a === b) throw new MapError2("give two different mailboxes the adapter can send from: --a <address> --b <address>");
|
|
2802
|
+
const r = await runConformance({
|
|
2803
|
+
adapter,
|
|
2804
|
+
a,
|
|
2805
|
+
b,
|
|
2806
|
+
host: s("repo-host") ?? defaultHost(),
|
|
2807
|
+
repo: s("repo"),
|
|
2808
|
+
handle: s("handle"),
|
|
2809
|
+
cliVersion,
|
|
2810
|
+
log: argv.flags.json ? (x) => process.stderr.write(`${x}
|
|
2811
|
+
`) : void 0
|
|
2812
|
+
});
|
|
2813
|
+
const md = markdown(r);
|
|
2814
|
+
const out = s("out");
|
|
2815
|
+
if (out) writeFileSync4(isAbsolute(out) ? out : resolve2(out), argv.flags.json ? `${JSON.stringify(r, null, 2)}
|
|
2816
|
+
` : md);
|
|
2817
|
+
process.stdout.write(argv.flags.json ? `${JSON.stringify(r, null, 2)}
|
|
2818
|
+
` : `
|
|
2819
|
+
${md}`);
|
|
2820
|
+
return r.passed ? 0 : 1;
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// src/history.ts
|
|
2824
|
+
var sentVisible = false;
|
|
2825
|
+
var SENT_VISIBLE_NOTE = "Search covers mail you received and mail you sent. It sees the repo's own mail (commit log, reviews, verdicts to others) only from the account that hosts the repo.";
|
|
2826
|
+
var NOT_VISIBLE = "Search reads inbound mail only. It cannot see mail you sent (your changes, arguments and replies) unless the repo copied it back to you, and it sees the repo's own mail (commit log, reviews, verdicts to others) only from the account that hosts the repo.";
|
|
2827
|
+
var NOISE = /* @__PURE__ */ new Set(["log", "snapshot", "status", "settings", "diff", "help", "received", "tick", "config", "confirm", "confirmed"]);
|
|
2828
|
+
var READS = /* @__PURE__ */ new Set(["log", "get", "status", "settings", "show", "diff"]);
|
|
2829
|
+
function gist(snippet) {
|
|
2830
|
+
return (snippet ?? "").replace(/\s*(?:Map|Gud)-[A-Za-z-]+:.*$/is, "").replace(/\s+/g, " ").trim().slice(0, 200);
|
|
2831
|
+
}
|
|
2832
|
+
function item(h) {
|
|
2833
|
+
const s = parseSubject(h.subject);
|
|
2834
|
+
if (s?.kind === "commit") {
|
|
2835
|
+
return { at: h.received_at, kind: "commit", who: s.fields.author ?? h.sender, what: `${s.id} ${gist(h.snippet)}`, why: s.fields.parent ? `on ${s.fields.parent}` : "", id: h.id };
|
|
2836
|
+
}
|
|
2837
|
+
return {
|
|
2838
|
+
at: h.received_at,
|
|
2839
|
+
kind: s?.kind ?? "mail",
|
|
2840
|
+
who: h.sender,
|
|
2841
|
+
what: s ? `${s.id ?? ""} ${s.rest}`.trim() : h.subject,
|
|
2842
|
+
why: gist(h.snippet),
|
|
2843
|
+
id: h.id
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
async function search(q) {
|
|
2847
|
+
const t = await transport();
|
|
2848
|
+
const r = await t.search(q);
|
|
2849
|
+
return { hits: r.data, command: r.command ?? `${t.name} search ${JSON.stringify({ ...q, me: void 0 })}`, unsupported: r.unsupported };
|
|
2850
|
+
}
|
|
2851
|
+
async function log(s, n) {
|
|
2852
|
+
const [now, legacy] = await Promise.all([
|
|
2853
|
+
search({ me: s.me, to: s.repo, subject: "map commit", sort: "received_at_desc", limit: n }),
|
|
2854
|
+
search({ me: s.me, to: s.repo, subject: "gud commit", sort: "received_at_desc", limit: n })
|
|
2855
|
+
]);
|
|
2856
|
+
const seen = new Set(now.hits.map((h) => h.id));
|
|
2857
|
+
const hits = [...now.hits, ...legacy.hits.filter((h) => !seen.has(h.id))].sort((a, b) => a.received_at < b.received_at ? 1 : a.received_at > b.received_at ? -1 : 0).slice(0, n);
|
|
2858
|
+
const searched = [now.command, ...legacy.hits.length ? [legacy.command] : []];
|
|
2859
|
+
const commits = hits.filter((h) => h.sender === s.repo && parseSubject(h.subject)?.kind === "commit").map((h) => {
|
|
2860
|
+
const p = parseSubject(h.subject);
|
|
2861
|
+
return { id: p.id, at: h.received_at, author: p.fields.author ?? "", message: gist(h.snippet), parent: p.fields.parent ?? "" };
|
|
2862
|
+
});
|
|
2863
|
+
if (commits.length) return { commands: searched, entries: commits, source: "mailbox" };
|
|
2864
|
+
const r = await s.logRead(n);
|
|
2865
|
+
return {
|
|
2866
|
+
commands: [...searched, `(no commit mail here; asked ${s.repo} with a "Map-Log: ${n}" mail)`],
|
|
2867
|
+
entries: parseLog(r.detail.body_text).entries.map((e) => ({ id: e.id, at: e.at, author: e.author, message: `${e.message}${e.pid ? ` (push ${e.pid})` : ""}`, parent: "" })),
|
|
2868
|
+
source: "repo"
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2871
|
+
async function history(s, term, limit = 30) {
|
|
2872
|
+
const q = term ? /[\s/.:]/.test(term) ? `"${term.replace(/"/g, "")}"` : term : void 0;
|
|
2873
|
+
const ask = Math.min(100, limit * 3);
|
|
2874
|
+
const all = await Promise.all([
|
|
2875
|
+
search({ me: s.me, to: s.repo, q, sort: "received_at_desc", limit: ask }),
|
|
2876
|
+
search({ me: s.me, from: s.repo, to: s.me, q, sort: "received_at_desc", limit: ask }),
|
|
2877
|
+
// What I sent the repo (my changes, arguments, replies), where the
|
|
2878
|
+
// provider can search sent mail.
|
|
2879
|
+
search({ me: s.me, from: s.me, to: s.repo, q, sort: "received_at_desc", limit: ask, direction: "outbound" })
|
|
2880
|
+
]);
|
|
2881
|
+
const runs = all.filter((r) => !r.unsupported);
|
|
2882
|
+
sentVisible = runs.length === all.length;
|
|
2883
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2884
|
+
const items = [];
|
|
2885
|
+
for (const r of runs) {
|
|
2886
|
+
for (const h of r.hits) {
|
|
2887
|
+
if (seen.has(h.id)) continue;
|
|
2888
|
+
seen.add(h.id);
|
|
2889
|
+
const k = parseSubject(h.subject)?.kind;
|
|
2890
|
+
if (k && NOISE.has(k)) continue;
|
|
2891
|
+
if (!k && READS.has(h.subject.trim().toLowerCase())) continue;
|
|
2892
|
+
items.push(item(h));
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
items.sort((a, b) => a.at < b.at ? 1 : -1);
|
|
2896
|
+
const shown2 = [];
|
|
2897
|
+
for (const i of items) {
|
|
2898
|
+
const dup = shown2.some((x) => x.kind === i.kind && x.what === i.what && Math.abs(Date.parse(x.at) - Date.parse(i.at)) < 3e4);
|
|
2899
|
+
if (!dup) shown2.push(i);
|
|
2900
|
+
}
|
|
2901
|
+
return { commands: runs.map((r) => r.command), items: shown2.slice(0, limit) };
|
|
2902
|
+
}
|
|
2903
|
+
function printHistory(items, commands, out) {
|
|
2904
|
+
out(`Searched:
|
|
2905
|
+
${commands.map((c) => ` ${c}`).join("\n")}
|
|
2906
|
+
`);
|
|
2907
|
+
if (!items.length) out("Nothing found.\n");
|
|
2908
|
+
for (const i of items) out(`${i.at.slice(0, 16).replace("T", " ")} ${i.kind.padEnd(10)} ${i.who}
|
|
2909
|
+
${i.what}${i.why ? `
|
|
2910
|
+
${i.why}` : ""}
|
|
2911
|
+
`);
|
|
2912
|
+
out(`
|
|
2913
|
+
${sentVisible ? SENT_VISIBLE_NOTE : NOT_VISIBLE}
|
|
2914
|
+
`);
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
// src/init.ts
|
|
2918
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
2919
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2920
|
+
import { homedir as homedir2 } from "node:os";
|
|
2921
|
+
import { join as join7 } from "node:path";
|
|
2922
|
+
import { createInterface } from "node:readline/promises";
|
|
2923
|
+
var PROVIDERS = [{ name: "primitive", label: "Primitive (primitive.dev)" }];
|
|
2924
|
+
var PROVIDER_NOTE = "Accepting PRs for other providers at primitivedotdev/gud (see CONFORMANCE.md).";
|
|
2925
|
+
function skillDirs(home) {
|
|
2926
|
+
return [
|
|
2927
|
+
{ dir: join7(home, ".agents", "skills", "map"), always: true },
|
|
2928
|
+
{ dir: join7(home, ".claude", "skills", "map"), always: false },
|
|
2929
|
+
{ dir: join7(home, ".codex", "skills", "map"), always: false }
|
|
2930
|
+
];
|
|
2931
|
+
}
|
|
2932
|
+
function installSkill(home, text2) {
|
|
2933
|
+
const out = [];
|
|
2934
|
+
for (const { dir, always } of skillDirs(home)) {
|
|
2935
|
+
const harness = join7(dir, "..", "..");
|
|
2936
|
+
if (!always && !existsSync3(harness)) continue;
|
|
2937
|
+
mkdirSync4(dir, { recursive: true });
|
|
2938
|
+
writeFileSync5(join7(dir, "SKILL.md"), text2);
|
|
2939
|
+
out.push(join7(dir, "SKILL.md"));
|
|
2940
|
+
}
|
|
2941
|
+
return out;
|
|
2942
|
+
}
|
|
2943
|
+
var say2 = (s = "") => process.stdout.write(`${s}
|
|
2944
|
+
`);
|
|
2945
|
+
var shown = (cmd) => cmd.map((x) => /^[\w@./=:-]+$/.test(x) ? x : `'${x}'`).join(" ");
|
|
2946
|
+
async function init(o) {
|
|
2947
|
+
const tty = !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
2948
|
+
const rl = tty ? createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
2949
|
+
const ask = async (q, dflt2) => rl ? (await rl.question(`${q} [${dflt2}] `)).trim() || dflt2 : dflt2;
|
|
2950
|
+
const confirm = async (q) => o.yes ? true : rl ? /^(y|yes|)$/i.test((await rl.question(`${q} [Y/n] `)).trim()) : false;
|
|
2951
|
+
try {
|
|
2952
|
+
say2("map init");
|
|
2953
|
+
say2();
|
|
2954
|
+
let provider = o.provider;
|
|
2955
|
+
if (!provider) {
|
|
2956
|
+
if (rl) {
|
|
2957
|
+
say2("Which email provider sends and reads your map mail?");
|
|
2958
|
+
PROVIDERS.forEach((p, i) => say2(` ${i + 1}. ${p.label}`));
|
|
2959
|
+
say2(` ${PROVIDER_NOTE}`);
|
|
2960
|
+
const pick = await ask("Provider", "1");
|
|
2961
|
+
provider = PROVIDERS[Number(pick) - 1]?.name ?? PROVIDERS.find((p) => p.name === pick)?.name;
|
|
2962
|
+
if (!provider) throw new MapError2(`no provider "${pick}"`);
|
|
2963
|
+
} else {
|
|
2964
|
+
provider = "primitive";
|
|
2965
|
+
say2(`Email provider: primitive (the only one so far; pass --provider to choose). ${PROVIDER_NOTE}`);
|
|
2966
|
+
}
|
|
2967
|
+
} else say2(`Email provider: ${provider}. ${PROVIDER_NOTE}`);
|
|
2968
|
+
process.env.MAP_TRANSPORT = provider;
|
|
2969
|
+
const t = await loadTransport(provider);
|
|
2970
|
+
let status = t.setup ? await t.setup() : { ready: true, steps: [] };
|
|
2971
|
+
for (const step of status.steps) {
|
|
2972
|
+
say2(`Needed: ${step.why}: ${shown(step.command)}`);
|
|
2973
|
+
const canRun = rl ? true : o.yes && !step.interactive;
|
|
2974
|
+
if (canRun && await confirm(`Run ${shown(step.command)} now?`)) {
|
|
2975
|
+
const code = await new Promise((res) => {
|
|
2976
|
+
const c = spawn3(step.command[0], step.command.slice(1), { stdio: "inherit" });
|
|
2977
|
+
c.on("close", (x) => res(x ?? 1));
|
|
2978
|
+
c.on("error", () => res(127));
|
|
2979
|
+
});
|
|
2980
|
+
if (code !== 0) say2(`That exited with ${code}.`);
|
|
2981
|
+
}
|
|
2982
|
+
status = t.setup ? await t.setup() : status;
|
|
2983
|
+
if (status.ready) break;
|
|
2984
|
+
}
|
|
2985
|
+
if (status.ready) say2(`Mail: ready${status.account ? `, logged in as ${status.account}` : ""}.`);
|
|
2986
|
+
else say2(`Mail: not ready yet. Run: ${status.steps.map((x) => shown(x.command)).join(" && ")}, then map init again.`);
|
|
2987
|
+
const which = await run2("sh", ["-c", "command -v map"]).catch(() => null);
|
|
2988
|
+
if (which && which.code === 0 && which.out.trim() && !o.binDir) say2(`map: already on PATH (${which.out.trim()}).`);
|
|
2989
|
+
else say2(await o.installBins(o.binDir));
|
|
2990
|
+
const current2 = readConfig().from ?? "";
|
|
2991
|
+
let address = o.from || process.env.MAP_FROM || status.address || current2 || "";
|
|
2992
|
+
if (!address && status.suggestedAddress) address = await ask("Send map mail from which address?", status.suggestedAddress);
|
|
2993
|
+
if (!address && rl) address = (await rl.question("Send map mail from which address? ")).trim();
|
|
2994
|
+
if (!address && o.yes && status.suggestedAddress) address = status.suggestedAddress;
|
|
2995
|
+
if (address && address !== current2) {
|
|
2996
|
+
if (status.address && status.address === address) say2(`Address: ${address} (from the connected agent; nothing to save).`);
|
|
2997
|
+
else if (await confirm(`Save ${address} as your map address in ${configPath()}?`)) {
|
|
2998
|
+
writeConfig({ from: address });
|
|
2999
|
+
say2(`Address: ${address} (saved in ${configPath()}).`);
|
|
3000
|
+
} else say2(`Address: ${address}. Pass --from ${address}, or run map init --from ${address} --yes to keep it.`);
|
|
3001
|
+
} else if (address) say2(`Address: ${address} (${configPath()}).`);
|
|
3002
|
+
else say2("Address: unknown. Run map init --from <address> --yes.");
|
|
3003
|
+
if (o.skill) {
|
|
3004
|
+
const paths = installSkill(o.home ?? homedir2(), o.skillText);
|
|
3005
|
+
say2("Skill installed at:");
|
|
3006
|
+
for (const p of paths) say2(` ${p}`);
|
|
3007
|
+
say2("Other harnesses: map skill prints it, to paste into a prompt or AGENTS.md.");
|
|
3008
|
+
}
|
|
3009
|
+
say2();
|
|
3010
|
+
if (address) say2(`You send as ${address}.`);
|
|
3011
|
+
say2(`Point your agent at a repo: <task> in owner-repo@${defaultHost()}`);
|
|
3012
|
+
return status.ready && address ? 0 : 1;
|
|
3013
|
+
} finally {
|
|
3014
|
+
rl?.close();
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
// src/patchgen.ts
|
|
3019
|
+
import { deflateSync as deflateSync2 } from "node:zlib";
|
|
3020
|
+
var B852 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~";
|
|
3021
|
+
function base85Lines(data) {
|
|
3022
|
+
const lines2 = [];
|
|
3023
|
+
for (let off = 0; off < data.length; off += 52) {
|
|
3024
|
+
const chunk = data.subarray(off, off + 52);
|
|
3025
|
+
const n = chunk.length;
|
|
3026
|
+
let line = n <= 26 ? String.fromCharCode(64 + n) : String.fromCharCode(70 + n);
|
|
3027
|
+
for (let i = 0; i < n; i += 4) {
|
|
3028
|
+
let acc = 0;
|
|
3029
|
+
for (let k = 0; k < 4; k++) acc = acc * 256 + (chunk[i + k] ?? 0);
|
|
3030
|
+
let s = "";
|
|
3031
|
+
for (let k = 0; k < 5; k++) {
|
|
3032
|
+
s = B852[acc % 85] + s;
|
|
3033
|
+
acc = Math.floor(acc / 85);
|
|
3034
|
+
}
|
|
3035
|
+
line += s;
|
|
3036
|
+
}
|
|
3037
|
+
lines2.push(line);
|
|
3038
|
+
}
|
|
3039
|
+
return lines2;
|
|
3040
|
+
}
|
|
3041
|
+
function literal(data) {
|
|
3042
|
+
return [`literal ${data.length}`, ...base85Lines(new Uint8Array(deflateSync2(data))), ""];
|
|
3043
|
+
}
|
|
3044
|
+
var same2 = (a, b) => a.length === b.length && Buffer.compare(Buffer.from(a), Buffer.from(b)) === 0;
|
|
3045
|
+
function makePatch(base, work) {
|
|
3046
|
+
const bin2 = new Set(binaryPaths(base, work));
|
|
3047
|
+
const textBase = new Map([...base].filter(([p]) => !bin2.has(p)));
|
|
3048
|
+
const textWork = new Map([...work].filter(([p]) => !bin2.has(p)));
|
|
3049
|
+
const out = [];
|
|
3050
|
+
const text2 = renderDiff(textBase, textWork);
|
|
3051
|
+
if (text2) out.push(text2.replace(/\n$/, ""));
|
|
3052
|
+
for (const p of [...bin2].sort()) {
|
|
3053
|
+
const a = base.get(p);
|
|
3054
|
+
const b = work.get(p);
|
|
3055
|
+
if (a && b && same2(a, b)) continue;
|
|
3056
|
+
out.push(`diff --git a/${p} b/${p}`);
|
|
3057
|
+
if (!a) out.push("new file mode 100644");
|
|
3058
|
+
if (!b) out.push("deleted file mode 100644");
|
|
3059
|
+
out.push("GIT binary patch", ...literal(b ?? new Uint8Array()), ...literal(a ?? new Uint8Array()));
|
|
3060
|
+
}
|
|
3061
|
+
return out.length ? `${out.join("\n")}
|
|
3062
|
+
` : "";
|
|
3063
|
+
}
|
|
3064
|
+
function changedPaths(base, work) {
|
|
3065
|
+
const added = [];
|
|
3066
|
+
const modified = [];
|
|
3067
|
+
const deleted = [];
|
|
3068
|
+
for (const [p, b] of work) {
|
|
3069
|
+
const a = base.get(p);
|
|
3070
|
+
if (!a) added.push(p);
|
|
3071
|
+
else if (!same2(a, b)) modified.push(p);
|
|
3072
|
+
}
|
|
3073
|
+
for (const p of base.keys()) if (!work.has(p)) deleted.push(p);
|
|
3074
|
+
return { added: added.sort(), modified: modified.sort(), deleted: deleted.sort() };
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
// src/session.ts
|
|
3078
|
+
import { existsSync as existsSync4, readdirSync as readdirSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
|
3079
|
+
import { join as join8 } from "node:path";
|
|
3080
|
+
|
|
3081
|
+
// src/mergework.ts
|
|
3082
|
+
var enc3 = new TextEncoder();
|
|
3083
|
+
var dec2 = new TextDecoder("utf-8", { fatal: true });
|
|
3084
|
+
function text(b) {
|
|
3085
|
+
if (!b) return "";
|
|
3086
|
+
try {
|
|
3087
|
+
const s = dec2.decode(b);
|
|
3088
|
+
return s.includes("\0") ? null : s;
|
|
3089
|
+
} catch {
|
|
3090
|
+
return null;
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
function lines(s) {
|
|
3094
|
+
return s.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
3095
|
+
}
|
|
3096
|
+
var nl = (s) => s === "" || s.endsWith("\n") ? s : `${s}
|
|
3097
|
+
`;
|
|
3098
|
+
function withMarkers(base, ours, theirs, conflicts, label) {
|
|
3099
|
+
const B = lines(base);
|
|
3100
|
+
const O = lines(ours);
|
|
3101
|
+
const T = lines(theirs);
|
|
3102
|
+
let b = 0;
|
|
3103
|
+
let o = 0;
|
|
3104
|
+
let t = 0;
|
|
3105
|
+
let out = "";
|
|
3106
|
+
const clean = (bs, be, os, oe, ts, te) => {
|
|
3107
|
+
const r = merge3(B.slice(bs, be).join(""), O.slice(os, oe).join(""), T.slice(ts, te).join(""));
|
|
3108
|
+
out += r.ok ? r.text : O.slice(os, oe).join("");
|
|
3109
|
+
};
|
|
3110
|
+
for (const c of conflicts) {
|
|
3111
|
+
const bs = c.baseRange.start - 1;
|
|
3112
|
+
const os = c.oursRange.start - 1;
|
|
3113
|
+
const ts = c.theirsRange.start - 1;
|
|
3114
|
+
clean(b, bs, o, os, t, ts);
|
|
3115
|
+
out += `<<<<<<< ${label.ours}
|
|
3116
|
+
${nl(c.ours)}=======
|
|
3117
|
+
${nl(c.theirs)}>>>>>>> ${label.theirs}
|
|
3118
|
+
`;
|
|
3119
|
+
b = bs + c.baseRange.count;
|
|
3120
|
+
o = os + c.oursRange.count;
|
|
3121
|
+
t = ts + c.theirsRange.count;
|
|
3122
|
+
}
|
|
3123
|
+
clean(b, B.length, o, O.length, t, T.length);
|
|
3124
|
+
return out;
|
|
3125
|
+
}
|
|
3126
|
+
function mergeWork(base, tip, work, label) {
|
|
3127
|
+
const whole = mergeTrees(base, tip, work);
|
|
3128
|
+
if (whole.ok) return { tree: whole.tree, conflicts: [] };
|
|
3129
|
+
const tree = /* @__PURE__ */ new Map();
|
|
3130
|
+
const conflicts = [];
|
|
3131
|
+
const same3 = (a, b) => !a && !b || !!a && !!b && a.length === b.length && Buffer.compare(Buffer.from(a), Buffer.from(b)) === 0;
|
|
3132
|
+
for (const p of /* @__PURE__ */ new Set([...base.keys(), ...tip.keys(), ...work.keys()])) {
|
|
3133
|
+
const B = base.get(p);
|
|
3134
|
+
const O = tip.get(p);
|
|
3135
|
+
const T = work.get(p);
|
|
3136
|
+
let v;
|
|
3137
|
+
if (same3(O, T)) v = O;
|
|
3138
|
+
else if (same3(B, O)) v = T;
|
|
3139
|
+
else if (same3(B, T)) v = O;
|
|
3140
|
+
else if (!O || !T) {
|
|
3141
|
+
v = O ?? T;
|
|
3142
|
+
conflicts.push({ path: p, reason: O ? "you deleted it and the tip changed it; kept the tip's version" : "the tip deleted it and you changed it; kept yours", markers: false });
|
|
3143
|
+
} else {
|
|
3144
|
+
const tb = B ? text(B) : "";
|
|
3145
|
+
const to = text(O);
|
|
3146
|
+
const tt = text(T);
|
|
3147
|
+
if (tb === null || to === null || tt === null) {
|
|
3148
|
+
v = T;
|
|
3149
|
+
conflicts.push({ path: p, reason: "binary file changed on both sides; kept yours, the tip's version is in the snapshot", markers: false });
|
|
3150
|
+
} else if (!B) {
|
|
3151
|
+
v = enc3.encode(`<<<<<<< ${label.ours}
|
|
3152
|
+
${nl(to)}=======
|
|
3153
|
+
${nl(tt)}>>>>>>> ${label.theirs}
|
|
3154
|
+
`);
|
|
3155
|
+
conflicts.push({ path: p, reason: "added on both sides with different content", markers: true });
|
|
3156
|
+
} else {
|
|
3157
|
+
const r = merge3(tb, to, tt);
|
|
3158
|
+
if (r.ok) v = enc3.encode(r.text);
|
|
3159
|
+
else {
|
|
3160
|
+
v = enc3.encode(withMarkers(tb, to, tt, r.conflicts, label));
|
|
3161
|
+
conflicts.push({ path: p, reason: `${r.conflicts.length} overlapping change${r.conflicts.length === 1 ? "" : "s"}`, markers: true });
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
if (v) tree.set(p, v);
|
|
3166
|
+
}
|
|
3167
|
+
return { tree, conflicts: conflicts.sort((a, b) => a.path < b.path ? -1 : 1) };
|
|
3168
|
+
}
|
|
3169
|
+
function hasMarkers(b) {
|
|
3170
|
+
const s = text(b);
|
|
3171
|
+
return !!s && /^<<<<<<< /m.test(s) && /^>>>>>>> /m.test(s);
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
// src/session.ts
|
|
3175
|
+
var MAX_GETS = 3;
|
|
3176
|
+
var PENDING_EXIT_STRICT = 75;
|
|
3177
|
+
function pendingExit() {
|
|
3178
|
+
return process.env.MAP_STRICT && process.env.MAP_STRICT !== "0" ? PENDING_EXIT_STRICT : 0;
|
|
3179
|
+
}
|
|
3180
|
+
function envNumber(name, dflt2) {
|
|
3181
|
+
const n = Number(process.env[name]);
|
|
3182
|
+
return Number.isFinite(n) && n > 0 ? n : dflt2;
|
|
3183
|
+
}
|
|
3184
|
+
function waitBudget() {
|
|
3185
|
+
if (process.env.MAP_WAIT_SECONDS) return envNumber("MAP_WAIT_SECONDS", 90);
|
|
3186
|
+
return process.stderr.isTTY ? 1800 : 90;
|
|
3187
|
+
}
|
|
3188
|
+
var Session = class {
|
|
3189
|
+
constructor(repo, me) {
|
|
3190
|
+
this.repo = repo;
|
|
3191
|
+
this.me = me;
|
|
3192
|
+
this.mail = new Mail(me);
|
|
3193
|
+
}
|
|
3194
|
+
mail;
|
|
3195
|
+
// Sends a mail of read commands and returns map's reply.
|
|
3196
|
+
async read(body, subject) {
|
|
3197
|
+
const sent = await this.mail.send({ to: this.repo, subject, body });
|
|
3198
|
+
const timeout = envNumber("MAP_READ_TIMEOUT", 180);
|
|
3199
|
+
for (let since; ; ) {
|
|
3200
|
+
const r = await this.mail.waitReply(sent, this.repo, { timeoutSec: timeout, since });
|
|
3201
|
+
if (!r) throw new MapError2(`no reply from ${this.repo} within ${timeout} s (sent email ${sent}). Is ${this.me} allowed to write to it?`);
|
|
3202
|
+
if (/^\[map not-allowed\]/.test(canonicalSubject(r.subject))) throw new MapError2(`${this.repo} does not accept mail from ${this.me}. Ask a repo owner to add your address to its rules.`);
|
|
3203
|
+
if (/^\[map (snapshot|log|diff|status|settings|help)\b/.test(canonicalSubject(r.subject))) return this.mail.get(r.id);
|
|
3204
|
+
since = after(r.received_at);
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
async logRead(n) {
|
|
3208
|
+
const detail = await this.read(`Map-Log: ${n}
|
|
3209
|
+
`, "log");
|
|
3210
|
+
return { ...parseLog(detail.body_text), detail };
|
|
3211
|
+
}
|
|
3212
|
+
// Snapshots of up to MAX_GETS commits per mail, by the full commit id.
|
|
3213
|
+
async snapshots(cids) {
|
|
3214
|
+
const out = /* @__PURE__ */ new Map();
|
|
3215
|
+
for (let i = 0; i < cids.length; i += MAX_GETS) {
|
|
3216
|
+
const batch = cids.slice(i, i + MAX_GETS);
|
|
3217
|
+
const d = await this.read(batch.map((c) => `Map-Get: ${c}`).join("\n") + "\n", "get");
|
|
3218
|
+
for (const cid of batch) {
|
|
3219
|
+
const full = batch.length === 1 ? parseSubject(d.subject)?.id ?? cid : cid;
|
|
3220
|
+
const att = d.attachments.find((a) => a.filename === `snapshot-${full}.tar.gz`) ?? (batch.length === 1 ? d.attachments.find((a) => a.filename === "snapshot.tar.gz") : void 0);
|
|
3221
|
+
if (!att) throw new MapError2(`map's reply did not carry the snapshot of ${cid}:
|
|
3222
|
+
${indent(d.body_text.slice(0, 1500))}`);
|
|
3223
|
+
const file = join8(tempDir2(), `snapshot-${full}.tar.gz`);
|
|
3224
|
+
out.set(full, unpackTarGz(new Uint8Array(await this.mail.download(d.id, att.part_index, file))));
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
return out;
|
|
3228
|
+
}
|
|
3229
|
+
// map get: a working copy of the tip (or `commit`) in `dir`.
|
|
3230
|
+
async get(dir, commit) {
|
|
3231
|
+
if (existsSync4(dir) && readdirSync3(dir).length) throw new MapError2(`${dir} exists and is not empty`);
|
|
3232
|
+
const want = commit ?? (await this.logRead(1)).tip;
|
|
3233
|
+
if (!want) throw new MapError2(`could not read the tip of ${this.repo}`);
|
|
3234
|
+
const [[cid, tree]] = [...await this.snapshots([want])];
|
|
3235
|
+
writeWork(dir, /* @__PURE__ */ new Map(), tree);
|
|
3236
|
+
writeBase(dir, tree);
|
|
3237
|
+
writeMeta(dir, { repo: this.repo, commit: cid, from: this.me, at: (/* @__PURE__ */ new Date()).toISOString(), pushes: [] });
|
|
3238
|
+
return { commit: cid, files: tree.size };
|
|
3239
|
+
}
|
|
3240
|
+
// map send: mails the change from the base to the folder and waits for the
|
|
3241
|
+
// verdict. Returns the exit status: 0 landed or pending (75 for pending with --strict), 1 not landed.
|
|
3242
|
+
async send(dir, subject, bodyText, o = {}) {
|
|
3243
|
+
const meta = readMeta(dir);
|
|
3244
|
+
const base = readBase(dir);
|
|
3245
|
+
const { tree, skipped } = readWork(dir, base);
|
|
3246
|
+
for (const s2 of skipped) say(`map: left out ${s2}`);
|
|
3247
|
+
const marked = (meta.conflicts ?? []).filter((p) => hasMarkers(tree.get(p)));
|
|
3248
|
+
if (marked.length) throw new MapError2(`resolve the conflict markers in ${marked.join(", ")} first (map update left them), then send`);
|
|
3249
|
+
const open = (meta.pushes ?? []).filter((p) => p.state === "pending");
|
|
3250
|
+
if (open.length && !o.force) {
|
|
3251
|
+
const p = open[open.length - 1];
|
|
3252
|
+
throw new MapError2(`${p.pid ?? "a push"} ("${p.subject}") is still pending, and this would send its change again. Run map wait ${p.pid ?? "<pid>"} first (or pass --force).`);
|
|
3253
|
+
}
|
|
3254
|
+
const patch = makePatch(base, tree);
|
|
3255
|
+
if (!patch) throw new MapError2(`nothing to send: the folder matches ${meta.commit}`);
|
|
3256
|
+
const intent = bodyText.split("\n").filter((l) => !/^\s*(?:map|gud)-[a-z-]+\s*(:|$)/i.test(l)).join("\n").trim();
|
|
3257
|
+
const body = `${intent ? `${intent}
|
|
3258
|
+
|
|
3259
|
+
` : ""}Map-Parent: ${meta.commit}
|
|
3260
|
+
`;
|
|
3261
|
+
const file = join8(tempDir2(), "change.patch");
|
|
3262
|
+
writeFileSync6(file, patch);
|
|
3263
|
+
const s = oneLine(subject, 150) || "Change sent with map";
|
|
3264
|
+
const sent = await this.mail.send({ to: this.repo, subject: s, body, attachment: file });
|
|
3265
|
+
const startedAt = new Date(Date.now() - 5e3).toISOString();
|
|
3266
|
+
const entry = { sent, subject: s, parent: meta.commit, at: startedAt, state: "pending" };
|
|
3267
|
+
writeSent(dir, sent, tree);
|
|
3268
|
+
meta.pushes = [...meta.pushes ?? [], entry];
|
|
3269
|
+
writeMeta(dir, meta);
|
|
3270
|
+
const c = changedPaths(base, tree);
|
|
3271
|
+
say(`map: sent "${s}" to ${this.repo}: ${c.added.length + c.modified.length + c.deleted.length} files changed on ${meta.commit}.`);
|
|
3272
|
+
if (process.env.MAP_NO_WAIT && process.env.MAP_NO_WAIT !== "0") {
|
|
3273
|
+
const ack = await this.findAck(s, startedAt, envNumber("MAP_ACK_TIMEOUT", 120));
|
|
3274
|
+
if (ack) this.save(dir, { ...entry, pid: ack });
|
|
3275
|
+
await this.pendingNote(ack, sent);
|
|
3276
|
+
return pendingExit();
|
|
3277
|
+
}
|
|
3278
|
+
const budget = waitBudget();
|
|
3279
|
+
say(`map: waiting for the verdict (up to ${budget} s here; review usually takes 1 to 3 minutes).`);
|
|
3280
|
+
const v = await this.waitVerdict(sent, budget);
|
|
3281
|
+
if (!v) {
|
|
3282
|
+
const ack = await this.findAck(s, startedAt, 5).catch(() => null);
|
|
3283
|
+
if (ack) this.save(dir, { ...entry, pid: ack });
|
|
3284
|
+
await this.pendingNote(ack, sent);
|
|
3285
|
+
return pendingExit();
|
|
3286
|
+
}
|
|
3287
|
+
return this.settle(dir, { ...entry, pid: v.verdict.pid }, v.verdict, v.mail);
|
|
3288
|
+
}
|
|
3289
|
+
// Records a verdict on a push in the working copy and prints it.
|
|
3290
|
+
settle(dir, entry, v, m) {
|
|
3291
|
+
if (dir && entry) {
|
|
3292
|
+
if (v.kind === "landed") this.save(dir, { ...entry, state: "landed" });
|
|
3293
|
+
else if (v.kind === "objected" || v.kind === "escalated") this.save(dir, { ...entry, state: "objected", seen: m.received_at });
|
|
3294
|
+
else {
|
|
3295
|
+
this.save(dir, { ...entry, state: "closed" });
|
|
3296
|
+
dropSent(dir, entry.sent);
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
return this.report(v);
|
|
3300
|
+
}
|
|
3301
|
+
save(dir, e) {
|
|
3302
|
+
const meta = readMeta(dir);
|
|
3303
|
+
meta.pushes = (meta.pushes ?? []).map((p) => p.sent === e.sent ? e : p);
|
|
3304
|
+
if (e.state === "closed") meta.pushes = meta.pushes.filter((p) => p.sent !== e.sent);
|
|
3305
|
+
writeMeta(dir, meta);
|
|
3306
|
+
}
|
|
3307
|
+
// map update: the new tip, with local edits carried onto it.
|
|
3308
|
+
async update(dir) {
|
|
3309
|
+
const meta = readMeta(dir);
|
|
3310
|
+
const base = readBase(dir);
|
|
3311
|
+
const { tree: work } = readWork(dir, base);
|
|
3312
|
+
const log2 = await this.logRead(20);
|
|
3313
|
+
if (!log2.tip) throw new MapError2(`could not read the log of ${this.repo}`);
|
|
3314
|
+
const at = log2.entries.findIndex((e) => e.id === meta.commit);
|
|
3315
|
+
const fresh = at === -1 ? log2.entries : log2.entries.slice(0, at);
|
|
3316
|
+
const landedPids = new Set(fresh.map((e) => e.pid).filter(Boolean));
|
|
3317
|
+
let mergeBase = base;
|
|
3318
|
+
const done = [];
|
|
3319
|
+
for (const p of meta.pushes ?? []) {
|
|
3320
|
+
if (p.pid && landedPids.has(p.pid)) {
|
|
3321
|
+
done.push(p);
|
|
3322
|
+
mergeBase = readSent(dir, p.sent) ?? mergeBase;
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
if (log2.tip === meta.commit) {
|
|
3326
|
+
const left2 = makePatch(base, work) ? "local edits not sent yet (map diff shows them)" : "nothing left to send";
|
|
3327
|
+
say(`map: already at the tip ${meta.commit}; ${left2}.`);
|
|
3328
|
+
return 0;
|
|
3329
|
+
}
|
|
3330
|
+
const tipTree = (await this.snapshots([log2.tip])).get(log2.tip);
|
|
3331
|
+
const { tree, conflicts } = mergeWork(mergeBase, tipTree, work, { ours: `tip ${log2.tip}`, theirs: "your edits" });
|
|
3332
|
+
writeWork(dir, work, tree);
|
|
3333
|
+
writeBase(dir, tipTree);
|
|
3334
|
+
for (const p of done) dropSent(dir, p.sent);
|
|
3335
|
+
const next = {
|
|
3336
|
+
...meta,
|
|
3337
|
+
commit: log2.tip,
|
|
3338
|
+
conflicts: conflicts.filter((c) => c.markers).map((c) => c.path),
|
|
3339
|
+
pushes: (meta.pushes ?? []).filter((p) => !done.includes(p))
|
|
3340
|
+
};
|
|
3341
|
+
writeMeta(dir, next);
|
|
3342
|
+
const between = fresh.length ? fresh.map((e) => `${e.id} ${e.message}`).reverse() : [log2.tip];
|
|
3343
|
+
say(`map: updated ${meta.commit} -> ${log2.tip}:`);
|
|
3344
|
+
for (const l of between) say(` ${l}`);
|
|
3345
|
+
if (done.length) say(`map: your ${done.map((p) => p.pid).join(", ")} landed in it.`);
|
|
3346
|
+
if (conflicts.length) {
|
|
3347
|
+
say("map: CONFLICT: your edits overlap changes at the tip in:");
|
|
3348
|
+
for (const c of conflicts) say(` ${c.path}: ${c.reason}${c.markers ? " (marked with <<<<<<< ======= >>>>>>>)" : ""}`);
|
|
3349
|
+
say("map: fix those files, then map send again.");
|
|
3350
|
+
return 1;
|
|
3351
|
+
}
|
|
3352
|
+
const left = makePatch(tipTree, tree);
|
|
3353
|
+
say(left ? `map: your edits not sent yet are carried over (map diff shows them).` : "map: the folder matches the tip; nothing left to send.");
|
|
3354
|
+
return 0;
|
|
3355
|
+
}
|
|
3356
|
+
// The verdict on a push: the first reply to it that is not a stuck note.
|
|
3357
|
+
async waitVerdict(sentId, timeoutSec, from) {
|
|
3358
|
+
const until = Date.now() + timeoutSec * 1e3;
|
|
3359
|
+
let since = from;
|
|
3360
|
+
for (; ; ) {
|
|
3361
|
+
const left = Math.round((until - Date.now()) / 1e3);
|
|
3362
|
+
if (left <= 0) return null;
|
|
3363
|
+
const r = await this.mail.waitReply(sentId, this.repo, { timeoutSec: left, since });
|
|
3364
|
+
if (!r) return null;
|
|
3365
|
+
since = after(r.received_at);
|
|
3366
|
+
const d = await this.mail.get(r.id);
|
|
3367
|
+
const v = parseVerdict(d.subject, d.body_text);
|
|
3368
|
+
if (v.kind === "stuck" || v.kind === "other") continue;
|
|
3369
|
+
return { verdict: v, mail: d };
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
// The push id from map's acknowledgement of the push with this subject.
|
|
3373
|
+
async findAck(subject, since, timeoutSec) {
|
|
3374
|
+
const until = Date.now() + timeoutSec * 1e3;
|
|
3375
|
+
const want = oneLine(subject, 1e5);
|
|
3376
|
+
for (; ; ) {
|
|
3377
|
+
const { data } = await this.mail.list({ after: since, limit: 50 });
|
|
3378
|
+
for (const m of data) {
|
|
3379
|
+
if (m.sender !== this.repo) continue;
|
|
3380
|
+
const x = /^\[map received (p-[0-9a-f]{8})\]\s*(.*)$/.exec(canonicalSubject(m.subject));
|
|
3381
|
+
if (x && x[2].trim() === want) return x[1];
|
|
3382
|
+
}
|
|
3383
|
+
if (Date.now() >= until) return null;
|
|
3384
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
// What a push waits on (from Map-Status) and how to keep waiting.
|
|
3388
|
+
async pendingNote(pid, sent) {
|
|
3389
|
+
if (!pid) {
|
|
3390
|
+
say(`map: PENDING: the change was sent (email ${sent}) but map has not acknowledged it yet. Do not send it again; check map inbox in a minute.`);
|
|
3391
|
+
return;
|
|
3392
|
+
}
|
|
3393
|
+
let state = "waiting for review";
|
|
3394
|
+
try {
|
|
3395
|
+
const st = parseStatus((await this.read("Map-Status\n", "status")).body_text);
|
|
3396
|
+
const p = st.pushes.find((x) => x.pid === pid);
|
|
3397
|
+
if (p) state = p.state.replace(/(round (\d+)\).*), round \2$/, "$1");
|
|
3398
|
+
} catch {
|
|
3399
|
+
}
|
|
3400
|
+
say(`map: PENDING ${pid}: ${state}. Not landed yet; nothing is wrong and there is nothing to resend.`);
|
|
3401
|
+
say(`map: run \`map wait ${pid}\` to keep waiting (it returns the verdict, or this line again after about 90 s).`);
|
|
3402
|
+
}
|
|
3403
|
+
// map wait <pid>: the verdict, or null when none came in time. A verdict
|
|
3404
|
+
// that arrived before the call is found too.
|
|
3405
|
+
async waitFor(pid, seconds, entry) {
|
|
3406
|
+
if (entry) return this.waitVerdict(entry.sent, seconds, entry.seen ? after(entry.seen) : void 0);
|
|
3407
|
+
const until = Date.now() + seconds * 1e3;
|
|
3408
|
+
const kinds = /* @__PURE__ */ new Set(["landed", "conflict", "objected", "rejected", "vetoed", "check-failed", "escalated", "not-allowed"]);
|
|
3409
|
+
const checked = /* @__PURE__ */ new Set();
|
|
3410
|
+
for (; ; ) {
|
|
3411
|
+
const { data } = await this.mail.list({ limit: 100 });
|
|
3412
|
+
for (const m of data) {
|
|
3413
|
+
if (m.sender !== this.repo || checked.has(m.id)) continue;
|
|
3414
|
+
const s = parseSubject(m.subject);
|
|
3415
|
+
if (!s || !kinds.has(s.kind)) continue;
|
|
3416
|
+
if (s.kind !== "landed" && s.id !== pid) continue;
|
|
3417
|
+
checked.add(m.id);
|
|
3418
|
+
const d = await this.mail.get(m.id);
|
|
3419
|
+
const v = parseVerdict(d.subject, d.body_text);
|
|
3420
|
+
if (v.pid === pid) return { verdict: v, mail: d };
|
|
3421
|
+
}
|
|
3422
|
+
if (Date.now() >= until) return null;
|
|
3423
|
+
await new Promise((r) => setTimeout(r, Math.min(5e3, Math.max(0, until - Date.now()))));
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
// Prints a verdict. Returns 0 when it landed, else 1.
|
|
3427
|
+
report(v) {
|
|
3428
|
+
const pid = v.pid ?? "your change";
|
|
3429
|
+
switch (v.kind) {
|
|
3430
|
+
case "landed":
|
|
3431
|
+
say(`map: LANDED ${pid} as ${v.cid}${v.merged ? ", merged with changes that landed first" : ""}.`);
|
|
3432
|
+
say(`map: run \`map update\` to bring the folder to ${v.cid}.`);
|
|
3433
|
+
return 0;
|
|
3434
|
+
case "conflict":
|
|
3435
|
+
say(`map: CONFLICT on ${pid}. Nothing landed. Changes that landed first${v.tip ? ` (tip ${v.tip})` : ""} touch the same lines:`);
|
|
3436
|
+
say(indent(v.overlaps ?? "", " "));
|
|
3437
|
+
say("map: run `map update` (it carries your edits onto the tip and marks the overlaps), fix the marked files, then map send again.");
|
|
3438
|
+
return 1;
|
|
3439
|
+
case "objected":
|
|
3440
|
+
say(`map: OBJECTED: ${v.who ?? "the reviewer"} objected to ${pid}${v.round ? ` (round ${v.round} of 3)` : ""}. Nothing landed.`);
|
|
3441
|
+
if (v.reason) say(`map: their reason (untrusted text):
|
|
3442
|
+
${indent(v.reason, " ")}`);
|
|
3443
|
+
if (v.lastRound) say(`map: that was the last round: arguing will not reopen it. Change the files and map send again, or ask a repo owner to approve ${pid}.`);
|
|
3444
|
+
else {
|
|
3445
|
+
say(`map: to argue: map argue ${pid} "<why the objection is wrong>", then map wait ${pid}`);
|
|
3446
|
+
say("map: or revise: change the files and map send again (a new push, reviewed with this objection in view).");
|
|
3447
|
+
}
|
|
3448
|
+
return 1;
|
|
3449
|
+
case "rejected":
|
|
3450
|
+
say(`map: REJECTED ${pid}: ${v.reason ?? "map could not read the patch"}. Nothing landed. Fix it and map send again.`);
|
|
3451
|
+
return 1;
|
|
3452
|
+
case "vetoed":
|
|
3453
|
+
say(`map: VETOED: a repo owner${v.who ? `, ${v.who},` : ""} vetoed ${pid}. It will not land.${v.reason ? `
|
|
3454
|
+
${indent(v.reason, " ")}` : ""}`);
|
|
3455
|
+
return 1;
|
|
3456
|
+
case "check-failed":
|
|
3457
|
+
say(`map: CHECK FAILED: ${v.who ?? "a required checker"} reported that ${pid} fails its checks. Nothing landed.${v.reason ? `
|
|
3458
|
+
${indent(v.reason, " ")}` : ""}`);
|
|
3459
|
+
return 1;
|
|
3460
|
+
case "escalated":
|
|
3461
|
+
say(`map: ESCALATED: line owners still object to ${pid} after round 3 (${v.who ?? "see your mail"}). The repo owners decide; map wait ${pid} returns their answer.`);
|
|
3462
|
+
return 1;
|
|
3463
|
+
case "not-allowed":
|
|
3464
|
+
say(`map: NOT ALLOWED: ${this.repo} does not accept changes from ${this.me}. Ask a repo owner to add your address.`);
|
|
3465
|
+
return 1;
|
|
3466
|
+
default:
|
|
3467
|
+
say(`map: unexpected reply about ${pid}`);
|
|
3468
|
+
return 1;
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
};
|
|
3472
|
+
async function createRepoByEmail(mail, o) {
|
|
3473
|
+
const newAddr = `new@${o.host}`;
|
|
3474
|
+
let handle = o.handle;
|
|
3475
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3476
|
+
const lines2 = [`Map-Create: ${o.name}`, ...handle ? [`Map-Handle: ${handle}`] : [], ...o.allow.map((a) => `Map-Allow: ${a}`)];
|
|
3477
|
+
const sent = await mail.send({ to: newAddr, subject: `create ${o.name}`, body: `${lines2.join("\n")}
|
|
3478
|
+
` });
|
|
3479
|
+
const timeoutSec = o.timeoutSec ?? 180;
|
|
3480
|
+
const from = [...handle ? [`${handle}-${o.name}@${o.host}`] : [], newAddr];
|
|
3481
|
+
const until = Date.now() + timeoutSec * 1e3;
|
|
3482
|
+
let r = null;
|
|
3483
|
+
for (let i = 0; !r && Date.now() < until; i++) r = await mail.waitReply(sent, from[i % from.length], { timeoutSec: 8 });
|
|
3484
|
+
if (!r) throw new MapError2(`no answer from ${newAddr} within ${timeoutSec} s`);
|
|
3485
|
+
const d = await mail.get(r.id);
|
|
3486
|
+
const created = /^\[map created ([a-z0-9]+-[a-z0-9]+)\]/.exec(canonicalSubject(d.subject));
|
|
3487
|
+
if (created) return { address: `${created[1]}@${o.host}`, handle: created[1].split("-")[0] };
|
|
3488
|
+
const reason = /^Reason: (.*)$/m.exec(d.body_text)?.[1] ?? d.subject;
|
|
3489
|
+
const mine = /already has the handle ([a-z][a-z0-9]+)/.exec(reason)?.[1];
|
|
3490
|
+
if (mine && attempt === 0) {
|
|
3491
|
+
handle = mine;
|
|
3492
|
+
continue;
|
|
3493
|
+
}
|
|
3494
|
+
throw new MapError2(`create failed: ${reason}`);
|
|
3495
|
+
}
|
|
3496
|
+
throw new MapError2("create failed");
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
// ../../skill/SKILL.md
|
|
3500
|
+
var SKILL_default = '---\nname: map\ndescription: Work on a map repo, a code host where each repo is an email address like owner-repo@map.dev, with plain files and email. Use when a task names a map.dev repo address, or when mail with a "[map ...]" subject arrives (verdicts, consent asks, objections).\n---\n\n# map\n\nA map repo is an email address, `owner-repo@map.dev`, and everything about it\nis email: you get the code as a mail attachment, send changes as a patch by\nmail, and the verdicts, reviews and history are mail too. The `map` command\ndoes that mail for you. There is no git remote.\n\n## Setup (once)\n\n```sh\nnpx -y @primitivedotdev/map init --yes # mail login check, PATH, your address, this skill\n```\n\n`map whoami` prints the address you send from. In a session where Primitive\ngave you an address, that is it: map uses `$PRIMITIVE_AGENT_ADDRESS`, so\nseveral agents on one machine each send as themselves with nothing to set.\n\n## The flow\n\n```sh\nmap get owner-repo@map.dev work && cd work # the code at the tip, in a folder\n# edit the files\nmap diff # what you would send (no network)\nmap send "Add retries to the fetcher" # mails the change, waits for the verdict\n```\n\n`map send` waits about 90 s. If map has not decided by then it prints\n`map: PENDING p-1a2b3c4d: ...`: nothing is wrong. Run `map wait p-1a2b3c4d`\nuntil it returns a verdict (each call waits about 90 s and finds a verdict\nthat arrived in between). Never send the same change again while it is\npending. If your harness wakes you on new mail you may stop after sending and\nrun `map wait <pid>` or `map inbox` when woken.\n\nExit codes of send and wait: 0 landed or still pending (read the PENDING line), 1 not landed (objected, conflict, rejected). Scripts can pass --strict to get 75 for pending.\n\n## The verdict\n\n- **LANDED** `as c4-...`: run `map update`. The folder then matches the tip\n and nothing is left to send.\n- **CONFLICT**: a change that landed first touches the same lines. Run\n `map update`: it merges the tip into your folder and marks each overlap\n with `<<<<<<<`, `=======`, `>>>>>>>`. Fix those files, then `map send` again.\n- **OBJECTED**: the reviewer or a line owner said no; the output says who,\n why, and the round (of 3).\n - Argue with `map argue <pid> "<why>"` when the objection misreads the\n change or is wrong; say exactly why. Then `map wait <pid>`.\n - Revise (edit, `map send` again) when the objection is right or cheap to\n satisfy. After round 3 only a revision or a repo owner helps.\n- **REJECTED**: map could not use the patch; fix what it names and send again.\n- **VETOED**, **CHECK FAILED**, **ESCALATED**: follow the message.\n\nBefore starting new work on an old folder, run `map update`.\n\n## When map asks you\n\n`map inbox` lists map mail that needs you (marked `*`) with the command to\nanswer it.\n\n- Someone changed lines you wrote (a consent ask): read it first with\n `map show <pid>`, which prints the part touching your lines and then the\n whole change. Then `map approve <pid>` to let it land, or\n `map object <pid> "<reason>"` to stop it. Silence lets it land.\n- An objection to your change: argue or revise, as above.\n- Repo owners: `map approve <pid>` lands over objections; `map veto <pid> "<reason>"` closes it.\n\n## History is the mailbox\n\n```sh\nmap log # the repo\'s commits\nmap history src/fetch.py # commits, verdicts, objections and arguments about a path\nmap history "retry" # ... or about some words\nmap status # the tip and open changes\n```\n\nEach prints the mail search it ran, for example\n\n```sh\nprimitive emails search --q \'"src/fetch.py"\' --from owner-repo@map.dev --to you@example.com\n```\n\nwhich you can run yourself and refine (`--subject`, `--body`, `--thread-id`).\nSearch sees mail you received; the repo\'s own commit mail is visible only to\nthe account that hosts the repo, so `map log` asks the repo by mail when your\nmailbox has none.\n';
|
|
3501
|
+
|
|
3502
|
+
// src/main.ts
|
|
3503
|
+
var VERSION = "0.2.0";
|
|
3504
|
+
var HELP = `map: work on a map repo (a repo is an email address) with files and mail.
|
|
3505
|
+
|
|
3506
|
+
New here? Run: npx -y @primitivedotdev/map init
|
|
3507
|
+
|
|
3508
|
+
map get owner-repo@map.dev [dir] the code at the tip, in a folder
|
|
3509
|
+
(edit files)
|
|
3510
|
+
map send "<what you changed>" mail the change and wait for the verdict
|
|
3511
|
+
map wait <pid> if send said PENDING, keep waiting (about 90 s a call)
|
|
3512
|
+
map update bring the folder to the new tip, keeping your edits
|
|
3513
|
+
|
|
3514
|
+
Working copy (the folder holds .map/ with the base it came from):
|
|
3515
|
+
map get <repo> [dir] [--commit cN] unpack the tip (or cN) into dir
|
|
3516
|
+
map diff [dir] the patch from the base to the files now
|
|
3517
|
+
map send "<subject>" [dir] [--body-file f]
|
|
3518
|
+
exit 0 landed or pending, 1 not landed (--strict: 75 pending)
|
|
3519
|
+
map update [dir] merge the new tip in; overlaps get conflict markers
|
|
3520
|
+
map wait <pid> [--seconds N] the verdict on a sent change (exit codes as above)
|
|
3521
|
+
|
|
3522
|
+
History is mail (each prints the mail search it ran):
|
|
3523
|
+
map log [repo] [-n N] the repo's commits
|
|
3524
|
+
map history [path or words] [--repo r]
|
|
3525
|
+
commits, verdicts, objections and arguments
|
|
3526
|
+
map status [repo] the tip and open pushes, asked by mail
|
|
3527
|
+
map show <commit> [repo] what a commit changed, asked by mail
|
|
3528
|
+
map show <push id> the newest map mail to you about a push, in full (a consent ask with its diff)
|
|
3529
|
+
|
|
3530
|
+
Reviews and consent:
|
|
3531
|
+
map inbox [--wait] [--all] map mail that asks you to act, and recent verdicts
|
|
3532
|
+
map approve <pid> [reason] let a push land (asked for consent, or repo owner)
|
|
3533
|
+
map object <pid> "<reason>" stop a push that changes your lines
|
|
3534
|
+
map argue <pid> "<text>" answer an objection to your push
|
|
3535
|
+
map veto <pid> "<reason>" close a push (repo owners)
|
|
3536
|
+
|
|
3537
|
+
Repos and setup:
|
|
3538
|
+
map create <name> [--handle h] [--allow a@x.com,*@y.com] [--host map.dev]
|
|
3539
|
+
map settings [repo] ["Map-...: v"] read settings; owners change them with lines
|
|
3540
|
+
map init [--provider primitive] [--yes] [--from addr]
|
|
3541
|
+
mail login, PATH, your address and the skill
|
|
3542
|
+
map skill print the agent skill
|
|
3543
|
+
map whoami the address map sends from
|
|
3544
|
+
map conformance transport --adapter <primitive|path> --a <addr> --b <addr>
|
|
3545
|
+
|
|
3546
|
+
Your address: --from, MAP_FROM, the working copy, or map init's config.
|
|
3547
|
+
Without a terminal map send waits about 90 s (MAP_WAIT_SECONDS), then prints
|
|
3548
|
+
"map: PENDING <pid>"; MAP_NO_WAIT=1 returns after the acknowledgement.
|
|
3549
|
+
`;
|
|
3550
|
+
function printJson(x) {
|
|
3551
|
+
process.stdout.write(`${JSON.stringify(x, null, 2)}
|
|
3552
|
+
`);
|
|
3553
|
+
}
|
|
3554
|
+
async function session(p, repoArg) {
|
|
3555
|
+
const repo = await resolveRepo(str(p, "repo") ?? repoArg);
|
|
3556
|
+
return new Session(repo, await resolveMe(str(p, "from")));
|
|
3557
|
+
}
|
|
3558
|
+
function textArg(p, from) {
|
|
3559
|
+
const file = str(p, "body-file");
|
|
3560
|
+
if (file) return readFileSync5(file === "-" ? 0 : file, "utf8");
|
|
3561
|
+
return p.pos.slice(from).join(" ");
|
|
3562
|
+
}
|
|
3563
|
+
function workdir(arg) {
|
|
3564
|
+
const d = arg ? findWorkdir(resolve3(arg)) : findWorkdir();
|
|
3565
|
+
if (d) return d;
|
|
3566
|
+
if (!arg) {
|
|
3567
|
+
const inside = readdirSync4(process.cwd(), { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync5(metaPath(join9(process.cwd(), e.name)))).map((e) => join9(process.cwd(), e.name));
|
|
3568
|
+
if (inside.length === 1) {
|
|
3569
|
+
process.stderr.write(`map: using the working copy in ${inside[0]}
|
|
3570
|
+
`);
|
|
3571
|
+
return inside[0];
|
|
3572
|
+
}
|
|
3573
|
+
if (inside.length > 1) throw new MapError2(`this folder is not a map working copy, and it holds several: ${inside.join(", ")}. Run the command in one of them, or pass its path.`);
|
|
3574
|
+
}
|
|
3575
|
+
throw new MapError2(`${arg ?? "this folder"} is not a map working copy. Make one with: map get <repo> [dir]`);
|
|
3576
|
+
}
|
|
3577
|
+
function itemLine(i) {
|
|
3578
|
+
const when = i.at.slice(0, 16).replace("T", " ");
|
|
3579
|
+
return `${i.action ? "*" : " "} ${(i.pid ?? "-").padEnd(12)} ${i.kind.padEnd(12)} ${when} ${i.repo}
|
|
3580
|
+
${i.subject}
|
|
3581
|
+
${i.hint.replace(/<pid>/g, i.pid ?? "<pid>")}`;
|
|
3582
|
+
}
|
|
3583
|
+
async function readCmd(p) {
|
|
3584
|
+
let body = "";
|
|
3585
|
+
let repoArg;
|
|
3586
|
+
if (p.cmd === "status") {
|
|
3587
|
+
body = "Map-Status";
|
|
3588
|
+
repoArg = p.pos[0];
|
|
3589
|
+
} else if (p.cmd === "settings") {
|
|
3590
|
+
const lines2 = p.pos.filter((x) => /^map-[a-z-]+\s*:/i.test(x));
|
|
3591
|
+
body = lines2.length ? lines2.join("\n") : "Map-Settings";
|
|
3592
|
+
repoArg = p.pos.find((x) => !lines2.includes(x));
|
|
3593
|
+
} else {
|
|
3594
|
+
const c = (p.pos[0] ?? "").toLowerCase();
|
|
3595
|
+
if (!CID.test(c)) throw new MapError2("usage: map show <commit> [repo], for example map show c3");
|
|
3596
|
+
body = `Map-Diff: ${c}`;
|
|
3597
|
+
repoArg = p.pos[1];
|
|
3598
|
+
}
|
|
3599
|
+
const s = await session(p, repoArg);
|
|
3600
|
+
const d = await s.read(`${body}
|
|
3601
|
+
`, p.cmd);
|
|
3602
|
+
const text2 = stripReadHelp(d.body_text);
|
|
3603
|
+
if (bool(p, "json")) printJson({ repo: s.repo, subject: d.subject, email: d.id, ...p.cmd === "status" ? parseStatus(text2) : {}, text: text2 });
|
|
3604
|
+
else process.stdout.write(`${text2}
|
|
3605
|
+
`);
|
|
3606
|
+
return 0;
|
|
3607
|
+
}
|
|
3608
|
+
async function getCmd(p) {
|
|
3609
|
+
const repo = parseRepo(p.pos[0] ?? "");
|
|
3610
|
+
const dir = resolve3(p.pos[1] ?? repo.split("@")[0].split("-").slice(1).join("-"));
|
|
3611
|
+
const commit = str(p, "commit");
|
|
3612
|
+
if (commit && !CID.test(commit)) throw new MapError2(`--commit "${commit}" is not a commit id (like c3)`);
|
|
3613
|
+
const s = new Session(repo, await resolveMe(str(p, "from")));
|
|
3614
|
+
const r = await s.get(dir, commit);
|
|
3615
|
+
if (bool(p, "json")) printJson({ repo, dir, ...r });
|
|
3616
|
+
else say(`map: ${r.commit} of ${repo} is in ${dir} (${r.files} files). Edit the files, then: map send "<what you changed>"`);
|
|
3617
|
+
return 0;
|
|
3618
|
+
}
|
|
3619
|
+
function diffCmd(p) {
|
|
3620
|
+
const dir = workdir(p.pos[0]);
|
|
3621
|
+
const base = readBase(dir);
|
|
3622
|
+
const { tree, skipped } = readWork(dir, base);
|
|
3623
|
+
for (const x of skipped) say(`map: left out ${x}`);
|
|
3624
|
+
const patch = makePatch(base, tree);
|
|
3625
|
+
if (bool(p, "json")) {
|
|
3626
|
+
printJson({ dir, base: readMeta(dir).commit, ...changedPaths(base, tree), patch });
|
|
3627
|
+
return 0;
|
|
3628
|
+
}
|
|
3629
|
+
if (!patch) say(`map: no changes since ${readMeta(dir).commit}`);
|
|
3630
|
+
else process.stdout.write(patch);
|
|
3631
|
+
return 0;
|
|
3632
|
+
}
|
|
3633
|
+
async function sendCmd(p) {
|
|
3634
|
+
const subject = p.pos[0] ?? "";
|
|
3635
|
+
if (!subject.trim()) throw new MapError2('say what you changed: map send "<subject>" [dir]');
|
|
3636
|
+
const dir = workdir(p.pos[1]);
|
|
3637
|
+
const meta = readMeta(dir);
|
|
3638
|
+
const s = new Session(meta.repo, await resolveMe(str(p, "from")));
|
|
3639
|
+
const file = str(p, "body-file");
|
|
3640
|
+
return s.send(dir, subject, file ? readFileSync5(file === "-" ? 0 : file, "utf8") : "", { force: bool(p, "force") });
|
|
3641
|
+
}
|
|
3642
|
+
async function updateCmd(p) {
|
|
3643
|
+
const dir = workdir(p.pos[0]);
|
|
3644
|
+
const s = new Session(readMeta(dir).repo, await resolveMe(str(p, "from")));
|
|
3645
|
+
return s.update(dir);
|
|
3646
|
+
}
|
|
3647
|
+
async function waitCmd(p) {
|
|
3648
|
+
const pid = pidArg(p.pos[0]);
|
|
3649
|
+
const seconds = num(p, "seconds", Number(process.env.MAP_WAIT_SECONDS) || 90);
|
|
3650
|
+
const dir = findWorkdir();
|
|
3651
|
+
const entry = dir ? (readMeta(dir).pushes ?? []).find((x) => x.pid === pid) ?? null : null;
|
|
3652
|
+
const s = await session(p, p.pos[1]);
|
|
3653
|
+
const v = await s.waitFor(pid, seconds, entry);
|
|
3654
|
+
if (!v) {
|
|
3655
|
+
await s.pendingNote(pid, entry?.sent ?? "");
|
|
3656
|
+
return pendingExit();
|
|
3657
|
+
}
|
|
3658
|
+
const code = s.settle(entry && dir ? dir : null, entry, v.verdict, v.mail);
|
|
3659
|
+
if (bool(p, "json")) printJson({ pid, ...v.verdict });
|
|
3660
|
+
return code;
|
|
3661
|
+
}
|
|
3662
|
+
async function logCmd(p) {
|
|
3663
|
+
const s = await session(p, p.pos[0]);
|
|
3664
|
+
const r = await log(s, num(p, "n", 10));
|
|
3665
|
+
if (bool(p, "json")) {
|
|
3666
|
+
printJson({ repo: s.repo, ...r, notVisible: NOT_VISIBLE });
|
|
3667
|
+
return 0;
|
|
3668
|
+
}
|
|
3669
|
+
const out = (x) => process.stdout.write(x);
|
|
3670
|
+
out(`Searched:
|
|
3671
|
+
${r.commands.map((c) => ` ${c}`).join("\n")}
|
|
3672
|
+
|
|
3673
|
+
`);
|
|
3674
|
+
for (const e of r.entries) out(`${e.id} ${e.at.slice(0, 16).replace("T", " ")} ${e.author}
|
|
3675
|
+
${e.message || "(no message)"}
|
|
3676
|
+
`);
|
|
3677
|
+
if (!r.entries.length) out("No commits found.\n");
|
|
3678
|
+
if (r.source === "repo") out(`
|
|
3679
|
+
Your mailbox holds no commit mail for ${s.repo}: that mail stays with the account that hosts the repo. The log above is the repo's reply, now in your mailbox.
|
|
3680
|
+
`);
|
|
3681
|
+
return 0;
|
|
3682
|
+
}
|
|
3683
|
+
async function historyCmd(p) {
|
|
3684
|
+
const s = await session(p);
|
|
3685
|
+
const r = await history(s, p.pos.join(" "), num(p, "n", 30));
|
|
3686
|
+
if (bool(p, "json")) printJson({ repo: s.repo, ...r, notVisible: NOT_VISIBLE });
|
|
3687
|
+
else printHistory(r.items, r.commands, (x) => process.stdout.write(x));
|
|
3688
|
+
return 0;
|
|
3689
|
+
}
|
|
3690
|
+
function pendingSince() {
|
|
3691
|
+
const dir = findWorkdir();
|
|
3692
|
+
if (!dir) return void 0;
|
|
3693
|
+
const open = (readMeta(dir).pushes ?? []).filter((x) => x.state === "pending" || x.state === "objected");
|
|
3694
|
+
return open.map((x) => x.at).sort()[0];
|
|
3695
|
+
}
|
|
3696
|
+
async function showPush(p) {
|
|
3697
|
+
const pid = p.pos[0];
|
|
3698
|
+
const mail = new Mail(await resolveMe(str(p, "from")));
|
|
3699
|
+
const m = (await mapMail(mail)).find((x) => parseSubject(x.subject)?.id === pid);
|
|
3700
|
+
if (!m) throw new MapError2(`no map mail to you about ${pid} (map inbox --all lists what there is)`);
|
|
3701
|
+
const d = await mail.get(m.id);
|
|
3702
|
+
if (bool(p, "json")) printJson({ id: d.id, subject: d.subject, from: d.sender, at: d.received_at, body: d.body_text });
|
|
3703
|
+
else process.stdout.write(`${d.subject}
|
|
3704
|
+
from ${d.sender}, ${d.received_at}
|
|
3705
|
+
|
|
3706
|
+
${d.body_text.trimEnd()}
|
|
3707
|
+
`);
|
|
3708
|
+
return 0;
|
|
3709
|
+
}
|
|
3710
|
+
async function inbox(p) {
|
|
3711
|
+
const me = await resolveMe(str(p, "from"));
|
|
3712
|
+
const mail = new Mail(me);
|
|
3713
|
+
const repo = str(p, "repo") ? parseRepo(str(p, "repo")) : void 0;
|
|
3714
|
+
let items;
|
|
3715
|
+
if (bool(p, "wait")) {
|
|
3716
|
+
const t = num(p, "timeout", 1800);
|
|
3717
|
+
items = await waitInbox(mail, t, repo, pendingSince());
|
|
3718
|
+
if (!items.length) {
|
|
3719
|
+
if (bool(p, "json")) printJson({ me, items: [] });
|
|
3720
|
+
else say(`map: nothing new in ${t} s`);
|
|
3721
|
+
return 2;
|
|
3722
|
+
}
|
|
3723
|
+
} else {
|
|
3724
|
+
items = inboxItems(await mapMail(mail, { repo }), { all: bool(p, "all") });
|
|
3725
|
+
}
|
|
3726
|
+
if (bool(p, "json")) {
|
|
3727
|
+
printJson({ me, items });
|
|
3728
|
+
return 0;
|
|
3729
|
+
}
|
|
3730
|
+
if (!items.length) {
|
|
3731
|
+
process.stdout.write(`No map mail for ${me} needs action.
|
|
3732
|
+
`);
|
|
3733
|
+
return 0;
|
|
3734
|
+
}
|
|
3735
|
+
const act2 = items.filter((i) => i.action).length;
|
|
3736
|
+
process.stdout.write(`${act2 ? `${act2} to act on (marked *)` : "Nothing to act on; recent verdicts"}, for ${me}:
|
|
3737
|
+
|
|
3738
|
+
${items.map(itemLine).join("\n\n")}
|
|
3739
|
+
`);
|
|
3740
|
+
return 0;
|
|
3741
|
+
}
|
|
3742
|
+
async function act(p) {
|
|
3743
|
+
const action = p.cmd;
|
|
3744
|
+
const pid = pidArg(p.pos[0]);
|
|
3745
|
+
const text2 = textArg(p, 1);
|
|
3746
|
+
const me = await resolveMe(str(p, "from"));
|
|
3747
|
+
const mail = new Mail(me);
|
|
3748
|
+
const repo = str(p, "repo") ? parseRepo(str(p, "repo")) : void 0;
|
|
3749
|
+
const target = pickTarget(action, pid, await mapMail(mail, { repo }));
|
|
3750
|
+
const body = actionBody(action, text2, pid);
|
|
3751
|
+
await mail.reply(target.id, body);
|
|
3752
|
+
if (bool(p, "json")) printJson({ action, pid, repliedTo: { id: target.id, subject: target.subject }, body });
|
|
3753
|
+
else {
|
|
3754
|
+
process.stdout.write(`Replied to "${target.subject}" from ${target.sender} with:
|
|
3755
|
+
${body.trim().split("\n")[0]}
|
|
3756
|
+
`);
|
|
3757
|
+
if (action === "argue") process.stdout.write(`The answer comes by mail; wait for it with: map wait ${pid}
|
|
3758
|
+
`);
|
|
3759
|
+
}
|
|
3760
|
+
return 0;
|
|
3761
|
+
}
|
|
3762
|
+
async function installBin(binDir) {
|
|
3763
|
+
const self = realpathSync2(fileURLToPath2(import.meta.url));
|
|
3764
|
+
const bin2 = resolve3(binDir ?? join9(homedir3(), ".local", "bin"));
|
|
3765
|
+
const share = join9(homedir3(), ".local", "share", "map");
|
|
3766
|
+
mkdirSync5(bin2, { recursive: true });
|
|
3767
|
+
mkdirSync5(share, { recursive: true });
|
|
3768
|
+
const target = join9(share, "map.mjs");
|
|
3769
|
+
if (self !== target) copyFileSync(self, target);
|
|
3770
|
+
chmodSync(target, 493);
|
|
3771
|
+
writeFileSync7(join9(bin2, "map"), `#!/bin/sh
|
|
3772
|
+
exec node "${target}" "$@"
|
|
3773
|
+
`, { mode: 493 });
|
|
3774
|
+
if (existsSync5(join9(bin2, "gud"))) writeFileSync7(join9(bin2, "gud"), `#!/bin/sh
|
|
3775
|
+
MAP_INVOKED_AS=gud exec node "${target}" "$@"
|
|
3776
|
+
`, { mode: 493 });
|
|
3777
|
+
let text2 = `Installed map in ${bin2}.`;
|
|
3778
|
+
if (!(process.env.PATH ?? "").split(delimiter).includes(bin2)) text2 += `
|
|
3779
|
+
${bin2} is not on your PATH. Add it:
|
|
3780
|
+
export PATH="${bin2}:$PATH"`;
|
|
3781
|
+
return text2;
|
|
3782
|
+
}
|
|
3783
|
+
var DEPRECATED_NOTICE = "map: the gud command is now map; run map instead (gud will be removed).";
|
|
3784
|
+
async function main(argv) {
|
|
3785
|
+
adoptLegacyEnv();
|
|
3786
|
+
const invoked = process.env.MAP_INVOKED_AS || basename(process.argv[1] ?? "");
|
|
3787
|
+
delete process.env.MAP_INVOKED_AS;
|
|
3788
|
+
if (/^gud(\.mjs|-conformance)?$/.test(invoked)) say(DEPRECATED_NOTICE);
|
|
3789
|
+
if (invoked.startsWith("map-conformance") || invoked.startsWith("gud-conformance")) return conformanceMain(parseArgs(["conformance", ...argv]), VERSION);
|
|
3790
|
+
const p = parseArgs(argv);
|
|
3791
|
+
if (bool(p, "strict")) process.env.MAP_STRICT = "1";
|
|
3792
|
+
if (bool(p, "version") || p.cmd === "version") {
|
|
3793
|
+
process.stdout.write(`${VERSION}
|
|
3794
|
+
`);
|
|
3795
|
+
return 0;
|
|
3796
|
+
}
|
|
3797
|
+
if (p.cmd === "" || p.cmd === "help" || bool(p, "help")) {
|
|
3798
|
+
process.stdout.write(HELP);
|
|
3799
|
+
return 0;
|
|
3800
|
+
}
|
|
3801
|
+
switch (p.cmd) {
|
|
3802
|
+
case "get":
|
|
3803
|
+
return getCmd(p);
|
|
3804
|
+
case "diff":
|
|
3805
|
+
return diffCmd(p);
|
|
3806
|
+
case "send":
|
|
3807
|
+
return sendCmd(p);
|
|
3808
|
+
case "update":
|
|
3809
|
+
return updateCmd(p);
|
|
3810
|
+
case "wait":
|
|
3811
|
+
return waitCmd(p);
|
|
3812
|
+
case "log":
|
|
3813
|
+
return logCmd(p);
|
|
3814
|
+
case "history":
|
|
3815
|
+
return historyCmd(p);
|
|
3816
|
+
case "show":
|
|
3817
|
+
if (/^p-[0-9a-f]{8}$/.test(p.pos[0] ?? "")) return showPush(p);
|
|
3818
|
+
return readCmd(p);
|
|
3819
|
+
case "status":
|
|
3820
|
+
case "settings":
|
|
3821
|
+
return readCmd(p);
|
|
3822
|
+
case "inbox":
|
|
3823
|
+
return inbox(p);
|
|
3824
|
+
case "approve":
|
|
3825
|
+
case "object":
|
|
3826
|
+
case "argue":
|
|
3827
|
+
case "veto":
|
|
3828
|
+
return act(p);
|
|
3829
|
+
case "whoami": {
|
|
3830
|
+
process.stdout.write(`${await resolveMe(str(p, "from"))}
|
|
3831
|
+
`);
|
|
3832
|
+
return 0;
|
|
3833
|
+
}
|
|
3834
|
+
case "create": {
|
|
3835
|
+
const name = (p.pos[0] ?? "").toLowerCase();
|
|
3836
|
+
if (!/^[a-z][a-z0-9]{1,30}$/.test(name)) throw new MapError2("usage: map create <name> [--handle h] [--allow rules] (names are 2 to 31 lowercase letters and digits)");
|
|
3837
|
+
const me = await resolveMe(str(p, "from"));
|
|
3838
|
+
const allow = (str(p, "allow") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
3839
|
+
const r = await createRepoByEmail(new Mail(me), { name, handle: str(p, "handle"), allow, host: str(p, "host") ?? defaultHost() });
|
|
3840
|
+
if (bool(p, "json")) printJson({ ...r, owner: me, allow });
|
|
3841
|
+
else process.stdout.write(`Created ${r.address}; you (${me}) own it.
|
|
3842
|
+
Get it with: map get ${r.address}
|
|
3843
|
+
`);
|
|
3844
|
+
return 0;
|
|
3845
|
+
}
|
|
3846
|
+
case "init":
|
|
3847
|
+
return init({
|
|
3848
|
+
provider: str(p, "provider"),
|
|
3849
|
+
yes: bool(p, "yes"),
|
|
3850
|
+
from: str(p, "from"),
|
|
3851
|
+
binDir: str(p, "bin-dir"),
|
|
3852
|
+
home: str(p, "skills-home"),
|
|
3853
|
+
skill: !bool(p, "no-skill"),
|
|
3854
|
+
skillText: SKILL_default,
|
|
3855
|
+
installBins: installBin
|
|
3856
|
+
});
|
|
3857
|
+
case "skill":
|
|
3858
|
+
process.stdout.write(SKILL_default);
|
|
3859
|
+
return 0;
|
|
3860
|
+
case "conformance":
|
|
3861
|
+
return conformanceMain(p, VERSION);
|
|
3862
|
+
default:
|
|
3863
|
+
throw new MapError2(`unknown command "${p.cmd}" (see map help)`);
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
var isMain = (() => {
|
|
3867
|
+
try {
|
|
3868
|
+
return !!process.argv[1] && realpathSync2(process.argv[1]) === realpathSync2(fileURLToPath2(import.meta.url));
|
|
3869
|
+
} catch {
|
|
3870
|
+
return false;
|
|
3871
|
+
}
|
|
3872
|
+
})();
|
|
3873
|
+
if (isMain || process.env.MAP_FORCE_MAIN) {
|
|
3874
|
+
main(process.argv.slice(2)).then(
|
|
3875
|
+
(code) => {
|
|
3876
|
+
process.exitCode = code;
|
|
3877
|
+
},
|
|
3878
|
+
(e) => {
|
|
3879
|
+
if (e instanceof MapError2 || e?.name === "TransportError") say(`map: ${e.message}`);
|
|
3880
|
+
else say(`map: unexpected error: ${e?.stack ?? String(e)}`);
|
|
3881
|
+
process.exitCode = e instanceof MapError2 ? e.code : 1;
|
|
3882
|
+
}
|
|
3883
|
+
);
|
|
3884
|
+
}
|
|
3885
|
+
export {
|
|
3886
|
+
DEPRECATED_NOTICE,
|
|
3887
|
+
HELP,
|
|
3888
|
+
main
|
|
3889
|
+
};
|