@agentshouse/kit 0.0.1 → 0.1.0-alpha.2
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/LICENSE +21 -0
- package/README.md +1696 -2
- package/bin/connect-linux.sh +127 -0
- package/bin/kit.js +1023 -0
- package/package.json +33 -3
- package/src/acp.js +99 -0
- package/src/adapter-mcp.js +44 -0
- package/src/adapter.js +252 -0
- package/src/agent-logins.js +65 -0
- package/src/agents.js +449 -0
- package/src/apply.js +29 -0
- package/src/attach.js +52 -0
- package/src/binding.js +90 -0
- package/src/blocks.js +156 -0
- package/src/bridge.js +449 -0
- package/src/bytes.js +89 -0
- package/src/channel.js +37 -0
- package/src/command.js +109 -0
- package/src/configuration.js +465 -0
- package/src/connector-login.js +91 -0
- package/src/connector.js +107 -0
- package/src/continuation.js +61 -0
- package/src/contract.js +363 -0
- package/src/conversation-originals.js +143 -0
- package/src/custom/authorization.js +63 -0
- package/src/declaration.js +35 -0
- package/src/delivery.js +191 -0
- package/src/directory.js +102 -0
- package/src/draft.js +89 -0
- package/src/google/calendar.js +177 -0
- package/src/google/declared.js +146 -0
- package/src/google/drive.js +435 -0
- package/src/google/gateway.js +166 -0
- package/src/google/gmail.js +249 -0
- package/src/google/id.js +1 -0
- package/src/google/oauth.js +273 -0
- package/src/google/source.js +25 -0
- package/src/hook.js +376 -0
- package/src/host.js +121 -0
- package/src/house-main.js +79 -0
- package/src/house.js +1085 -0
- package/src/inbound.js +175 -0
- package/src/instance/google.js +1 -0
- package/src/launcher-main.js +19 -0
- package/src/launcher.js +276 -0
- package/src/local-files.js +113 -0
- package/src/login.js +274 -0
- package/src/mcp.js +71 -0
- package/src/migrator.js +248 -0
- package/src/mount.js +582 -0
- package/src/operation.js +20 -0
- package/src/original.js +631 -0
- package/src/peer.js +124 -0
- package/src/plan.js +57 -0
- package/src/question.js +316 -0
- package/src/refusal.js +79 -0
- package/src/repository.js +521 -0
- package/src/session-bin/house +2 -0
- package/src/settlement.js +57 -0
- package/src/setup.js +185 -0
- package/src/source-originals.js +116 -0
- package/src/source.js +286 -0
- package/src/starter-set/grilling/LICENSE +21 -0
- package/src/starter-set/grilling/SKILL.md +30 -0
- package/src/starter-set/grilling/agents/openai.yaml +3 -0
- package/src/starter-set.js +17 -0
- package/src/state.js +157 -0
- package/src/sync-library.js +293 -0
- package/src/sync-state.js +116 -0
- package/src/sync.js +477 -0
- package/src/telegram.js +1045 -0
- package/src/terminal.js +57 -0
- package/src/tmux.js +119 -0
- package/src/worker.js +756 -0
- package/src/workspace.js +500 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { open, readlink, realpath } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { deliverPublicationChunk, maintainControl, openPublicationManifest, openSession } from "./house.js";
|
|
7
|
+
import { KitRefusal } from "./refusal.js";
|
|
8
|
+
import { readRepositoryState, writeRepositoryState } from "./state.js";
|
|
9
|
+
|
|
10
|
+
const BLOCK_BYTES = 512;
|
|
11
|
+
const READ_BYTES = 64 * 1024;
|
|
12
|
+
const MANIFEST_ROUNDS = 3;
|
|
13
|
+
const USTAR_NAME_BYTES = 100;
|
|
14
|
+
const PUBLICATION_REFERENCE = /^p_[0-9abcdefghjkmnpqrstvwxyz]+\.[0-9abcdefghjkmnpqrstvwxyz]$/;
|
|
15
|
+
const SOURCE_HANDLE = /^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/;
|
|
16
|
+
const REFERENCE_ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
17
|
+
const MAX_REFERENCE_ORDINAL = (1n << 63n) - 1n;
|
|
18
|
+
const ITEM_OUTCOMES = [
|
|
19
|
+
"accepted",
|
|
20
|
+
"duplicate",
|
|
21
|
+
"path_not_in_manifest",
|
|
22
|
+
"fingerprint_mismatch",
|
|
23
|
+
"refused",
|
|
24
|
+
];
|
|
25
|
+
const PUBLICATION_STATES = ["active", "completed"];
|
|
26
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
27
|
+
|
|
28
|
+
function refuse(code, detail = null) {
|
|
29
|
+
throw new KitRefusal(code, detail);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function active(signal) {
|
|
33
|
+
if (signal?.aborted === true) refuse("kit_command_cancelled");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function validSourceReference(value) {
|
|
37
|
+
if (typeof value !== "string") return false;
|
|
38
|
+
const matched = /^s_([0-9a-z]+)\.([0-9a-z])$/.exec(value);
|
|
39
|
+
if (matched === null || (matched[1].length > 1 && matched[1][0] === "0")) return false;
|
|
40
|
+
let ordinal = 0n;
|
|
41
|
+
for (const character of matched[1]) {
|
|
42
|
+
const digit = REFERENCE_ALPHABET.indexOf(character);
|
|
43
|
+
if (digit === -1) return false;
|
|
44
|
+
ordinal = (ordinal << 5n) + BigInt(digit);
|
|
45
|
+
if (ordinal > MAX_REFERENCE_ORDINAL) return false;
|
|
46
|
+
}
|
|
47
|
+
return ordinal > 0n && REFERENCE_ALPHABET.includes(matched[2]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validRepositoryPath(value) {
|
|
51
|
+
return !(
|
|
52
|
+
value.length === 0 ||
|
|
53
|
+
value.startsWith("/") ||
|
|
54
|
+
value.endsWith("/") ||
|
|
55
|
+
value.includes("\\") ||
|
|
56
|
+
value.split("/").some((segment) => segment === "" || segment === "." || segment === "..")
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function repositoryPath(value) {
|
|
61
|
+
if (!validRepositoryPath(value)) refuse("kit_repository_path_invalid", value);
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function stateKey(identity) {
|
|
66
|
+
return createHash("sha256")
|
|
67
|
+
.update(JSON.stringify([identity.checkout, identity.room, identity.sourceHandle]))
|
|
68
|
+
.digest("hex");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function exactObject(value, keys) {
|
|
72
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
73
|
+
const present = Object.keys(value).sort();
|
|
74
|
+
return present.length === keys.length && present.every((key, index) => key === keys[index]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function stateOf(value, identity) {
|
|
78
|
+
if (value === null) {
|
|
79
|
+
return {
|
|
80
|
+
checkout: identity.checkout,
|
|
81
|
+
room: identity.room,
|
|
82
|
+
source_handle: identity.sourceHandle,
|
|
83
|
+
source_ref: null,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (
|
|
87
|
+
!exactObject(value, ["checkout", "room", "source_handle", "source_ref"]) ||
|
|
88
|
+
value.checkout !== identity.checkout ||
|
|
89
|
+
value.room !== identity.room ||
|
|
90
|
+
value.source_handle !== identity.sourceHandle ||
|
|
91
|
+
(value.source_ref !== null && !validSourceReference(value.source_ref))
|
|
92
|
+
) {
|
|
93
|
+
refuse("kit_state_unreadable");
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
checkout: value.checkout,
|
|
97
|
+
room: value.room,
|
|
98
|
+
source_handle: value.source_handle,
|
|
99
|
+
source_ref: value.source_ref,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function octal(header, offset, length, value) {
|
|
104
|
+
const encoded = value.toString(8).padStart(length - 1, "0");
|
|
105
|
+
if (encoded.length >= length) refuse("kit_repository_archive_failed");
|
|
106
|
+
header.write(encoded, offset, length - 1, "ascii");
|
|
107
|
+
header[offset + length - 1] = 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function tarHeader(name, size, type) {
|
|
111
|
+
const header = Buffer.alloc(BLOCK_BYTES);
|
|
112
|
+
Buffer.from(name, "utf8").copy(header, 0, 0, USTAR_NAME_BYTES);
|
|
113
|
+
octal(header, 100, 8, 0o644);
|
|
114
|
+
octal(header, 108, 8, 0);
|
|
115
|
+
octal(header, 116, 8, 0);
|
|
116
|
+
octal(header, 124, 12, size);
|
|
117
|
+
octal(header, 136, 12, 0);
|
|
118
|
+
header.fill(32, 148, 156);
|
|
119
|
+
header[156] = type.charCodeAt(0);
|
|
120
|
+
header.write("ustar\0", 257, 6, "ascii");
|
|
121
|
+
header.write("00", 263, 2, "ascii");
|
|
122
|
+
const checksum = [...header]
|
|
123
|
+
.reduce((sum, byte) => sum + byte, 0)
|
|
124
|
+
.toString(8)
|
|
125
|
+
.padStart(6, "0");
|
|
126
|
+
header.write(checksum, 148, 6, "ascii");
|
|
127
|
+
header[154] = 0;
|
|
128
|
+
header[155] = 32;
|
|
129
|
+
return header;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function paxRecord(name, value) {
|
|
133
|
+
const content = `${name}=${value}\n`;
|
|
134
|
+
const contentBytes = Buffer.byteLength(content);
|
|
135
|
+
let length = contentBytes + 3;
|
|
136
|
+
for (;;) {
|
|
137
|
+
const next = String(length).length + 1 + contentBytes;
|
|
138
|
+
if (next === length) return Buffer.from(`${length} ${content}`, "utf8");
|
|
139
|
+
length = next;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function padding(size) {
|
|
144
|
+
const remainder = size % BLOCK_BYTES;
|
|
145
|
+
return remainder === 0 ? Buffer.alloc(0) : Buffer.alloc(BLOCK_BYTES - remainder);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function material(path, signal) {
|
|
149
|
+
active(signal);
|
|
150
|
+
let handle;
|
|
151
|
+
try {
|
|
152
|
+
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (error.code === "ENOENT") return null;
|
|
155
|
+
if (error.code === "ELOOP") {
|
|
156
|
+
try {
|
|
157
|
+
const body = await readlink(path, { encoding: "buffer" });
|
|
158
|
+
active(signal);
|
|
159
|
+
return { kind: "buffer", body };
|
|
160
|
+
} catch {
|
|
161
|
+
active(signal);
|
|
162
|
+
refuse("kit_repository_checkout_unreadable", path);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
refuse("kit_repository_checkout_unreadable", path);
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
active(signal);
|
|
169
|
+
const status = await handle.stat();
|
|
170
|
+
active(signal);
|
|
171
|
+
if (!status.isFile()) {
|
|
172
|
+
await handle.close();
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
return { kind: "file", size: status.size, handle };
|
|
176
|
+
} catch (error) {
|
|
177
|
+
await handle.close().catch(() => undefined);
|
|
178
|
+
if (error instanceof KitRefusal) throw error;
|
|
179
|
+
if (signal?.aborted === true || error?.name === "AbortError") {
|
|
180
|
+
refuse("kit_command_cancelled");
|
|
181
|
+
} else {
|
|
182
|
+
refuse("kit_repository_checkout_unreadable", path);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function* fileBody(selected, signal) {
|
|
188
|
+
if (selected.kind === "buffer") {
|
|
189
|
+
yield selected.body;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const chunk = Buffer.allocUnsafe(READ_BYTES);
|
|
193
|
+
let read = 0;
|
|
194
|
+
while (read < selected.size) {
|
|
195
|
+
active(signal);
|
|
196
|
+
const { bytesRead } = await selected.handle.read(
|
|
197
|
+
chunk,
|
|
198
|
+
0,
|
|
199
|
+
Math.min(chunk.length, selected.size - read),
|
|
200
|
+
read,
|
|
201
|
+
);
|
|
202
|
+
if (bytesRead === 0) break;
|
|
203
|
+
read += bytesRead;
|
|
204
|
+
yield Buffer.from(chunk.subarray(0, bytesRead));
|
|
205
|
+
}
|
|
206
|
+
if (read < selected.size) yield Buffer.alloc(selected.size - read);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function selectedFile(checkout, path, signal) {
|
|
210
|
+
const selected = await material(join(checkout, path), signal);
|
|
211
|
+
if (selected === null) return null;
|
|
212
|
+
try {
|
|
213
|
+
const digest = createHash("sha256");
|
|
214
|
+
const size = selected.kind === "buffer" ? selected.body.length : selected.size;
|
|
215
|
+
for await (const body of fileBody(selected, signal)) digest.update(body);
|
|
216
|
+
return { path, fingerprint: digest.digest("hex"), bytes: size };
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (error instanceof KitRefusal) throw error;
|
|
219
|
+
if (signal?.aborted === true || error?.name === "AbortError") {
|
|
220
|
+
refuse("kit_command_cancelled");
|
|
221
|
+
}
|
|
222
|
+
refuse("kit_repository_checkout_unreadable", path);
|
|
223
|
+
} finally {
|
|
224
|
+
if (selected.kind === "file") await selected.handle.close().catch(() => undefined);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function manifestOf(checkout, paths, signal) {
|
|
229
|
+
const files = [];
|
|
230
|
+
for (const path of paths) {
|
|
231
|
+
const held = await selectedFile(checkout, path, signal);
|
|
232
|
+
if (held !== null) files.push(held);
|
|
233
|
+
}
|
|
234
|
+
return files;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function* chunkArchive(checkout, entries, signal) {
|
|
238
|
+
for (const entry of entries) {
|
|
239
|
+
const selected = await material(join(checkout, entry.path), signal);
|
|
240
|
+
if (selected === null) continue;
|
|
241
|
+
try {
|
|
242
|
+
const size = selected.kind === "buffer" ? selected.body.length : selected.size;
|
|
243
|
+
if (Buffer.byteLength(entry.path, "utf8") > USTAR_NAME_BYTES) {
|
|
244
|
+
const record = paxRecord("path", entry.path);
|
|
245
|
+
yield tarHeader("PaxHeaders.X/entry", record.length, "x");
|
|
246
|
+
yield record;
|
|
247
|
+
yield padding(record.length);
|
|
248
|
+
yield tarHeader("entry", size, "0");
|
|
249
|
+
} else {
|
|
250
|
+
yield tarHeader(entry.path, size, "0");
|
|
251
|
+
}
|
|
252
|
+
for await (const body of fileBody(selected, signal)) yield body;
|
|
253
|
+
yield padding(size);
|
|
254
|
+
} finally {
|
|
255
|
+
if (selected.kind === "file") await selected.handle.close().catch(() => undefined);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
yield Buffer.alloc(BLOCK_BYTES * 2);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function runGit(checkout, args, dependencies, capture, signal) {
|
|
262
|
+
active(signal);
|
|
263
|
+
const launcher = dependencies.spawn ?? spawn;
|
|
264
|
+
return new Promise((resolve, reject) => {
|
|
265
|
+
let child;
|
|
266
|
+
try {
|
|
267
|
+
child = launcher("git", args, {
|
|
268
|
+
cwd: checkout,
|
|
269
|
+
env: process.env,
|
|
270
|
+
stdio: capture ? ["ignore", "pipe", "inherit"] : "inherit",
|
|
271
|
+
signal,
|
|
272
|
+
killSignal: "SIGKILL",
|
|
273
|
+
});
|
|
274
|
+
} catch (error) {
|
|
275
|
+
reject(
|
|
276
|
+
signal?.aborted === true || error?.name === "AbortError"
|
|
277
|
+
? new KitRefusal("kit_command_cancelled")
|
|
278
|
+
: new KitRefusal("kit_git_unavailable"),
|
|
279
|
+
);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const output = [];
|
|
283
|
+
let settled = false;
|
|
284
|
+
const finish = (operation) => {
|
|
285
|
+
if (settled) return;
|
|
286
|
+
settled = true;
|
|
287
|
+
operation();
|
|
288
|
+
};
|
|
289
|
+
child.stdout?.on("data", (chunk) => output.push(Buffer.from(chunk)));
|
|
290
|
+
child.once("error", (error) =>
|
|
291
|
+
finish(() =>
|
|
292
|
+
reject(
|
|
293
|
+
signal?.aborted === true || error?.name === "AbortError"
|
|
294
|
+
? new KitRefusal("kit_command_cancelled")
|
|
295
|
+
: new KitRefusal("kit_git_unavailable"),
|
|
296
|
+
),
|
|
297
|
+
),
|
|
298
|
+
);
|
|
299
|
+
child.once("close", (status) => {
|
|
300
|
+
finish(() => {
|
|
301
|
+
if (signal?.aborted === true) {
|
|
302
|
+
reject(new KitRefusal("kit_command_cancelled"));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (status !== 0) {
|
|
306
|
+
reject(
|
|
307
|
+
new KitRefusal(args[0] === "pull" ? "kit_git_pull_failed" : "kit_git_selection_failed"),
|
|
308
|
+
);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
resolve(Buffer.concat(output));
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function selectedPaths(checkout, dependencies, signal) {
|
|
318
|
+
const output = await runGit(
|
|
319
|
+
checkout,
|
|
320
|
+
["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
|
|
321
|
+
dependencies,
|
|
322
|
+
true,
|
|
323
|
+
signal,
|
|
324
|
+
);
|
|
325
|
+
const paths = [];
|
|
326
|
+
let offset = 0;
|
|
327
|
+
for (let end = output.indexOf(0); end !== -1; end = output.indexOf(0, offset)) {
|
|
328
|
+
let path;
|
|
329
|
+
try {
|
|
330
|
+
path = utf8.decode(output.subarray(offset, end));
|
|
331
|
+
} catch {
|
|
332
|
+
refuse("kit_repository_path_invalid");
|
|
333
|
+
}
|
|
334
|
+
paths.push(repositoryPath(path));
|
|
335
|
+
offset = end + 1;
|
|
336
|
+
}
|
|
337
|
+
if (offset !== output.length) refuse("kit_git_selection_failed");
|
|
338
|
+
return [...new Set(paths)].sort();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function openedPublication(answer, sourceRef) {
|
|
342
|
+
if (
|
|
343
|
+
answer === null ||
|
|
344
|
+
typeof answer !== "object" ||
|
|
345
|
+
typeof answer.publication !== "string" ||
|
|
346
|
+
!PUBLICATION_REFERENCE.test(answer.publication) ||
|
|
347
|
+
!validSourceReference(answer.source_ref) ||
|
|
348
|
+
!PUBLICATION_STATES.includes(answer.state) ||
|
|
349
|
+
!Array.isArray(answer.needed) ||
|
|
350
|
+
answer.needed.some((path) => typeof path !== "string" || !validRepositoryPath(path)) ||
|
|
351
|
+
answer.needed_count !== answer.needed.length ||
|
|
352
|
+
answer.chunk_bound === null ||
|
|
353
|
+
typeof answer.chunk_bound !== "object" ||
|
|
354
|
+
!Number.isSafeInteger(answer.chunk_bound.paths) ||
|
|
355
|
+
answer.chunk_bound.paths < 1 ||
|
|
356
|
+
!Number.isSafeInteger(answer.chunk_bound.bytes) ||
|
|
357
|
+
answer.chunk_bound.bytes < 1 ||
|
|
358
|
+
(sourceRef !== null && answer.source_ref !== sourceRef)
|
|
359
|
+
) {
|
|
360
|
+
refuse("kit_publication_rejected");
|
|
361
|
+
}
|
|
362
|
+
return answer;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function deliveredChunk(answer, expected) {
|
|
366
|
+
if (
|
|
367
|
+
answer === null ||
|
|
368
|
+
typeof answer !== "object" ||
|
|
369
|
+
answer.publication !== expected ||
|
|
370
|
+
!PUBLICATION_STATES.includes(answer.state) ||
|
|
371
|
+
!Array.isArray(answer.outstanding) ||
|
|
372
|
+
answer.outstanding.some((path) => typeof path !== "string" || !validRepositoryPath(path)) ||
|
|
373
|
+
answer.outstanding_count !== answer.outstanding.length ||
|
|
374
|
+
!Array.isArray(answer.items) ||
|
|
375
|
+
answer.items.some(
|
|
376
|
+
(item) =>
|
|
377
|
+
item === null ||
|
|
378
|
+
typeof item !== "object" ||
|
|
379
|
+
typeof item.path !== "string" ||
|
|
380
|
+
!ITEM_OUTCOMES.includes(item.outcome),
|
|
381
|
+
)
|
|
382
|
+
) {
|
|
383
|
+
refuse("kit_publication_rejected");
|
|
384
|
+
}
|
|
385
|
+
return answer;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function chunksOf(needed, files, bound) {
|
|
389
|
+
const bytes = new Map(files.map((file) => [file.path, file.bytes]));
|
|
390
|
+
const chunks = [];
|
|
391
|
+
let held = [];
|
|
392
|
+
let size = 0;
|
|
393
|
+
for (const path of needed) {
|
|
394
|
+
const entry = { path, bytes: bytes.get(path) ?? 0 };
|
|
395
|
+
if (held.length > 0 && (held.length >= bound.paths || size + entry.bytes > bound.bytes)) {
|
|
396
|
+
chunks.push(held);
|
|
397
|
+
held = [];
|
|
398
|
+
size = 0;
|
|
399
|
+
}
|
|
400
|
+
held.push(entry);
|
|
401
|
+
size += entry.bytes;
|
|
402
|
+
}
|
|
403
|
+
if (held.length > 0) chunks.push(held);
|
|
404
|
+
return chunks;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function deliverRound(session, options, state, opened, files, signal) {
|
|
408
|
+
const delivered = [];
|
|
409
|
+
const refused = [];
|
|
410
|
+
let state_ = opened.state;
|
|
411
|
+
for (const chunk of chunksOf(opened.needed, files, opened.chunk_bound)) {
|
|
412
|
+
active(signal);
|
|
413
|
+
let answer;
|
|
414
|
+
try {
|
|
415
|
+
answer = deliveredChunk(
|
|
416
|
+
await deliverPublicationChunk(
|
|
417
|
+
session,
|
|
418
|
+
{ room: state.room, publication: opened.publication },
|
|
419
|
+
chunkArchive(options.checkout, chunk, signal),
|
|
420
|
+
signal,
|
|
421
|
+
),
|
|
422
|
+
opened.publication,
|
|
423
|
+
);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
if (error instanceof KitRefusal && error.code === "publication_ended") {
|
|
426
|
+
return {
|
|
427
|
+
delivered,
|
|
428
|
+
refused,
|
|
429
|
+
state: "ended",
|
|
430
|
+
ending: error.payload?.error?.ending ?? "completed",
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
for (const item of answer.items) {
|
|
436
|
+
if (item.outcome === "accepted" || item.outcome === "duplicate") delivered.push(item.path);
|
|
437
|
+
else refused.push(item);
|
|
438
|
+
}
|
|
439
|
+
state_ = answer.state;
|
|
440
|
+
}
|
|
441
|
+
return { delivered, refused, state: state_, ending: state_ === "completed" ? "completed" : null };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function checkoutOf(value) {
|
|
445
|
+
try {
|
|
446
|
+
return await realpath(value);
|
|
447
|
+
} catch {
|
|
448
|
+
refuse("kit_repository_checkout_invalid", value);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export async function captureRepository(options, dependencies = {}) {
|
|
453
|
+
if (
|
|
454
|
+
typeof options.room !== "string" ||
|
|
455
|
+
options.room.length === 0 ||
|
|
456
|
+
typeof options.sourceHandle !== "string" ||
|
|
457
|
+
!SOURCE_HANDLE.test(options.sourceHandle)
|
|
458
|
+
) {
|
|
459
|
+
refuse("kit_repository_declaration_invalid");
|
|
460
|
+
}
|
|
461
|
+
const identity = {
|
|
462
|
+
checkout: await checkoutOf(options.checkout),
|
|
463
|
+
room: options.room,
|
|
464
|
+
sourceHandle: options.sourceHandle,
|
|
465
|
+
};
|
|
466
|
+
const key = stateKey(identity);
|
|
467
|
+
const session = await openSession(options, dependencies);
|
|
468
|
+
return maintainControl(
|
|
469
|
+
session,
|
|
470
|
+
async (signal) => {
|
|
471
|
+
const state = stateOf(await readRepositoryState(options.directory, key), identity);
|
|
472
|
+
await runGit(identity.checkout, ["pull"], dependencies, false, signal);
|
|
473
|
+
const now = dependencies.now ?? (() => new Date());
|
|
474
|
+
let sourceRef = state.source_ref;
|
|
475
|
+
let publication = null;
|
|
476
|
+
let delivered = [];
|
|
477
|
+
let refused = [];
|
|
478
|
+
let ending = null;
|
|
479
|
+
for (let round = 0; round < MANIFEST_ROUNDS; round += 1) {
|
|
480
|
+
const paths = await selectedPaths(identity.checkout, dependencies, signal);
|
|
481
|
+
const files = await manifestOf(identity.checkout, paths, signal);
|
|
482
|
+
if (files.length === 0) refuse("kit_repository_empty");
|
|
483
|
+
const opened = openedPublication(
|
|
484
|
+
await openPublicationManifest(
|
|
485
|
+
session,
|
|
486
|
+
{
|
|
487
|
+
room_ref: state.room,
|
|
488
|
+
source_handle: state.source_handle,
|
|
489
|
+
observed_at: now().toISOString(),
|
|
490
|
+
paths: files,
|
|
491
|
+
...(sourceRef === null ? {} : { source_ref: sourceRef }),
|
|
492
|
+
},
|
|
493
|
+
signal,
|
|
494
|
+
),
|
|
495
|
+
sourceRef,
|
|
496
|
+
);
|
|
497
|
+
sourceRef = opened.source_ref;
|
|
498
|
+
publication = opened.publication;
|
|
499
|
+
if (sourceRef !== state.source_ref) {
|
|
500
|
+
state.source_ref = sourceRef;
|
|
501
|
+
await writeRepositoryState(options.directory, key, state, signal);
|
|
502
|
+
}
|
|
503
|
+
const round_ = await deliverRound(session, options, state, opened, files, signal);
|
|
504
|
+
delivered = round_.delivered;
|
|
505
|
+
refused = round_.refused;
|
|
506
|
+
ending = round_.ending;
|
|
507
|
+
if (round_.state === "completed") break;
|
|
508
|
+
if (round_.state === "ended" && round_.ending === "superseded") break;
|
|
509
|
+
if (round_.state === "active" && refused.length === 0) break;
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
sourceRef,
|
|
513
|
+
publication,
|
|
514
|
+
delivered: delivered.length,
|
|
515
|
+
refused,
|
|
516
|
+
ending: ending ?? "active",
|
|
517
|
+
};
|
|
518
|
+
},
|
|
519
|
+
options.signal,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdir, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { KitRefusal } from "./refusal.js";
|
|
5
|
+
import { readNote, writeNote } from "./state.js";
|
|
6
|
+
import { reportedNote, retainedReport } from "./terminal.js";
|
|
7
|
+
|
|
8
|
+
function settlementDirectory(directory) {
|
|
9
|
+
return join(directory, "state", "settlements");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function settlementName(run) {
|
|
13
|
+
return `${createHash("sha256").update(run).digest("hex")}.json`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function writeSettlement(directory, settlement) {
|
|
17
|
+
await writeNote(join(settlementDirectory(directory), settlementName(settlement.run)), {
|
|
18
|
+
installation: directory,
|
|
19
|
+
run: settlement.run,
|
|
20
|
+
session: settlement.session,
|
|
21
|
+
reported: reportedNote(settlement.reported),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function removeSettlement(directory, run) {
|
|
26
|
+
await rm(join(settlementDirectory(directory), settlementName(run)), { force: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function completeSettlement(directory, stored, name) {
|
|
30
|
+
if (typeof stored !== "object" || stored === null || Array.isArray(stored)) return null;
|
|
31
|
+
if (stored.installation !== directory) return null;
|
|
32
|
+
const named = [stored.run, stored.session];
|
|
33
|
+
if (named.some((value) => typeof value !== "string" || value.length === 0)) return null;
|
|
34
|
+
if (settlementName(stored.run) !== name) return null;
|
|
35
|
+
const reported = retainedReport(stored.reported);
|
|
36
|
+
if (reported === null || reported === undefined) return null;
|
|
37
|
+
return { run: stored.run, session: stored.session, reported };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function readSettlements(directory) {
|
|
41
|
+
let present;
|
|
42
|
+
try {
|
|
43
|
+
present = await readdir(settlementDirectory(directory));
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error?.code === "ENOENT") return [];
|
|
46
|
+
throw new KitRefusal("kit_state_unreadable", settlementDirectory(directory));
|
|
47
|
+
}
|
|
48
|
+
const retained = [];
|
|
49
|
+
for (const name of present.filter((entry) => entry.endsWith(".json")).sort()) {
|
|
50
|
+
const stored = await readNote(join(settlementDirectory(directory), name), null);
|
|
51
|
+
if (stored === null) continue;
|
|
52
|
+
const settlement = completeSettlement(directory, stored, name);
|
|
53
|
+
if (settlement === null) continue;
|
|
54
|
+
retained.push(settlement);
|
|
55
|
+
}
|
|
56
|
+
return retained;
|
|
57
|
+
}
|