@agentshouse/kit 0.2.1-cbabe8ad337ff → 0.2.1-cbff58027f078
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -46
- package/dist/acp.js +90 -19
- package/dist/afk.js +17 -0
- package/dist/agents.js +154 -44
- package/dist/api.js +7 -3
- package/dist/app-git.js +10 -26
- package/dist/authority-main.js +2 -2
- package/dist/bridge.js +11 -25
- package/dist/building.js +50 -0
- package/dist/calls.js +71 -0
- package/dist/chatgpt.js +32 -0
- package/dist/chunks.js +49 -0
- package/dist/clis.js +95 -48
- package/dist/connection.js +105 -0
- package/dist/conversations.js +574 -179
- package/dist/copies.js +431 -218
- package/dist/end.js +29 -4
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +70 -8
- package/dist/files.js +139 -31
- package/dist/git.js +57 -21
- package/dist/home.js +89 -7
- package/dist/house-cli.js +299 -0
- package/dist/house-main.js +7 -177
- package/dist/instructions.js +8 -10
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -22
- package/dist/login.js +113 -23
- package/dist/logout.js +0 -2
- package/dist/mcp.js +6 -2
- package/dist/parts.js +44 -16
- package/dist/placement.js +31 -0
- package/dist/relay.js +33 -2
- package/dist/resident.js +111 -44
- package/dist/scope.js +15 -4
- package/dist/scripts.js +255 -0
- package/dist/service-main.js +9 -0
- package/dist/service.js +55 -0
- package/dist/sign-in.js +22 -39
- package/dist/silence.js +28 -0
- package/dist/skills.js +49 -35
- package/dist/transcription.js +85 -45
- package/dist/translate.js +2 -8
- package/dist/update.js +113 -105
- package/dist/wire.js +40 -0
- package/dist/work.js +3 -5
- package/package.json +20 -20
- package/dist/bootstrap.json +0 -24
- package/dist/clis-main.js +0 -23
- package/dist/stream.js +0 -72
- package/how-we-work.md +0 -23
- package/skills/ask-matt/LICENSE +0 -21
- package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
- package/skills/ask-matt/SKILL.md +0 -91
- package/skills/ask-matt/agents/openai.yaml +0 -4
- package/skills/code-review/LICENSE +0 -21
- package/skills/code-review/SKILL.md +0 -89
- package/skills/code-review/agents/openai.yaml +0 -3
- package/skills/codebase-design/DEEPENING.md +0 -37
- package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
- package/skills/codebase-design/LICENSE +0 -21
- package/skills/codebase-design/SKILL.md +0 -116
- package/skills/codebase-design/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/LICENSE +0 -21
- package/skills/diagnosing-bugs/SKILL.md +0 -140
- package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
- package/skills/domain-modeling/ADR-FORMAT.md +0 -47
- package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
- package/skills/domain-modeling/LICENSE +0 -21
- package/skills/domain-modeling/SKILL.md +0 -76
- package/skills/domain-modeling/agents/openai.yaml +0 -3
- package/skills/grill-me/LICENSE +0 -21
- package/skills/grill-me/SKILL.md +0 -8
- package/skills/grill-me/agents/openai.yaml +0 -4
- package/skills/grill-with-docs/LICENSE +0 -21
- package/skills/grill-with-docs/SKILL.md +0 -8
- package/skills/grill-with-docs/agents/openai.yaml +0 -4
- package/skills/grilling/LICENSE +0 -21
- package/skills/grilling/SKILL.md +0 -30
- package/skills/grilling/agents/openai.yaml +0 -3
- package/skills/handoff/LICENSE +0 -21
- package/skills/handoff/SKILL.md +0 -17
- package/skills/handoff/agents/openai.yaml +0 -4
- package/skills/implement/LICENSE +0 -21
- package/skills/implement/SKILL.md +0 -16
- package/skills/implement/agents/openai.yaml +0 -4
- package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
- package/skills/improve-codebase-architecture/LICENSE +0 -21
- package/skills/improve-codebase-architecture/SKILL.md +0 -72
- package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
- package/skills/prototype/LICENSE +0 -21
- package/skills/prototype/LOGIC.md +0 -67
- package/skills/prototype/SKILL.md +0 -28
- package/skills/prototype/UI.md +0 -112
- package/skills/prototype/agents/openai.yaml +0 -3
- package/skills/research/LICENSE +0 -21
- package/skills/research/SKILL.md +0 -14
- package/skills/research/agents/openai.yaml +0 -3
- package/skills/resolving-merge-conflicts/LICENSE +0 -21
- package/skills/resolving-merge-conflicts/SKILL.md +0 -16
- package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
- package/skills/tdd/LICENSE +0 -21
- package/skills/tdd/SKILL.md +0 -40
- package/skills/tdd/agents/openai.yaml +0 -3
- package/skills/tdd/mocking.md +0 -59
- package/skills/tdd/tests.md +0 -77
- package/skills/teach/GLOSSARY-FORMAT.md +0 -35
- package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
- package/skills/teach/LICENSE +0 -21
- package/skills/teach/MISSION-FORMAT.md +0 -31
- package/skills/teach/RESOURCES-FORMAT.md +0 -32
- package/skills/teach/SKILL.md +0 -141
- package/skills/teach/agents/openai.yaml +0 -4
- package/skills/to-questionnaire/LICENSE +0 -21
- package/skills/to-questionnaire/SKILL.md +0 -55
- package/skills/to-questionnaire/agents/openai.yaml +0 -4
- package/skills/to-spec/LICENSE +0 -21
- package/skills/to-spec/SKILL.md +0 -76
- package/skills/to-spec/agents/openai.yaml +0 -4
- package/skills/to-tickets/LICENSE +0 -21
- package/skills/to-tickets/SKILL.md +0 -106
- package/skills/to-tickets/agents/openai.yaml +0 -4
- package/skills/wait-what/LICENSE +0 -21
- package/skills/wait-what/SKILL.md +0 -8
- package/skills/wait-what/agents/openai.yaml +0 -4
- package/skills/wayfinder/LICENSE +0 -21
- package/skills/wayfinder/SKILL.md +0 -129
- package/skills/wayfinder/agents/openai.yaml +0 -4
- package/skills/writing-for-agents/LICENSE +0 -21
- package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
- package/skills/writing-for-agents/SKILL.md +0 -83
- package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/copies.js
CHANGED
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
import { parse } from '@agentshouse/mdmodel';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { mkdir, readFile, readdir, realpath, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { finalRefusal, HouseRefusal, retrying } from './api.js';
|
|
6
|
-
import {
|
|
6
|
+
import { chunkTexts, stagedChunks } from './chunks.js';
|
|
7
|
+
import { blobId, blobText, git, isText, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.js';
|
|
8
|
+
import { paused } from './files.js';
|
|
7
9
|
import { kitHome } from './home.js';
|
|
8
10
|
import { operationId } from './operation.js';
|
|
9
11
|
import { refusalOf } from './refusals.js';
|
|
10
12
|
import { changed } from './translate.js';
|
|
11
|
-
import { relayed } from './relay.js';
|
|
12
13
|
const BRANCH = 'main';
|
|
13
14
|
const PROVENANCE = '_provenance';
|
|
14
|
-
const IDENTITY = { GIT_COMMITTER_NAME: '
|
|
15
|
+
const IDENTITY = { GIT_COMMITTER_NAME: 'Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
|
|
16
|
+
const ROOMS = 'rooms/';
|
|
17
|
+
const ENUMERATE = '/kit/door/enumerate';
|
|
18
|
+
const NOT_READY = 'not ready yet; Kit copies it once House has built it';
|
|
19
|
+
const STAGED_CHUNKS = 'house-chunks';
|
|
20
|
+
// A Room answered not ready is asked again on this schedule: its chunk build usually catches up within seconds, so the first asks come soon and the last repeats until it does.
|
|
21
|
+
const ASKED_AGAIN_MS = [1000, 2000, 3000, 5000, 8000, 11_000, 15_000];
|
|
22
|
+
class NotReady extends Error {
|
|
23
|
+
rooms;
|
|
24
|
+
constructor(rooms) {
|
|
25
|
+
super(NOT_READY);
|
|
26
|
+
this.rooms = rooms;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
15
29
|
function causeOf(error) {
|
|
16
30
|
return error instanceof Error ? error.message : String(error);
|
|
17
31
|
}
|
|
32
|
+
function houseCause(error) {
|
|
33
|
+
return error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
|
|
34
|
+
}
|
|
18
35
|
function logged(error) {
|
|
19
36
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
20
37
|
}
|
|
@@ -29,9 +46,35 @@ const REBASED = new Set([
|
|
|
29
46
|
function rebaseFixes(refusal) {
|
|
30
47
|
return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
|
|
31
48
|
}
|
|
49
|
+
function textOnly(paths) {
|
|
50
|
+
const [named, them] = paths.length === 1 ? [`${paths[0]} is`, 'it'] : [`${paths.join(', ')} are`, 'them'];
|
|
51
|
+
return `${named} not text, and house push sends text only; remove ${them} from the commit and place ${them} with house upload`;
|
|
52
|
+
}
|
|
53
|
+
function outside(path) {
|
|
54
|
+
return { refused: true, text: `${path} is not a House Local copy; run house push inside one` };
|
|
55
|
+
}
|
|
32
56
|
function short(commit) {
|
|
33
57
|
return commit.slice(0, 12);
|
|
34
58
|
}
|
|
59
|
+
function moved(before, after) {
|
|
60
|
+
if (before.house !== after.house)
|
|
61
|
+
return [`${rootOf(after.room_handle)}: ${HOUSE_REF} is at ${short(after.house)}`];
|
|
62
|
+
return before.position === after.position ? [] : [`${rootOf(after.room_handle)}: read-only files refreshed`];
|
|
63
|
+
}
|
|
64
|
+
function expiredAuthority(error) {
|
|
65
|
+
if (!(error instanceof HouseRefusal))
|
|
66
|
+
return undefined;
|
|
67
|
+
try {
|
|
68
|
+
const { error: refused } = JSON.parse(error.text);
|
|
69
|
+
return refused?.code === 'expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function grouped(items, size) {
|
|
76
|
+
return items.flatMap((_, index) => (index % size === 0 ? [items.slice(index, index + size)] : []));
|
|
77
|
+
}
|
|
35
78
|
function covers(path, prefix) {
|
|
36
79
|
return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
|
|
37
80
|
}
|
|
@@ -40,6 +83,12 @@ function sidecar(path) {
|
|
|
40
83
|
segments.splice(-1, 0, PROVENANCE);
|
|
41
84
|
return segments.join('/');
|
|
42
85
|
}
|
|
86
|
+
function recordOf(key) {
|
|
87
|
+
return join(kitHome(), 'local-copies', `${key}.json`);
|
|
88
|
+
}
|
|
89
|
+
function stagingPrefix() {
|
|
90
|
+
return `.${createHash('sha256').update(kitHome()).digest('hex').slice(0, 16)}-`;
|
|
91
|
+
}
|
|
43
92
|
function rootOf(handle) {
|
|
44
93
|
return `/rooms/${handle}`;
|
|
45
94
|
}
|
|
@@ -67,32 +116,59 @@ async function removeFile(repository, path) {
|
|
|
67
116
|
return;
|
|
68
117
|
}
|
|
69
118
|
}
|
|
70
|
-
async function
|
|
119
|
+
async function readOnlyFiles(repository, held) {
|
|
71
120
|
const tracked = new Set((await git(repository, ['ls-files', '-z'])).split('\0'));
|
|
72
121
|
const holding = new Set(held);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
122
|
+
return {
|
|
123
|
+
remove: async (path) => {
|
|
124
|
+
if (!holding.has(path))
|
|
125
|
+
return;
|
|
126
|
+
if (!tracked.has(path))
|
|
127
|
+
await removeFile(repository, path);
|
|
128
|
+
holding.delete(path);
|
|
129
|
+
},
|
|
130
|
+
place: async (path, content) => {
|
|
131
|
+
if (tracked.has(path))
|
|
132
|
+
return;
|
|
133
|
+
if (!holding.has(path) && (await exists(join(repository, path))))
|
|
134
|
+
return;
|
|
135
|
+
await writeText(join(repository, path), content);
|
|
136
|
+
holding.add(path);
|
|
137
|
+
},
|
|
138
|
+
held: () => [...holding].sort(),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
async function readOnlyPlaced(repository, held, written, removed) {
|
|
142
|
+
const files = await readOnlyFiles(repository, held);
|
|
143
|
+
for (const path of removed)
|
|
144
|
+
await files.remove(path);
|
|
145
|
+
for (const [path, content] of written)
|
|
146
|
+
await files.place(path, content);
|
|
147
|
+
return files.held();
|
|
148
|
+
}
|
|
149
|
+
async function importedWhole(repository, scope, texts, commit, readOnly) {
|
|
150
|
+
const revisions = {};
|
|
151
|
+
async function* tracked() {
|
|
152
|
+
for await (const { path, content, revision: held, provenance } of texts) {
|
|
153
|
+
if (declaredWritable(path, scope)) {
|
|
154
|
+
revisions[path] = held;
|
|
155
|
+
yield [path, content];
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
await readOnly.place(path, content);
|
|
159
|
+
}
|
|
160
|
+
if (provenance !== undefined)
|
|
161
|
+
await readOnly.place(sidecar(path), provenance);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { house: await importCommits(repository, [{ ...commit, files: tracked() }]), revisions };
|
|
89
165
|
}
|
|
90
166
|
async function ruled(path, rules) {
|
|
91
167
|
if ((await readFile(path, 'utf8').catch(() => null)) !== rules)
|
|
92
168
|
await writeFile(path, rules);
|
|
93
169
|
}
|
|
94
170
|
async function ignoreRules(repository, writable, copy) {
|
|
95
|
-
const readOnly =
|
|
171
|
+
const readOnly = copy.read_only.filter((path) => !path.split('/').includes(PROVENANCE));
|
|
96
172
|
const protectedPath = (path) => copy.protected.some((prefix) => prefix !== '' && covers(path, prefix));
|
|
97
173
|
const whole = copy.protected.filter((prefix) => prefix !== '' && !writable.some((path) => covers(path, prefix)));
|
|
98
174
|
const house = [
|
|
@@ -101,7 +177,7 @@ async function ignoreRules(repository, writable, copy) {
|
|
|
101
177
|
].map(pattern);
|
|
102
178
|
const granted = readOnly.filter((path) => !protectedPath(path)).map(pattern);
|
|
103
179
|
await ruled(join(repository, '.gitignore'), [
|
|
104
|
-
'#
|
|
180
|
+
'# Kit writes this file and .ignore. The paths below are read-only House',
|
|
105
181
|
'# content: Git does not track them, and House refuses a change to them even',
|
|
106
182
|
'# when this file is edited or a path is force-added.',
|
|
107
183
|
'/.gitignore',
|
|
@@ -116,7 +192,7 @@ async function ignoreRules(repository, writable, copy) {
|
|
|
116
192
|
'',
|
|
117
193
|
].join('\n'));
|
|
118
194
|
await ruled(join(repository, '.ignore'), [
|
|
119
|
-
'#
|
|
195
|
+
'# Kit writes this file so native search still reads the read-only House',
|
|
120
196
|
'# content that .gitignore keeps out of Git.',
|
|
121
197
|
`!${PROVENANCE}`,
|
|
122
198
|
...[...house, ...granted].map((rule) => `!${rule}`),
|
|
@@ -152,8 +228,8 @@ export class LocalCopies {
|
|
|
152
228
|
house;
|
|
153
229
|
copies = new Map();
|
|
154
230
|
queues = new Map();
|
|
155
|
-
|
|
156
|
-
|
|
231
|
+
waiting = new Set();
|
|
232
|
+
answering = Promise.resolve();
|
|
157
233
|
constructor(house) {
|
|
158
234
|
this.house = house;
|
|
159
235
|
this.owner = {
|
|
@@ -184,43 +260,128 @@ export class LocalCopies {
|
|
|
184
260
|
this.copies.set(copy.key, copy);
|
|
185
261
|
}
|
|
186
262
|
}
|
|
263
|
+
async swept() {
|
|
264
|
+
const rooms = join(localCopies(), 'rooms');
|
|
265
|
+
for (const name of await readdir(rooms).catch(() => [])) {
|
|
266
|
+
if (name.startsWith(stagingPrefix()))
|
|
267
|
+
await rm(join(rooms, name), { recursive: true, force: true });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
187
270
|
async mapping() {
|
|
188
|
-
await this.
|
|
271
|
+
await this.answering;
|
|
189
272
|
return [...this.copies.values()]
|
|
190
|
-
.filter((copy) => copy.
|
|
273
|
+
.filter((copy) => copy.unfollowed === undefined)
|
|
191
274
|
.map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
|
|
192
275
|
.sort();
|
|
193
276
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
await
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
277
|
+
connected() {
|
|
278
|
+
return this.selected();
|
|
279
|
+
}
|
|
280
|
+
async changed(rooms) {
|
|
281
|
+
if (rooms.length > 0)
|
|
282
|
+
await this.selected(rooms);
|
|
283
|
+
}
|
|
284
|
+
fetch() {
|
|
285
|
+
return this.answered(() => this.fetched(true));
|
|
286
|
+
}
|
|
287
|
+
async selected(rooms) {
|
|
288
|
+
const copied = await this.answered(() => this.fetched(false, rooms));
|
|
289
|
+
if (copied.refused)
|
|
290
|
+
logged(copied.text);
|
|
291
|
+
}
|
|
292
|
+
answered(work) {
|
|
293
|
+
const answered = this.answering.then(work);
|
|
294
|
+
this.answering = answered.catch(() => undefined);
|
|
295
|
+
return answered;
|
|
296
|
+
}
|
|
297
|
+
async fetched(refreshing, rooms) {
|
|
298
|
+
const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', rooms === undefined ? {} : { rooms });
|
|
299
|
+
const synced = new Map(answer.rooms.map((room) => [`${ROOMS}${room.room_ref}`, room.room_handle]));
|
|
300
|
+
const named = rooms === undefined ? null : new Set(rooms.map((room) => `${ROOMS}${room}`));
|
|
301
|
+
const held = () => [...this.copies.values()].filter((copy) => named === null || named.has(copy.key));
|
|
302
|
+
return this.holding([...new Set([...held().map((copy) => copy.key), ...synced.keys()])].sort(), async () => {
|
|
303
|
+
const said = [];
|
|
304
|
+
let refused = false;
|
|
305
|
+
const mark = refreshing ? 'said' : 'unsaid';
|
|
306
|
+
for (const copy of held()) {
|
|
307
|
+
if (synced.has(copy.key) || copy.unfollowed === 'said' || copy.unfollowed === mark)
|
|
308
|
+
continue;
|
|
309
|
+
await this.save({ ...copy, unfollowed: mark });
|
|
310
|
+
if (refreshing)
|
|
311
|
+
said.push(`${rootOf(copy.room_handle)}: no longer followed; kept as it is`);
|
|
312
|
+
}
|
|
313
|
+
const current = [];
|
|
314
|
+
const wanted = new Map();
|
|
315
|
+
for (const [key, handle] of synced) {
|
|
316
|
+
try {
|
|
317
|
+
const held = this.copies.get(key);
|
|
318
|
+
if (held === undefined || !(await exists(held.repository))) {
|
|
319
|
+
wanted.set(key, handle);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const { unfollowed, ...followed } = held;
|
|
323
|
+
current.push(unfollowed === undefined && held.room_handle === handle ? held : await this.save({ ...followed, room_handle: handle }));
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
refused = true;
|
|
327
|
+
said.push(`${rootOf(handle)}: ${causeOf(error)}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const unready = [];
|
|
331
|
+
const unsettled = [];
|
|
332
|
+
if (wanted.size > 0) {
|
|
333
|
+
const { authorities } = await this.discovered([...wanted.values()]);
|
|
334
|
+
for (const [key, handle] of wanted) {
|
|
335
|
+
const authority = authorities.find((entry) => `${ROOMS}${entry.room_ref}` === key);
|
|
336
|
+
try {
|
|
337
|
+
if (authority === undefined)
|
|
338
|
+
throw new Error('House serves nothing of it to copy');
|
|
339
|
+
said.push(await this.copied({ key, handle, authority }));
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
if (error instanceof NotReady) {
|
|
343
|
+
unready.push(...error.rooms);
|
|
344
|
+
said.push(`${rootOf(handle)}: ${NOT_READY}`);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
refused = true;
|
|
348
|
+
unsettled.push(key.slice(ROOMS.length));
|
|
349
|
+
said.push(`${rootOf(handle)}: ${causeOf(error)}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const reading = refreshing ? current : current.filter((copy) => this.waiting.has(copy.room_ref));
|
|
355
|
+
if (reading.length > 0) {
|
|
356
|
+
const failed = await this.refreshed(reading, unready).then(() => null, houseCause);
|
|
357
|
+
for (const copy of reading) {
|
|
358
|
+
said.push(...(unready.includes(copy.room_ref) ? [`${rootOf(copy.room_handle)}: ${NOT_READY}`] : moved(copy, this.copies.get(copy.key))));
|
|
359
|
+
}
|
|
360
|
+
if (failed !== null) {
|
|
361
|
+
refused = true;
|
|
362
|
+
unsettled.push(...reading.map((copy) => copy.room_ref));
|
|
363
|
+
said.push(failed);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
for (const room of rooms ?? [...this.waiting]) {
|
|
367
|
+
if (!unready.includes(room) && !unsettled.includes(room))
|
|
368
|
+
this.waiting.delete(room);
|
|
369
|
+
}
|
|
370
|
+
const due = unready.filter((room) => !this.waiting.has(room));
|
|
371
|
+
for (const room of due)
|
|
372
|
+
this.waiting.add(room);
|
|
373
|
+
if (due.length > 0)
|
|
374
|
+
void this.askedAgain(due);
|
|
375
|
+
return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
async askedAgain(rooms) {
|
|
379
|
+
let asking = rooms;
|
|
380
|
+
for (let step = 0; asking.length > 0; step++) {
|
|
381
|
+
await paused(ASKED_AGAIN_MS[Math.min(step, ASKED_AGAIN_MS.length - 1)]);
|
|
382
|
+
await this.selected().catch(logged);
|
|
383
|
+
asking = asking.filter((room) => this.waiting.has(room));
|
|
384
|
+
}
|
|
224
385
|
}
|
|
225
386
|
async observed(report) {
|
|
226
387
|
const key = await this.keyAt(dirname(report.common));
|
|
@@ -250,11 +411,11 @@ export class LocalCopies {
|
|
|
250
411
|
async push(cwd, selector, caller) {
|
|
251
412
|
const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
|
|
252
413
|
if (located.status !== 0)
|
|
253
|
-
return
|
|
414
|
+
return outside(cwd);
|
|
254
415
|
const [common, worktree] = located.stdout.toString('utf8').split('\n');
|
|
255
416
|
const key = await this.keyAt(dirname(common));
|
|
256
417
|
if (key === undefined)
|
|
257
|
-
return
|
|
418
|
+
return outside(worktree);
|
|
258
419
|
return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
|
|
259
420
|
}
|
|
260
421
|
async keyAt(repository) {
|
|
@@ -270,19 +431,12 @@ export class LocalCopies {
|
|
|
270
431
|
this.queues.set(key, queued.catch(() => undefined));
|
|
271
432
|
return queued;
|
|
272
433
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
this.delivering.add(key);
|
|
277
|
-
void this.exclusive(key, async () => {
|
|
278
|
-
this.delivering.delete(key);
|
|
279
|
-
const copy = this.copies.get(key);
|
|
280
|
-
if (copy.active)
|
|
281
|
-
await this.save(await this.caughtUp(copy));
|
|
282
|
-
}).catch(logged);
|
|
434
|
+
holding(keys, work) {
|
|
435
|
+
const [key, ...rest] = keys;
|
|
436
|
+
return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
|
|
283
437
|
}
|
|
284
438
|
async save(copy) {
|
|
285
|
-
const path =
|
|
439
|
+
const path = recordOf(copy.key);
|
|
286
440
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
287
441
|
const temporary = `${path}.${process.pid}`;
|
|
288
442
|
await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
|
|
@@ -290,161 +444,210 @@ export class LocalCopies {
|
|
|
290
444
|
this.copies.set(copy.key, copy);
|
|
291
445
|
return copy;
|
|
292
446
|
}
|
|
293
|
-
async
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (view === undefined)
|
|
301
|
-
return null;
|
|
302
|
-
const authority = discovery.authorities.find((entry) => entry.room_ref === view.manifest.anchor);
|
|
303
|
-
const contents = new Map(view.contents.map((entry) => [entry.path, entry.content]));
|
|
304
|
-
const read = {
|
|
305
|
-
anchor: view.manifest.anchor,
|
|
306
|
-
position: view.manifest.position,
|
|
307
|
-
log_epoch: view.manifest.log_epoch,
|
|
308
|
-
write: authority.operations.write,
|
|
309
|
-
protected: authority.protected_paths,
|
|
310
|
-
writable: new Map(),
|
|
311
|
-
revisions: {},
|
|
312
|
-
readOnly: new Map(),
|
|
313
|
-
};
|
|
314
|
-
for (const file of view.manifest.files) {
|
|
315
|
-
const content = contents.get(file.path);
|
|
316
|
-
if (file.writable) {
|
|
317
|
-
read.writable.set(file.path, content);
|
|
318
|
-
read.revisions[file.path] = file.revision;
|
|
319
|
-
}
|
|
320
|
-
else {
|
|
321
|
-
read.readOnly.set(file.path, content);
|
|
322
|
-
}
|
|
323
|
-
if (file.provenance !== undefined)
|
|
324
|
-
read.readOnly.set(sidecar(file.path), file.provenance);
|
|
325
|
-
}
|
|
326
|
-
return read;
|
|
447
|
+
async discovered(handles) {
|
|
448
|
+
const discover = (group) => this.house.post('/kit/door/discover', { prefixes: group.map(rootOf) });
|
|
449
|
+
const [first, ...rest] = handles;
|
|
450
|
+
const { bounds, authorities } = await discover([first]);
|
|
451
|
+
for (const group of grouped(rest, bounds.max_prefixes))
|
|
452
|
+
authorities.push(...(await discover(group)).authorities);
|
|
453
|
+
return { bounds, authorities };
|
|
327
454
|
}
|
|
328
|
-
async
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
await this.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (authority === undefined)
|
|
345
|
-
return;
|
|
346
|
-
if (authority.managed_replication !== 'admitted') {
|
|
347
|
-
logged(`operation_denied: ${rootOf(copy.room_handle)} is not synchronized to this Environment`);
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
|
|
351
|
-
JSON.stringify([copy.write, copy.protected])) {
|
|
352
|
-
await this.save(await this.received(copy));
|
|
353
|
-
return;
|
|
455
|
+
async chunked(root, room, directory) {
|
|
456
|
+
await mkdir(directory, { recursive: true });
|
|
457
|
+
for (let renewed = false;; renewed = true) {
|
|
458
|
+
const { rooms } = await this.house.post('/kit/door/room-chunks/list', { prefixes: [root] });
|
|
459
|
+
const list = rooms.find((entry) => entry.room_ref === room);
|
|
460
|
+
if (list === undefined)
|
|
461
|
+
throw new Error('House serves nothing of it to copy');
|
|
462
|
+
if (!list.ready)
|
|
463
|
+
throw new NotReady([room]);
|
|
464
|
+
const held = new Set(await readdir(directory));
|
|
465
|
+
const failure = await stagedChunks(directory, list.chunks.filter(({ hash }) => !held.has(hash)));
|
|
466
|
+
if (failure === undefined) {
|
|
467
|
+
return { position: list.position, log_epoch: list.log_epoch, texts: chunkTexts(directory, list.chunks, list.models) };
|
|
468
|
+
}
|
|
469
|
+
if (renewed && (await readdir(directory)).length === held.size)
|
|
470
|
+
throw failure;
|
|
354
471
|
}
|
|
355
|
-
await this.save(await this.caughtUp(copy));
|
|
356
472
|
}
|
|
357
|
-
async
|
|
358
|
-
const
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
const repository = join(localCopies(), 'rooms', read.anchor);
|
|
364
|
-
if (await exists(repository)) {
|
|
365
|
-
logged(`${repository} already exists and House Kit leaves it as it is`);
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
const staging = join(dirname(repository), `.${randomUUID()}`);
|
|
473
|
+
async materialized({ key, handle, authority }, whole) {
|
|
474
|
+
const repository = join(localCopies(), 'rooms', authority.room_ref);
|
|
475
|
+
if (await exists(repository))
|
|
476
|
+
throw new Error(`${repository} already exists and Kit leaves it as it is`);
|
|
477
|
+
const staging = join(dirname(repository), `${stagingPrefix()}${randomUUID()}`);
|
|
478
|
+
const scope = { write: authority.operations.write, protected: authority.protected_paths };
|
|
369
479
|
await mkdir(staging, { recursive: true });
|
|
370
|
-
let
|
|
371
|
-
let readOnly;
|
|
480
|
+
let copy;
|
|
372
481
|
try {
|
|
373
482
|
await git(staging, ['init', '-q', '-b', BRANCH]);
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
483
|
+
const read = await whole(join(staging, '.git', STAGED_CHUNKS));
|
|
484
|
+
const readOnly = await readOnlyFiles(staging, []);
|
|
485
|
+
const { house, revisions } = await importedWhole(staging, scope, read.texts, { message: `House ${rootOf(handle)}`, from: null }, readOnly);
|
|
377
486
|
await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
|
|
378
487
|
await git(staging, ['reset', '-q', '--hard']);
|
|
379
|
-
|
|
380
|
-
|
|
488
|
+
copy = {
|
|
489
|
+
key,
|
|
490
|
+
room_ref: authority.room_ref,
|
|
491
|
+
room_handle: handle,
|
|
492
|
+
repository,
|
|
493
|
+
house,
|
|
494
|
+
position: read.position,
|
|
495
|
+
log_epoch: read.log_epoch,
|
|
496
|
+
...scope,
|
|
497
|
+
read_only: readOnly.held(),
|
|
498
|
+
manifests: { [house]: { parent: null, files: revisions } },
|
|
499
|
+
moves: [],
|
|
500
|
+
};
|
|
501
|
+
await ignoreRules(staging, Object.keys(revisions), copy);
|
|
502
|
+
await rm(join(staging, '.git', STAGED_CHUNKS), { recursive: true, force: true });
|
|
381
503
|
await rename(staging, repository);
|
|
382
504
|
}
|
|
383
505
|
catch (error) {
|
|
384
506
|
await rm(staging, { recursive: true, force: true });
|
|
385
507
|
throw error;
|
|
386
508
|
}
|
|
387
|
-
|
|
388
|
-
key: scope.key,
|
|
389
|
-
room_ref: read.anchor,
|
|
390
|
-
room_handle: scope.handle,
|
|
391
|
-
repository,
|
|
392
|
-
active: true,
|
|
393
|
-
house,
|
|
394
|
-
position: read.position,
|
|
395
|
-
log_epoch: read.log_epoch,
|
|
396
|
-
write: read.write,
|
|
397
|
-
protected: read.protected,
|
|
398
|
-
read_only: readOnly,
|
|
399
|
-
manifests: { [house]: { parent: null, files: read.revisions } },
|
|
400
|
-
moves: [],
|
|
401
|
-
});
|
|
509
|
+
return this.save(copy);
|
|
402
510
|
}
|
|
403
|
-
async
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
511
|
+
async copied(wanted) {
|
|
512
|
+
const { handle, authority } = wanted;
|
|
513
|
+
const root = rootOf(handle);
|
|
514
|
+
let answer = null;
|
|
515
|
+
try {
|
|
516
|
+
if (!this.waiting.has(authority.room_ref)) {
|
|
517
|
+
answer = await this.house.post(ENUMERATE, {
|
|
518
|
+
prefixes: [root],
|
|
519
|
+
positions: [
|
|
520
|
+
{ authority: authority.room_ref, position: '0', log_epoch: authority.log_epoch, page: { through: authority.position, read: 0 } },
|
|
521
|
+
],
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
if (expiredAuthority(error) !== authority.room_ref)
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
const view = answer?.authorities.find((entry) => entry.authority === authority.room_ref);
|
|
530
|
+
if (answer !== null && view === undefined)
|
|
531
|
+
throw new Error('House serves nothing of it to copy');
|
|
532
|
+
if (view !== undefined && view.next === undefined) {
|
|
533
|
+
const texts = view.changes.filter((change) => !('removed' in change));
|
|
534
|
+
const copy = await this.materialized(wanted, async () => ({ position: view.position, log_epoch: view.log_epoch, texts }));
|
|
535
|
+
return `${root}: copied to ${copy.repository}`;
|
|
536
|
+
}
|
|
537
|
+
const copy = await this.materialized(wanted, (directory) => this.chunked(root, authority.room_ref, directory));
|
|
538
|
+
const [caught] = await this.caughtUp([copy], true);
|
|
539
|
+
await succeeded(copy.repository, ['merge', '--ff-only', '-q', caught.house]);
|
|
540
|
+
return `${root}: copied to ${copy.repository}`;
|
|
424
541
|
}
|
|
425
|
-
async
|
|
426
|
-
|
|
427
|
-
|
|
542
|
+
async received(copy, authority) {
|
|
543
|
+
const root = rootOf(copy.room_handle);
|
|
544
|
+
const directory = join(copy.repository, '.git', STAGED_CHUNKS);
|
|
545
|
+
const scope = { write: authority.operations.write, protected: authority.protected_paths };
|
|
546
|
+
try {
|
|
547
|
+
const read = await this.chunked(root, copy.room_ref, directory);
|
|
548
|
+
const readOnly = await readOnlyFiles(copy.repository, copy.read_only);
|
|
549
|
+
for (const path of copy.read_only)
|
|
550
|
+
await readOnly.remove(path);
|
|
551
|
+
const commit = { message: `House ${root} at position ${read.position}`, from: copy.house, replaces: true };
|
|
552
|
+
const { house, revisions } = await importedWhole(copy.repository, scope, read.texts, commit, readOnly);
|
|
553
|
+
const next = {
|
|
554
|
+
...copy,
|
|
555
|
+
...scope,
|
|
556
|
+
house,
|
|
557
|
+
position: read.position,
|
|
558
|
+
log_epoch: read.log_epoch,
|
|
559
|
+
read_only: readOnly.held(),
|
|
560
|
+
manifests: { ...copy.manifests, [house]: { parent: null, files: revisions } },
|
|
561
|
+
};
|
|
562
|
+
await ignoreRules(copy.repository, Object.keys(revisions), next);
|
|
563
|
+
return next;
|
|
564
|
+
}
|
|
565
|
+
finally {
|
|
566
|
+
await rm(directory, { recursive: true, force: true });
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
async reread(copy) {
|
|
570
|
+
const { authorities } = await this.discovered([copy.room_handle]);
|
|
571
|
+
const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
572
|
+
if (authority === undefined)
|
|
573
|
+
throw new NotReady([copy.room_ref]);
|
|
574
|
+
return this.received(copy, authority);
|
|
575
|
+
}
|
|
576
|
+
async refreshed(copies, unready) {
|
|
577
|
+
const { bounds, authorities } = await this.discovered(copies.map((copy) => copy.room_handle));
|
|
578
|
+
const read = [];
|
|
579
|
+
const unchanged = [];
|
|
580
|
+
for (const copy of copies) {
|
|
581
|
+
const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
582
|
+
if (authority !== undefined &&
|
|
583
|
+
JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
|
|
584
|
+
try {
|
|
585
|
+
read.push(await this.save(await this.received(copy, authority)));
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
if (!(error instanceof NotReady))
|
|
589
|
+
throw error;
|
|
590
|
+
unready.push(...error.rooms);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
unchanged.push(copy);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
for (const group of grouped(unchanged, bounds.max_prefixes))
|
|
598
|
+
await this.caughtUp(group, false, unready);
|
|
599
|
+
for (const group of grouped(read, bounds.max_prefixes))
|
|
600
|
+
await this.caughtUp(group, true, unready);
|
|
601
|
+
}
|
|
602
|
+
async caughtUp(copies, whole, unready) {
|
|
603
|
+
const caught = new Map(copies.map((copy) => [copy.key, copy]));
|
|
604
|
+
const read = new Set();
|
|
605
|
+
const waiting = unready ?? [];
|
|
606
|
+
let asking = [...copies];
|
|
607
|
+
while (asking.length > 0) {
|
|
428
608
|
let answer;
|
|
429
609
|
try {
|
|
430
|
-
answer = await this.house.post(
|
|
431
|
-
prefixes:
|
|
432
|
-
positions:
|
|
610
|
+
answer = await this.house.post(ENUMERATE, {
|
|
611
|
+
prefixes: asking.map((copy) => rootOf(copy.room_handle)),
|
|
612
|
+
positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
|
|
433
613
|
});
|
|
434
614
|
}
|
|
435
615
|
catch (error) {
|
|
436
|
-
const
|
|
437
|
-
if (
|
|
438
|
-
return this.received(copy);
|
|
439
|
-
if (code !== 'replica_behind')
|
|
616
|
+
const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
|
|
617
|
+
if (expired === undefined)
|
|
440
618
|
throw error;
|
|
441
|
-
|
|
619
|
+
asking = asking.filter((copy) => copy !== expired);
|
|
620
|
+
if (read.has(expired.key)) {
|
|
621
|
+
waiting.push(expired.room_ref);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
read.add(expired.key);
|
|
625
|
+
try {
|
|
626
|
+
const next = await this.save(await this.reread(expired));
|
|
627
|
+
caught.set(expired.key, next);
|
|
628
|
+
asking.push(next);
|
|
629
|
+
}
|
|
630
|
+
catch (refused) {
|
|
631
|
+
if (!(refused instanceof NotReady))
|
|
632
|
+
throw refused;
|
|
633
|
+
waiting.push(expired.room_ref);
|
|
634
|
+
}
|
|
635
|
+
continue;
|
|
442
636
|
}
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
637
|
+
const advanced = [];
|
|
638
|
+
for (const copy of asking) {
|
|
639
|
+
const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
|
|
640
|
+
if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
|
|
641
|
+
continue;
|
|
642
|
+
const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
|
|
643
|
+
caught.set(copy.key, next);
|
|
644
|
+
advanced.push(next);
|
|
645
|
+
}
|
|
646
|
+
asking = advanced.filter((copy) => whole || read.has(copy.key));
|
|
447
647
|
}
|
|
648
|
+
if (unready === undefined && waiting.length > 0)
|
|
649
|
+
throw new NotReady(waiting);
|
|
650
|
+
return copies.map((copy) => caught.get(copy.key));
|
|
448
651
|
}
|
|
449
652
|
async incorporated(copy, changes, position, logEpoch) {
|
|
450
653
|
const houseTree = await tree(copy.repository, copy.house);
|
|
@@ -507,9 +710,6 @@ export class LocalCopies {
|
|
|
507
710
|
}
|
|
508
711
|
copy = await this.save(without(copy));
|
|
509
712
|
}
|
|
510
|
-
if (!copy.active) {
|
|
511
|
-
return { refused: true, text: `${rootOf(copy.room_handle)} is no longer synced to this Environment, so House takes no push from it` };
|
|
512
|
-
}
|
|
513
713
|
const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
|
|
514
714
|
const base = listed.find((ancestor) => ancestor in copy.manifests);
|
|
515
715
|
if (base === undefined) {
|
|
@@ -520,7 +720,7 @@ export class LocalCopies {
|
|
|
520
720
|
changes = await this.derived(copy, base, commit);
|
|
521
721
|
}
|
|
522
722
|
catch (error) {
|
|
523
|
-
return { refused: true, text:
|
|
723
|
+
return { refused: true, text: houseCause(error) };
|
|
524
724
|
}
|
|
525
725
|
if (changes.length === 0)
|
|
526
726
|
return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
|
|
@@ -547,7 +747,18 @@ export class LocalCopies {
|
|
|
547
747
|
const after = await tree(copy.repository, commit);
|
|
548
748
|
const moved = movedPaths(copy.moves, before, after);
|
|
549
749
|
const arrived = new Set(moved.values());
|
|
550
|
-
const
|
|
750
|
+
const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
|
|
751
|
+
if (arriving.length > 0) {
|
|
752
|
+
const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
|
|
753
|
+
const binary = [];
|
|
754
|
+
for (const [path, entry] of arriving) {
|
|
755
|
+
if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
|
|
756
|
+
binary.push(path);
|
|
757
|
+
}
|
|
758
|
+
if (binary.length > 0)
|
|
759
|
+
throw new Error(textOnly(binary));
|
|
760
|
+
}
|
|
761
|
+
const text = (entry) => blobText(copy.repository, entry);
|
|
551
762
|
const changes = [];
|
|
552
763
|
for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
|
|
553
764
|
if (arrived.has(path))
|
|
@@ -560,7 +771,7 @@ export class LocalCopies {
|
|
|
560
771
|
changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
|
|
561
772
|
const moving = after.get(to);
|
|
562
773
|
if (moving !== prior) {
|
|
563
|
-
changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving
|
|
774
|
+
changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving) });
|
|
564
775
|
}
|
|
565
776
|
continue;
|
|
566
777
|
}
|
|
@@ -569,9 +780,9 @@ export class LocalCopies {
|
|
|
569
780
|
if (next === undefined)
|
|
570
781
|
changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
|
|
571
782
|
else if (prior === undefined)
|
|
572
|
-
changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next
|
|
783
|
+
changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
|
|
573
784
|
else
|
|
574
|
-
changes.push(...changed(`${root}/${path}`, path, known, await text(prior
|
|
785
|
+
changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
|
|
575
786
|
}
|
|
576
787
|
return changes;
|
|
577
788
|
}
|
|
@@ -598,12 +809,14 @@ export class LocalCopies {
|
|
|
598
809
|
const { upload, ...unstaged } = copy.push;
|
|
599
810
|
return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
|
|
600
811
|
}
|
|
601
|
-
if (outcome.refused.code === '
|
|
812
|
+
if (outcome.refused.code === 'expired')
|
|
602
813
|
return this.recovered(copy);
|
|
603
|
-
const rebased = rebaseFixes(outcome.refused);
|
|
604
814
|
let settled = await this.save(without(copy));
|
|
815
|
+
if (settled.unfollowed !== undefined)
|
|
816
|
+
return { refused: true, text: outcome.refused.text };
|
|
817
|
+
const rebased = rebaseFixes(outcome.refused);
|
|
605
818
|
try {
|
|
606
|
-
settled =
|
|
819
|
+
settled = (await this.caughtUp([settled], true))[0];
|
|
607
820
|
}
|
|
608
821
|
catch {
|
|
609
822
|
return {
|
|
@@ -622,7 +835,7 @@ export class LocalCopies {
|
|
|
622
835
|
}
|
|
623
836
|
async recovered(copy) {
|
|
624
837
|
const push = copy.push;
|
|
625
|
-
const current =
|
|
838
|
+
const current = (await this.caughtUp([copy], true))[0];
|
|
626
839
|
const house = await tree(copy.repository, current.house);
|
|
627
840
|
const root = rootOf(current.room_handle);
|
|
628
841
|
if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
|
|
@@ -638,7 +851,7 @@ export class LocalCopies {
|
|
|
638
851
|
written.push({
|
|
639
852
|
path: `${root}/${path}`,
|
|
640
853
|
revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
|
|
641
|
-
content: entry === undefined ? null : await blobText(copy.repository, entry
|
|
854
|
+
content: entry === undefined ? null : await blobText(copy.repository, entry),
|
|
642
855
|
});
|
|
643
856
|
}
|
|
644
857
|
return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
|
|
@@ -666,7 +879,7 @@ export class LocalCopies {
|
|
|
666
879
|
const push = copy.push;
|
|
667
880
|
let written = push.written;
|
|
668
881
|
if (push.changes.some((change) => change.op === 'rename')) {
|
|
669
|
-
copy =
|
|
882
|
+
copy = (await this.caughtUp([copy], true))[0];
|
|
670
883
|
const house = await tree(copy.repository, copy.house);
|
|
671
884
|
const sent = await tree(copy.repository, push.house);
|
|
672
885
|
if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
|
|
@@ -682,7 +895,7 @@ export class LocalCopies {
|
|
|
682
895
|
{
|
|
683
896
|
path: `${rootOf(copy.room_handle)}/${path}`,
|
|
684
897
|
revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
|
|
685
|
-
content: entry === undefined ? null : await blobText(copy.repository, entry
|
|
898
|
+
content: entry === undefined ? null : await blobText(copy.repository, entry),
|
|
686
899
|
},
|
|
687
900
|
];
|
|
688
901
|
}
|
|
@@ -727,7 +940,7 @@ export class LocalCopies {
|
|
|
727
940
|
async landed(repository, changes, house, selected) {
|
|
728
941
|
const text = async (path) => {
|
|
729
942
|
const entry = house.get(relative(path));
|
|
730
|
-
return entry === undefined ? undefined : blobText(repository, entry
|
|
943
|
+
return entry === undefined ? undefined : blobText(repository, entry);
|
|
731
944
|
};
|
|
732
945
|
for (const change of changes) {
|
|
733
946
|
const current = await text(change.path);
|