@agentshouse/kit 0.2.1-cb6679d048e60 → 0.2.1-cc719fffeec71
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 +32 -31
- package/dist/acp.js +46 -103
- package/dist/agents.js +178 -49
- package/dist/api.js +36 -3
- package/dist/app-git.js +48 -0
- package/dist/bootstrap.json +4 -4
- package/dist/bridge.js +39 -58
- package/dist/chatgpt.js +32 -0
- package/dist/clis.js +137 -65
- package/dist/codex-main.js +58 -0
- package/dist/conversations.js +393 -275
- package/dist/copies.js +215 -146
- package/dist/end.js +23 -0
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +525 -0
- package/dist/files.js +24 -3
- package/dist/git.js +10 -8
- package/dist/home.js +96 -7
- package/dist/house-cli.js +291 -0
- package/dist/house-main.js +7 -159
- package/dist/inputs.js +37 -0
- package/dist/instructions.js +10 -32
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -19
- package/dist/login.js +118 -103
- package/dist/logout.js +14 -13
- package/dist/mcp.js +2 -1
- package/dist/parts.js +82 -0
- package/dist/placement.js +30 -0
- package/dist/refusals.js +12 -0
- package/dist/relay.js +20 -0
- package/dist/resident.js +91 -35
- package/dist/scope.js +203 -0
- package/dist/secret-input.js +4 -1
- package/dist/service-main.js +9 -0
- package/dist/service.js +55 -0
- package/dist/shell.js +3 -0
- package/dist/sign-in.js +34 -27
- package/dist/skills.js +81 -12
- package/dist/stream.js +89 -12
- package/dist/transcription.js +57 -9
- package/dist/update.js +50 -29
- package/dist/wire.js +6 -0
- package/dist/work.js +346 -0
- package/how-we-work.md +13 -3
- package/package.json +3 -2
- package/skills/ask-matt/SKILL.md +17 -11
- package/skills/ask-matt/agents/openai.yaml +1 -0
- package/skills/code-review/SKILL.md +5 -5
- package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
- package/skills/codebase-design/SKILL.md +0 -2
- package/skills/diagnosing-bugs/SKILL.md +2 -4
- package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
- package/skills/domain-modeling/SKILL.md +12 -14
- package/skills/grill-me/SKILL.md +1 -2
- package/skills/grill-me/agents/openai.yaml +1 -0
- package/skills/grill-with-docs/SKILL.md +1 -2
- package/skills/grill-with-docs/agents/openai.yaml +1 -0
- package/skills/grilling/SKILL.md +2 -2
- package/skills/handoff/SKILL.md +2 -3
- package/skills/handoff/agents/openai.yaml +1 -0
- package/skills/implement/SKILL.md +5 -4
- package/skills/implement/agents/openai.yaml +1 -0
- package/skills/implement-spec/SKILL.md +40 -0
- package/skills/implement-spec/agents/openai.yaml +5 -0
- package/skills/improve-codebase-architecture/SKILL.md +6 -7
- package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
- package/skills/pr/CREDITS.md +3 -0
- package/skills/pr/LICENSE +21 -0
- package/skills/pr/SKILL.md +170 -0
- package/skills/pr/agents/openai.yaml +3 -0
- package/skills/prototype/SKILL.md +0 -2
- package/skills/research/SKILL.md +0 -2
- package/skills/retro/LICENSE +21 -0
- package/skills/retro/SKILL.md +44 -0
- package/skills/retro/agents/openai.yaml +5 -0
- package/skills/tdd/SKILL.md +2 -4
- package/skills/teach/SKILL.md +3 -4
- package/skills/teach/agents/openai.yaml +1 -0
- package/skills/to-questionnaire/SKILL.md +1 -2
- package/skills/to-questionnaire/agents/openai.yaml +1 -0
- package/skills/to-spec/SKILL.md +1 -2
- package/skills/to-spec/agents/openai.yaml +1 -0
- package/skills/to-tickets/SKILL.md +3 -4
- package/skills/to-tickets/agents/openai.yaml +1 -0
- package/skills/triage/AGENT-BRIEF.md +207 -0
- package/skills/triage/LICENSE +21 -0
- package/skills/triage/OUT-OF-SCOPE.md +105 -0
- package/skills/triage/SKILL.md +112 -0
- package/skills/triage/agents/openai.yaml +5 -0
- package/skills/wait-what/SKILL.md +2 -3
- package/skills/wait-what/agents/openai.yaml +1 -0
- package/skills/wayfinder/SKILL.md +5 -6
- package/skills/wayfinder/agents/openai.yaml +1 -0
- package/skills/wizard/LICENSE +21 -0
- package/skills/wizard/SKILL.md +44 -0
- package/skills/wizard/agents/openai.yaml +3 -0
- package/skills/wizard/template.sh +223 -0
- package/skills/writing-for-agents/SKILL.md +0 -2
- package/dist/blocks.js +0 -51
- package/skills/resolving-merge-conflicts/SKILL.md +0 -16
- package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
- /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
package/dist/copies.js
CHANGED
|
@@ -2,19 +2,22 @@ import { parse } from '@agentshouse/mdmodel';
|
|
|
2
2
|
import { 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
|
-
import { HouseRefusal } from './api.js';
|
|
6
|
-
import { blobId, blobText, git, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.js';
|
|
5
|
+
import { finalRefusal, HouseRefusal, retrying } from './api.js';
|
|
6
|
+
import { blobId, blobText, git, isText, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.js';
|
|
7
7
|
import { kitHome } from './home.js';
|
|
8
8
|
import { operationId } from './operation.js';
|
|
9
9
|
import { refusalOf } from './refusals.js';
|
|
10
10
|
import { changed } from './translate.js';
|
|
11
|
+
import { relayed } from './relay.js';
|
|
11
12
|
const BRANCH = 'main';
|
|
12
13
|
const PROVENANCE = '_provenance';
|
|
13
|
-
const REPLICA_RETRY_MS = 30_000;
|
|
14
14
|
const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
|
|
15
15
|
function causeOf(error) {
|
|
16
16
|
return error instanceof Error ? error.message : String(error);
|
|
17
17
|
}
|
|
18
|
+
function houseCause(error) {
|
|
19
|
+
return error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
|
|
20
|
+
}
|
|
18
21
|
function logged(error) {
|
|
19
22
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
20
23
|
}
|
|
@@ -29,9 +32,35 @@ const REBASED = new Set([
|
|
|
29
32
|
function rebaseFixes(refusal) {
|
|
30
33
|
return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
|
|
31
34
|
}
|
|
35
|
+
function textOnly(paths) {
|
|
36
|
+
const [named, them] = paths.length === 1 ? [`${paths[0]} is`, 'it'] : [`${paths.join(', ')} are`, 'them'];
|
|
37
|
+
return `${named} not text, and house push sends text only; remove ${them} from the commit and place ${them} with house upload`;
|
|
38
|
+
}
|
|
39
|
+
function outside(path) {
|
|
40
|
+
return { refused: true, text: `${path} is not a House Local copy; run house push inside one` };
|
|
41
|
+
}
|
|
32
42
|
function short(commit) {
|
|
33
43
|
return commit.slice(0, 12);
|
|
34
44
|
}
|
|
45
|
+
function moved(before, after) {
|
|
46
|
+
if (before.house !== after.house)
|
|
47
|
+
return [`${rootOf(after.room_handle)}: ${HOUSE_REF} is at ${short(after.house)}`];
|
|
48
|
+
return before.position === after.position ? [] : [`${rootOf(after.room_handle)}: read-only files refreshed`];
|
|
49
|
+
}
|
|
50
|
+
function expiredAuthority(error) {
|
|
51
|
+
if (!(error instanceof HouseRefusal))
|
|
52
|
+
return undefined;
|
|
53
|
+
try {
|
|
54
|
+
const { error: refused } = JSON.parse(error.text);
|
|
55
|
+
return refused?.code === 'expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function grouped(items, size) {
|
|
62
|
+
return items.flatMap((_, index) => (index % size === 0 ? [items.slice(index, index + size)] : []));
|
|
63
|
+
}
|
|
35
64
|
function covers(path, prefix) {
|
|
36
65
|
return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
|
|
37
66
|
}
|
|
@@ -40,11 +69,14 @@ function sidecar(path) {
|
|
|
40
69
|
segments.splice(-1, 0, PROVENANCE);
|
|
41
70
|
return segments.join('/');
|
|
42
71
|
}
|
|
43
|
-
function
|
|
44
|
-
return
|
|
72
|
+
function recordOf(key) {
|
|
73
|
+
return join(kitHome(), 'local-copies', `${key}.json`);
|
|
74
|
+
}
|
|
75
|
+
function rootOf(handle) {
|
|
76
|
+
return `/rooms/${handle}`;
|
|
45
77
|
}
|
|
46
78
|
function relative(path) {
|
|
47
|
-
return path.replace(/^\/
|
|
79
|
+
return path.replace(/^\/rooms\/[^/]+\//, '');
|
|
48
80
|
}
|
|
49
81
|
function declaredWritable(path, copy) {
|
|
50
82
|
const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
|
|
@@ -152,19 +184,18 @@ export class LocalCopies {
|
|
|
152
184
|
house;
|
|
153
185
|
copies = new Map();
|
|
154
186
|
queues = new Map();
|
|
155
|
-
|
|
156
|
-
selecting = Promise.resolve();
|
|
187
|
+
answering = Promise.resolve();
|
|
157
188
|
constructor(house) {
|
|
158
189
|
this.house = house;
|
|
159
190
|
this.owner = {
|
|
160
191
|
key: 'owner',
|
|
161
192
|
submit: async (operation, changes) => {
|
|
162
193
|
try {
|
|
163
|
-
const outcome = await house.post('/kit/door/batch', { operation_id: operation, changes });
|
|
194
|
+
const outcome = await retrying(() => house.post('/kit/door/batch', { operation_id: operation, changes }));
|
|
164
195
|
return { accepted: outcome.contents };
|
|
165
196
|
}
|
|
166
197
|
catch (error) {
|
|
167
|
-
if (!(error
|
|
198
|
+
if (!finalRefusal(error))
|
|
168
199
|
throw error;
|
|
169
200
|
return {
|
|
170
201
|
refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
|
|
@@ -176,7 +207,7 @@ export class LocalCopies {
|
|
|
176
207
|
async load() {
|
|
177
208
|
const home = join(kitHome(), 'local-copies');
|
|
178
209
|
const names = (await readdir(join(home, 'rooms')).catch(() => [])).filter((name) => name.endsWith('.json'));
|
|
179
|
-
for (const path of
|
|
210
|
+
for (const path of names.map((name) => join(home, 'rooms', name))) {
|
|
180
211
|
const text = await readFile(path, 'utf8').catch(() => null);
|
|
181
212
|
if (text === null)
|
|
182
213
|
continue;
|
|
@@ -185,47 +216,81 @@ export class LocalCopies {
|
|
|
185
216
|
}
|
|
186
217
|
}
|
|
187
218
|
async mapping() {
|
|
188
|
-
await this.
|
|
219
|
+
await this.answering;
|
|
189
220
|
return [...this.copies.values()]
|
|
190
|
-
.filter((copy) => copy.
|
|
191
|
-
.map((copy) => `${rootOf(copy)}: ${copy.repository}`)
|
|
221
|
+
.filter((copy) => copy.erasing === undefined)
|
|
222
|
+
.map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
|
|
192
223
|
.sort();
|
|
193
224
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
this.selecting = selected.catch(() => undefined);
|
|
197
|
-
return selected;
|
|
198
|
-
}
|
|
199
|
-
async reselect() {
|
|
200
|
-
const { local_copy: chosen } = await this.house.post('/kit/local-copy/selection', {});
|
|
201
|
-
const selected = [
|
|
202
|
-
...(chosen?.rooms ?? [])
|
|
203
|
-
.filter((room) => room.room_handle !== null)
|
|
204
|
-
.map((room) => ({ key: `rooms/${room.room_ref}`, private: false, handle: room.room_handle })),
|
|
205
|
-
...(chosen?.private === true ? [{ key: 'private', private: true, handle: null }] : []),
|
|
206
|
-
];
|
|
207
|
-
const keys = new Set(selected.map((scope) => scope.key));
|
|
208
|
-
const work = [];
|
|
209
|
-
for (const key of this.copies.keys()) {
|
|
210
|
-
if (keys.has(key))
|
|
211
|
-
continue;
|
|
212
|
-
work.push(this.exclusive(key, async () => {
|
|
213
|
-
const copy = this.copies.get(key);
|
|
214
|
-
if (copy.active)
|
|
215
|
-
await this.save({ ...copy, active: false });
|
|
216
|
-
}));
|
|
217
|
-
}
|
|
218
|
-
for (const scope of selected)
|
|
219
|
-
work.push(this.exclusive(scope.key, () => this.apply(scope)).catch(logged));
|
|
220
|
-
await Promise.all(work);
|
|
225
|
+
connected() {
|
|
226
|
+
return this.selected();
|
|
221
227
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
228
|
+
async changed(rooms) {
|
|
229
|
+
if (rooms.length > 0)
|
|
230
|
+
await this.selected(rooms);
|
|
231
|
+
}
|
|
232
|
+
fetch() {
|
|
233
|
+
return this.answered(() => this.fetched(true));
|
|
234
|
+
}
|
|
235
|
+
async selected(rooms) {
|
|
236
|
+
const copied = await this.answered(() => this.fetched(false, rooms));
|
|
237
|
+
if (copied.refused)
|
|
238
|
+
logged(copied.text);
|
|
239
|
+
}
|
|
240
|
+
answered(work) {
|
|
241
|
+
const answered = this.answering.then(work);
|
|
242
|
+
this.answering = answered.catch(() => undefined);
|
|
243
|
+
return answered;
|
|
244
|
+
}
|
|
245
|
+
async fetched(refreshing, rooms) {
|
|
246
|
+
const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', rooms === undefined ? {} : { rooms });
|
|
247
|
+
const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
|
|
248
|
+
const named = rooms === undefined ? null : new Set(rooms.map((room) => `rooms/${room}`));
|
|
249
|
+
const held = () => [...this.copies.values()].filter((copy) => named === null || named.has(copy.key));
|
|
250
|
+
return this.holding([...new Set([...held().map((copy) => copy.key), ...synced.keys()])].sort(), async () => {
|
|
251
|
+
const said = [];
|
|
252
|
+
let refused = false;
|
|
253
|
+
for (const copy of held()) {
|
|
254
|
+
const returned = synced.has(copy.key);
|
|
255
|
+
if (returned && copy.erasing === undefined)
|
|
256
|
+
continue;
|
|
257
|
+
try {
|
|
258
|
+
await this.erase(copy);
|
|
259
|
+
said.push(`${rootOf(copy.room_handle)}: ${returned ? '' : 'no longer synced to this Environment; '}erased from the Local copy`);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
refused = true;
|
|
263
|
+
said.push(`${rootOf(copy.room_handle)}: ${causeOf(error)}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const current = [];
|
|
267
|
+
for (const [key, handle] of synced) {
|
|
268
|
+
try {
|
|
269
|
+
const held = this.copies.get(key);
|
|
270
|
+
if (held?.erasing)
|
|
271
|
+
continue;
|
|
272
|
+
if (held === undefined || !(await exists(held.repository))) {
|
|
273
|
+
said.push(await this.materialize(key, handle));
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
current.push(held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle }));
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
refused = true;
|
|
280
|
+
said.push(`${rootOf(handle)}: ${causeOf(error)}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (refreshing && current.length > 0) {
|
|
284
|
+
const failed = await this.refreshed(current).then(() => null, houseCause);
|
|
285
|
+
for (const copy of current)
|
|
286
|
+
said.push(...moved(copy, this.copies.get(copy.key)));
|
|
287
|
+
if (failed !== null) {
|
|
288
|
+
refused = true;
|
|
289
|
+
said.push(failed);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
|
|
293
|
+
});
|
|
229
294
|
}
|
|
230
295
|
async observed(report) {
|
|
231
296
|
const key = await this.keyAt(dirname(report.common));
|
|
@@ -233,6 +298,8 @@ export class LocalCopies {
|
|
|
233
298
|
return;
|
|
234
299
|
await this.exclusive(key, async () => {
|
|
235
300
|
const copy = this.copies.get(key);
|
|
301
|
+
if (copy === undefined || copy.erasing)
|
|
302
|
+
return;
|
|
236
303
|
let moves = copy.moves;
|
|
237
304
|
for (const { from, to } of report.moves) {
|
|
238
305
|
const held = moves.find((move) => move.worktree === report.worktree && move.to === from);
|
|
@@ -255,11 +322,11 @@ export class LocalCopies {
|
|
|
255
322
|
async push(cwd, selector, caller) {
|
|
256
323
|
const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
|
|
257
324
|
if (located.status !== 0)
|
|
258
|
-
return
|
|
325
|
+
return outside(cwd);
|
|
259
326
|
const [common, worktree] = located.stdout.toString('utf8').split('\n');
|
|
260
327
|
const key = await this.keyAt(dirname(common));
|
|
261
328
|
if (key === undefined)
|
|
262
|
-
return
|
|
329
|
+
return outside(worktree);
|
|
263
330
|
return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
|
|
264
331
|
}
|
|
265
332
|
async keyAt(repository) {
|
|
@@ -275,19 +342,12 @@ export class LocalCopies {
|
|
|
275
342
|
this.queues.set(key, queued.catch(() => undefined));
|
|
276
343
|
return queued;
|
|
277
344
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
this.delivering.add(key);
|
|
282
|
-
void this.exclusive(key, async () => {
|
|
283
|
-
this.delivering.delete(key);
|
|
284
|
-
const copy = this.copies.get(key);
|
|
285
|
-
if (copy.active)
|
|
286
|
-
await this.save(await this.caughtUp(copy));
|
|
287
|
-
}).catch(logged);
|
|
345
|
+
holding(keys, work) {
|
|
346
|
+
const [key, ...rest] = keys;
|
|
347
|
+
return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
|
|
288
348
|
}
|
|
289
349
|
async save(copy) {
|
|
290
|
-
const path =
|
|
350
|
+
const path = recordOf(copy.key);
|
|
291
351
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
292
352
|
const temporary = `${path}.${process.pid}`;
|
|
293
353
|
await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
|
|
@@ -295,12 +355,18 @@ export class LocalCopies {
|
|
|
295
355
|
this.copies.set(copy.key, copy);
|
|
296
356
|
return copy;
|
|
297
357
|
}
|
|
298
|
-
async
|
|
299
|
-
|
|
358
|
+
async erase(copy) {
|
|
359
|
+
await this.save({ ...copy, erasing: true });
|
|
360
|
+
await rm(copy.repository, { recursive: true, force: true });
|
|
361
|
+
await rm(recordOf(copy.key), { force: true });
|
|
362
|
+
this.copies.delete(copy.key);
|
|
363
|
+
}
|
|
364
|
+
async read(handle) {
|
|
365
|
+
const prefixes = [rootOf(handle)];
|
|
300
366
|
const discovery = await this.house.post('/kit/door/discover', { prefixes });
|
|
301
367
|
let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
|
|
302
368
|
if ('transfer' in bundle)
|
|
303
|
-
bundle = (await (await fetch(bundle.transfer.url)).json());
|
|
369
|
+
bundle = (await (await fetch(relayed(bundle.transfer.url))).json());
|
|
304
370
|
const [view] = bundle.scopes;
|
|
305
371
|
if (view === undefined)
|
|
306
372
|
return null;
|
|
@@ -330,46 +396,13 @@ export class LocalCopies {
|
|
|
330
396
|
}
|
|
331
397
|
return read;
|
|
332
398
|
}
|
|
333
|
-
async
|
|
334
|
-
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
copy = await this.save({ ...copy, room_handle: scope.handle });
|
|
341
|
-
if (!copy.active) {
|
|
342
|
-
await this.save(await this.received({ ...copy, active: true }));
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
const discovery = await this.house.post('/kit/door/discover', {
|
|
346
|
-
prefixes: [rootOf(copy)],
|
|
347
|
-
});
|
|
348
|
-
const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
349
|
-
if (authority === undefined)
|
|
350
|
-
return;
|
|
351
|
-
if (authority.managed_replication !== 'admitted') {
|
|
352
|
-
logged(`operation_denied: ${rootOf(copy)} is not synchronized to this Environment`);
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
|
|
356
|
-
JSON.stringify([copy.write, copy.protected])) {
|
|
357
|
-
await this.save(await this.received(copy));
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
await this.save(await this.caughtUp(copy));
|
|
361
|
-
}
|
|
362
|
-
async materialize(scope) {
|
|
363
|
-
const read = await this.read({ private: scope.private, room_handle: scope.handle });
|
|
364
|
-
if (read === null) {
|
|
365
|
-
logged(`House serves nothing of ${rootOf({ private: scope.private, room_handle: scope.handle })} to copy`);
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
const repository = scope.private ? join(localCopies(), 'private') : join(localCopies(), 'rooms', read.anchor);
|
|
369
|
-
if (await exists(repository)) {
|
|
370
|
-
logged(`${repository} already exists and House Kit leaves it as it is`);
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
399
|
+
async materialize(key, handle) {
|
|
400
|
+
const read = await this.read(handle);
|
|
401
|
+
if (read === null)
|
|
402
|
+
throw new Error('House serves nothing of it to copy');
|
|
403
|
+
const repository = join(localCopies(), 'rooms', read.anchor);
|
|
404
|
+
if (await exists(repository))
|
|
405
|
+
throw new Error(`${repository} already exists and House Kit leaves it as it is`);
|
|
373
406
|
const staging = join(dirname(repository), `.${randomUUID()}`);
|
|
374
407
|
await mkdir(staging, { recursive: true });
|
|
375
408
|
let house;
|
|
@@ -377,7 +410,7 @@ export class LocalCopies {
|
|
|
377
410
|
try {
|
|
378
411
|
await git(staging, ['init', '-q', '-b', BRANCH]);
|
|
379
412
|
house = await importCommits(staging, [
|
|
380
|
-
{ message: `House ${rootOf(
|
|
413
|
+
{ message: `House ${rootOf(handle)}`, from: null, files: read.writable },
|
|
381
414
|
]);
|
|
382
415
|
await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
|
|
383
416
|
await git(staging, ['reset', '-q', '--hard']);
|
|
@@ -390,12 +423,10 @@ export class LocalCopies {
|
|
|
390
423
|
throw error;
|
|
391
424
|
}
|
|
392
425
|
await this.save({
|
|
393
|
-
key
|
|
394
|
-
private: scope.private,
|
|
426
|
+
key,
|
|
395
427
|
room_ref: read.anchor,
|
|
396
|
-
room_handle:
|
|
428
|
+
room_handle: handle,
|
|
397
429
|
repository,
|
|
398
|
-
active: true,
|
|
399
430
|
house,
|
|
400
431
|
position: read.position,
|
|
401
432
|
log_epoch: read.log_epoch,
|
|
@@ -405,13 +436,14 @@ export class LocalCopies {
|
|
|
405
436
|
manifests: { [house]: { parent: null, files: read.revisions } },
|
|
406
437
|
moves: [],
|
|
407
438
|
});
|
|
439
|
+
return `${rootOf(handle)}: copied to ${repository}`;
|
|
408
440
|
}
|
|
409
441
|
async received(copy) {
|
|
410
|
-
const read = await this.read(copy);
|
|
442
|
+
const read = await this.read(copy.room_handle);
|
|
411
443
|
if (read === null)
|
|
412
444
|
return copy;
|
|
413
445
|
const house = await importCommits(copy.repository, [
|
|
414
|
-
{ message: `House ${rootOf(copy)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
|
|
446
|
+
{ message: `House ${rootOf(copy.room_handle)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
|
|
415
447
|
]);
|
|
416
448
|
const removed = copy.read_only.filter((path) => !read.readOnly.has(path));
|
|
417
449
|
const readOnly = await readOnlyPlaced(copy.repository, copy.read_only, read.readOnly, removed);
|
|
@@ -428,30 +460,57 @@ export class LocalCopies {
|
|
|
428
460
|
await ignoreRules(copy.repository, [...read.writable.keys()], next);
|
|
429
461
|
return next;
|
|
430
462
|
}
|
|
431
|
-
async
|
|
432
|
-
|
|
433
|
-
|
|
463
|
+
async refreshed(copies) {
|
|
464
|
+
const discover = (group) => this.house.post('/kit/door/discover', { prefixes: group.map((copy) => rootOf(copy.room_handle)) });
|
|
465
|
+
const [first, ...rest] = copies;
|
|
466
|
+
const { bounds, authorities } = await discover([first]);
|
|
467
|
+
for (const group of grouped(rest, bounds.max_prefixes))
|
|
468
|
+
authorities.push(...(await discover(group)).authorities);
|
|
469
|
+
const unchanged = [];
|
|
470
|
+
for (const copy of copies) {
|
|
471
|
+
const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
472
|
+
if (authority !== undefined &&
|
|
473
|
+
JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
|
|
474
|
+
await this.save(await this.received(copy));
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
unchanged.push(copy);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
for (const group of grouped(unchanged, bounds.max_prefixes))
|
|
481
|
+
await this.caughtUp(group, false);
|
|
482
|
+
}
|
|
483
|
+
async caughtUp(copies, whole) {
|
|
484
|
+
const caught = new Map(copies.map((copy) => [copy.key, copy]));
|
|
485
|
+
let asking = [...copies];
|
|
486
|
+
while (asking.length > 0) {
|
|
434
487
|
let answer;
|
|
435
488
|
try {
|
|
436
489
|
answer = await this.house.post('/kit/door/enumerate', {
|
|
437
|
-
prefixes:
|
|
438
|
-
positions:
|
|
490
|
+
prefixes: asking.map((copy) => rootOf(copy.room_handle)),
|
|
491
|
+
positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
|
|
439
492
|
});
|
|
440
493
|
}
|
|
441
494
|
catch (error) {
|
|
442
|
-
const
|
|
443
|
-
if (
|
|
444
|
-
return this.received(copy);
|
|
445
|
-
if (code !== 'replica_behind')
|
|
495
|
+
const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
|
|
496
|
+
if (expired === undefined)
|
|
446
497
|
throw error;
|
|
447
|
-
|
|
448
|
-
|
|
498
|
+
caught.set(expired.key, await this.save(await this.received(expired)));
|
|
499
|
+
asking = asking.filter((copy) => copy !== expired);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const advanced = [];
|
|
503
|
+
for (const copy of asking) {
|
|
504
|
+
const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
|
|
505
|
+
if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
|
|
506
|
+
continue;
|
|
507
|
+
const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
|
|
508
|
+
caught.set(copy.key, next);
|
|
509
|
+
advanced.push(next);
|
|
449
510
|
}
|
|
450
|
-
|
|
451
|
-
if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
|
|
452
|
-
return copy;
|
|
453
|
-
copy = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
|
|
511
|
+
asking = whole ? advanced : [];
|
|
454
512
|
}
|
|
513
|
+
return copies.map((copy) => caught.get(copy.key));
|
|
455
514
|
}
|
|
456
515
|
async incorporated(copy, changes, position, logEpoch) {
|
|
457
516
|
const houseTree = await tree(copy.repository, copy.house);
|
|
@@ -484,7 +543,7 @@ export class LocalCopies {
|
|
|
484
543
|
let { house, manifests } = copy;
|
|
485
544
|
if (files.size > 0) {
|
|
486
545
|
house = await importCommits(copy.repository, [
|
|
487
|
-
{ message: `House ${rootOf(copy)} at position ${position}`, from: copy.house, files },
|
|
546
|
+
{ message: `House ${rootOf(copy.room_handle)} at position ${position}`, from: copy.house, files },
|
|
488
547
|
]);
|
|
489
548
|
manifests = { ...manifests, [house]: { parent: copy.house, files: revisions } };
|
|
490
549
|
}
|
|
@@ -501,6 +560,8 @@ export class LocalCopies {
|
|
|
501
560
|
}
|
|
502
561
|
async pushed(key, worktree, selector, caller) {
|
|
503
562
|
let copy = this.copies.get(key);
|
|
563
|
+
if (copy === undefined || copy.erasing)
|
|
564
|
+
return outside(worktree);
|
|
504
565
|
if (copy.push?.state === 'submitted')
|
|
505
566
|
return this.settle(copy, caller);
|
|
506
567
|
const selected = await run(worktree, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${selector}^{commit}`]);
|
|
@@ -514,9 +575,6 @@ export class LocalCopies {
|
|
|
514
575
|
}
|
|
515
576
|
copy = await this.save(without(copy));
|
|
516
577
|
}
|
|
517
|
-
if (!copy.active) {
|
|
518
|
-
return { refused: true, text: `${rootOf(copy)} is no longer synced to this Environment, so House takes no push from it` };
|
|
519
|
-
}
|
|
520
578
|
const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
|
|
521
579
|
const base = listed.find((ancestor) => ancestor in copy.manifests);
|
|
522
580
|
if (base === undefined) {
|
|
@@ -527,7 +585,7 @@ export class LocalCopies {
|
|
|
527
585
|
changes = await this.derived(copy, base, commit);
|
|
528
586
|
}
|
|
529
587
|
catch (error) {
|
|
530
|
-
return { refused: true, text:
|
|
588
|
+
return { refused: true, text: houseCause(error) };
|
|
531
589
|
}
|
|
532
590
|
if (changes.length === 0)
|
|
533
591
|
return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
|
|
@@ -549,12 +607,23 @@ export class LocalCopies {
|
|
|
549
607
|
return this.settle(submitted, caller);
|
|
550
608
|
}
|
|
551
609
|
async derived(copy, base, commit) {
|
|
552
|
-
const root = rootOf(copy);
|
|
610
|
+
const root = rootOf(copy.room_handle);
|
|
553
611
|
const before = await tree(copy.repository, base);
|
|
554
612
|
const after = await tree(copy.repository, commit);
|
|
555
613
|
const moved = movedPaths(copy.moves, before, after);
|
|
556
614
|
const arrived = new Set(moved.values());
|
|
557
|
-
const
|
|
615
|
+
const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
|
|
616
|
+
if (arriving.length > 0) {
|
|
617
|
+
const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
|
|
618
|
+
const binary = [];
|
|
619
|
+
for (const [path, entry] of arriving) {
|
|
620
|
+
if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
|
|
621
|
+
binary.push(path);
|
|
622
|
+
}
|
|
623
|
+
if (binary.length > 0)
|
|
624
|
+
throw new Error(textOnly(binary));
|
|
625
|
+
}
|
|
626
|
+
const text = (entry) => blobText(copy.repository, entry);
|
|
558
627
|
const changes = [];
|
|
559
628
|
for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
|
|
560
629
|
if (arrived.has(path))
|
|
@@ -567,7 +636,7 @@ export class LocalCopies {
|
|
|
567
636
|
changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
|
|
568
637
|
const moving = after.get(to);
|
|
569
638
|
if (moving !== prior) {
|
|
570
|
-
changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving
|
|
639
|
+
changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving) });
|
|
571
640
|
}
|
|
572
641
|
continue;
|
|
573
642
|
}
|
|
@@ -576,9 +645,9 @@ export class LocalCopies {
|
|
|
576
645
|
if (next === undefined)
|
|
577
646
|
changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
|
|
578
647
|
else if (prior === undefined)
|
|
579
|
-
changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next
|
|
648
|
+
changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
|
|
580
649
|
else
|
|
581
|
-
changes.push(...changed(`${root}/${path}`, path, known, await text(prior
|
|
650
|
+
changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
|
|
582
651
|
}
|
|
583
652
|
return changes;
|
|
584
653
|
}
|
|
@@ -605,12 +674,12 @@ export class LocalCopies {
|
|
|
605
674
|
const { upload, ...unstaged } = copy.push;
|
|
606
675
|
return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
|
|
607
676
|
}
|
|
608
|
-
if (outcome.refused.code === '
|
|
677
|
+
if (outcome.refused.code === 'expired')
|
|
609
678
|
return this.recovered(copy);
|
|
610
679
|
const rebased = rebaseFixes(outcome.refused);
|
|
611
680
|
let settled = await this.save(without(copy));
|
|
612
681
|
try {
|
|
613
|
-
settled =
|
|
682
|
+
settled = (await this.caughtUp([settled], true))[0];
|
|
614
683
|
}
|
|
615
684
|
catch {
|
|
616
685
|
return {
|
|
@@ -629,9 +698,9 @@ export class LocalCopies {
|
|
|
629
698
|
}
|
|
630
699
|
async recovered(copy) {
|
|
631
700
|
const push = copy.push;
|
|
632
|
-
const current =
|
|
701
|
+
const current = (await this.caughtUp([copy], true))[0];
|
|
633
702
|
const house = await tree(copy.repository, current.house);
|
|
634
|
-
const root = rootOf(current);
|
|
703
|
+
const root = rootOf(current.room_handle);
|
|
635
704
|
if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
|
|
636
705
|
await this.save(without(current));
|
|
637
706
|
return {
|
|
@@ -645,7 +714,7 @@ export class LocalCopies {
|
|
|
645
714
|
written.push({
|
|
646
715
|
path: `${root}/${path}`,
|
|
647
716
|
revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
|
|
648
|
-
content: entry === undefined ? null : await blobText(copy.repository, entry
|
|
717
|
+
content: entry === undefined ? null : await blobText(copy.repository, entry),
|
|
649
718
|
});
|
|
650
719
|
}
|
|
651
720
|
return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
|
|
@@ -673,7 +742,7 @@ export class LocalCopies {
|
|
|
673
742
|
const push = copy.push;
|
|
674
743
|
let written = push.written;
|
|
675
744
|
if (push.changes.some((change) => change.op === 'rename')) {
|
|
676
|
-
copy =
|
|
745
|
+
copy = (await this.caughtUp([copy], true))[0];
|
|
677
746
|
const house = await tree(copy.repository, copy.house);
|
|
678
747
|
const sent = await tree(copy.repository, push.house);
|
|
679
748
|
if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
|
|
@@ -687,9 +756,9 @@ export class LocalCopies {
|
|
|
687
756
|
written = [
|
|
688
757
|
...written,
|
|
689
758
|
{
|
|
690
|
-
path: `${rootOf(copy)}/${path}`,
|
|
759
|
+
path: `${rootOf(copy.room_handle)}/${path}`,
|
|
691
760
|
revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
|
|
692
|
-
content: entry === undefined ? null : await blobText(copy.repository, entry
|
|
761
|
+
content: entry === undefined ? null : await blobText(copy.repository, entry),
|
|
693
762
|
},
|
|
694
763
|
];
|
|
695
764
|
}
|
|
@@ -707,7 +776,7 @@ export class LocalCopies {
|
|
|
707
776
|
}
|
|
708
777
|
const edits = new Map([...files].filter(([path, content]) => (content === null ? selected.has(path) : selected.get(path) !== blobId(content))));
|
|
709
778
|
const received = [...files.keys()].filter((path) => revisionAt(copy, copy.house, path) === revisionAt(copy, push.house, path));
|
|
710
|
-
const accepted = `House ${rootOf(copy)} with ${push.commit} accepted`;
|
|
779
|
+
const accepted = `House ${rootOf(copy.room_handle)} with ${push.commit} accepted`;
|
|
711
780
|
const house = await importCommits(copy.repository, [
|
|
712
781
|
...(edits.size === 0 ? [] : [{ message: accepted, from: push.commit, files: edits }]),
|
|
713
782
|
{
|
|
@@ -734,7 +803,7 @@ export class LocalCopies {
|
|
|
734
803
|
async landed(repository, changes, house, selected) {
|
|
735
804
|
const text = async (path) => {
|
|
736
805
|
const entry = house.get(relative(path));
|
|
737
|
-
return entry === undefined ? undefined : blobText(repository, entry
|
|
806
|
+
return entry === undefined ? undefined : blobText(repository, entry);
|
|
738
807
|
};
|
|
739
808
|
for (const change of changes) {
|
|
740
809
|
const current = await text(change.path);
|
package/dist/end.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { forgetRejected } from './home.js';
|
|
2
|
+
import { installCommand } from './placement.js';
|
|
3
|
+
import { killDescendants } from './scope.js';
|
|
4
|
+
export const REPLACED = 'another Kit started for this Environment';
|
|
5
|
+
function exit(reason, status) {
|
|
6
|
+
process.stderr.write(`kit: ${reason}\n`);
|
|
7
|
+
killDescendants();
|
|
8
|
+
process.exit(status);
|
|
9
|
+
}
|
|
10
|
+
export function end(reason) {
|
|
11
|
+
exit(reason, 0);
|
|
12
|
+
}
|
|
13
|
+
export function restart(reason) {
|
|
14
|
+
exit(reason, 1);
|
|
15
|
+
}
|
|
16
|
+
export function updateRequired() {
|
|
17
|
+
const command = installCommand();
|
|
18
|
+
end(command === null ? 'This Kit is too old for House; install it again.' : `This Kit is too old for House. Update it: ${command}`);
|
|
19
|
+
}
|
|
20
|
+
export async function signInAgain(rejected) {
|
|
21
|
+
await forgetRejected(rejected);
|
|
22
|
+
restart('sign in again');
|
|
23
|
+
}
|
package/dist/enrol-main.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { writeEnrolment } from './home.js';
|
|
3
3
|
const [house, environment] = process.argv.slice(2);
|
|
4
|
-
|
|
4
|
+
const relay = process.env.HOUSE_KIT_RELAY || undefined;
|
|
5
|
+
await writeEnrolment({
|
|
6
|
+
house: house,
|
|
7
|
+
...(relay === undefined ? {} : { relay }),
|
|
8
|
+
environment: environment,
|
|
9
|
+
credential: readFileSync(0, 'utf8').trim(),
|
|
10
|
+
});
|