@agentshouse/kit 0.2.1-cd9cbd8b6428c → 0.2.1-cde47fd72fe59
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 +34 -2
- package/dist/app-git.js +48 -0
- package/dist/bootstrap.json +4 -4
- package/dist/bridge.js +38 -58
- package/dist/chatgpt.js +32 -0
- package/dist/clis.js +137 -65
- package/dist/codex-main.js +58 -0
- package/dist/conversations.js +388 -276
- package/dist/copies.js +213 -145
- package/dist/end.js +23 -0
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +525 -0
- package/dist/files.js +22 -2
- 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/parts.js +82 -0
- package/dist/placement.js +30 -0
- package/dist/refusals.js +12 -0
- package/dist/relay.js +12 -2
- 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 +87 -11
- package/dist/transcription.js +57 -9
- package/dist/translate.js +2 -8
- 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,8 +2,8 @@ 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';
|
|
@@ -11,11 +11,13 @@ import { changed } from './translate.js';
|
|
|
11
11
|
import { relayed } from './relay.js';
|
|
12
12
|
const BRANCH = 'main';
|
|
13
13
|
const PROVENANCE = '_provenance';
|
|
14
|
-
const REPLICA_RETRY_MS = 30_000;
|
|
15
14
|
const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
|
|
16
15
|
function causeOf(error) {
|
|
17
16
|
return error instanceof Error ? error.message : String(error);
|
|
18
17
|
}
|
|
18
|
+
function houseCause(error) {
|
|
19
|
+
return error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
|
|
20
|
+
}
|
|
19
21
|
function logged(error) {
|
|
20
22
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
21
23
|
}
|
|
@@ -30,9 +32,35 @@ const REBASED = new Set([
|
|
|
30
32
|
function rebaseFixes(refusal) {
|
|
31
33
|
return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
|
|
32
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
|
+
}
|
|
33
42
|
function short(commit) {
|
|
34
43
|
return commit.slice(0, 12);
|
|
35
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
|
+
}
|
|
36
64
|
function covers(path, prefix) {
|
|
37
65
|
return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
|
|
38
66
|
}
|
|
@@ -41,11 +69,14 @@ function sidecar(path) {
|
|
|
41
69
|
segments.splice(-1, 0, PROVENANCE);
|
|
42
70
|
return segments.join('/');
|
|
43
71
|
}
|
|
44
|
-
function
|
|
45
|
-
return
|
|
72
|
+
function recordOf(key) {
|
|
73
|
+
return join(kitHome(), 'local-copies', `${key}.json`);
|
|
74
|
+
}
|
|
75
|
+
function rootOf(handle) {
|
|
76
|
+
return `/rooms/${handle}`;
|
|
46
77
|
}
|
|
47
78
|
function relative(path) {
|
|
48
|
-
return path.replace(/^\/
|
|
79
|
+
return path.replace(/^\/rooms\/[^/]+\//, '');
|
|
49
80
|
}
|
|
50
81
|
function declaredWritable(path, copy) {
|
|
51
82
|
const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
|
|
@@ -153,19 +184,18 @@ export class LocalCopies {
|
|
|
153
184
|
house;
|
|
154
185
|
copies = new Map();
|
|
155
186
|
queues = new Map();
|
|
156
|
-
|
|
157
|
-
selecting = Promise.resolve();
|
|
187
|
+
answering = Promise.resolve();
|
|
158
188
|
constructor(house) {
|
|
159
189
|
this.house = house;
|
|
160
190
|
this.owner = {
|
|
161
191
|
key: 'owner',
|
|
162
192
|
submit: async (operation, changes) => {
|
|
163
193
|
try {
|
|
164
|
-
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 }));
|
|
165
195
|
return { accepted: outcome.contents };
|
|
166
196
|
}
|
|
167
197
|
catch (error) {
|
|
168
|
-
if (!(error
|
|
198
|
+
if (!finalRefusal(error))
|
|
169
199
|
throw error;
|
|
170
200
|
return {
|
|
171
201
|
refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
|
|
@@ -177,7 +207,7 @@ export class LocalCopies {
|
|
|
177
207
|
async load() {
|
|
178
208
|
const home = join(kitHome(), 'local-copies');
|
|
179
209
|
const names = (await readdir(join(home, 'rooms')).catch(() => [])).filter((name) => name.endsWith('.json'));
|
|
180
|
-
for (const path of
|
|
210
|
+
for (const path of names.map((name) => join(home, 'rooms', name))) {
|
|
181
211
|
const text = await readFile(path, 'utf8').catch(() => null);
|
|
182
212
|
if (text === null)
|
|
183
213
|
continue;
|
|
@@ -186,47 +216,81 @@ export class LocalCopies {
|
|
|
186
216
|
}
|
|
187
217
|
}
|
|
188
218
|
async mapping() {
|
|
189
|
-
await this.
|
|
219
|
+
await this.answering;
|
|
190
220
|
return [...this.copies.values()]
|
|
191
|
-
.filter((copy) => copy.
|
|
192
|
-
.map((copy) => `${rootOf(copy)}: ${copy.repository}`)
|
|
221
|
+
.filter((copy) => copy.erasing === undefined)
|
|
222
|
+
.map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
|
|
193
223
|
.sort();
|
|
194
224
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
this.selecting = selected.catch(() => undefined);
|
|
198
|
-
return selected;
|
|
199
|
-
}
|
|
200
|
-
async reselect() {
|
|
201
|
-
const { local_copy: chosen } = await this.house.post('/kit/local-copy/selection', {});
|
|
202
|
-
const selected = [
|
|
203
|
-
...(chosen?.rooms ?? [])
|
|
204
|
-
.filter((room) => room.room_handle !== null)
|
|
205
|
-
.map((room) => ({ key: `rooms/${room.room_ref}`, private: false, handle: room.room_handle })),
|
|
206
|
-
...(chosen?.private === true ? [{ key: 'private', private: true, handle: null }] : []),
|
|
207
|
-
];
|
|
208
|
-
const keys = new Set(selected.map((scope) => scope.key));
|
|
209
|
-
const work = [];
|
|
210
|
-
for (const key of this.copies.keys()) {
|
|
211
|
-
if (keys.has(key))
|
|
212
|
-
continue;
|
|
213
|
-
work.push(this.exclusive(key, async () => {
|
|
214
|
-
const copy = this.copies.get(key);
|
|
215
|
-
if (copy.active)
|
|
216
|
-
await this.save({ ...copy, active: false });
|
|
217
|
-
}));
|
|
218
|
-
}
|
|
219
|
-
for (const scope of selected)
|
|
220
|
-
work.push(this.exclusive(scope.key, () => this.apply(scope)).catch(logged));
|
|
221
|
-
await Promise.all(work);
|
|
225
|
+
connected() {
|
|
226
|
+
return this.selected();
|
|
222
227
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
+
});
|
|
230
294
|
}
|
|
231
295
|
async observed(report) {
|
|
232
296
|
const key = await this.keyAt(dirname(report.common));
|
|
@@ -234,6 +298,8 @@ export class LocalCopies {
|
|
|
234
298
|
return;
|
|
235
299
|
await this.exclusive(key, async () => {
|
|
236
300
|
const copy = this.copies.get(key);
|
|
301
|
+
if (copy === undefined || copy.erasing)
|
|
302
|
+
return;
|
|
237
303
|
let moves = copy.moves;
|
|
238
304
|
for (const { from, to } of report.moves) {
|
|
239
305
|
const held = moves.find((move) => move.worktree === report.worktree && move.to === from);
|
|
@@ -256,11 +322,11 @@ export class LocalCopies {
|
|
|
256
322
|
async push(cwd, selector, caller) {
|
|
257
323
|
const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
|
|
258
324
|
if (located.status !== 0)
|
|
259
|
-
return
|
|
325
|
+
return outside(cwd);
|
|
260
326
|
const [common, worktree] = located.stdout.toString('utf8').split('\n');
|
|
261
327
|
const key = await this.keyAt(dirname(common));
|
|
262
328
|
if (key === undefined)
|
|
263
|
-
return
|
|
329
|
+
return outside(worktree);
|
|
264
330
|
return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
|
|
265
331
|
}
|
|
266
332
|
async keyAt(repository) {
|
|
@@ -276,19 +342,12 @@ export class LocalCopies {
|
|
|
276
342
|
this.queues.set(key, queued.catch(() => undefined));
|
|
277
343
|
return queued;
|
|
278
344
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
this.delivering.add(key);
|
|
283
|
-
void this.exclusive(key, async () => {
|
|
284
|
-
this.delivering.delete(key);
|
|
285
|
-
const copy = this.copies.get(key);
|
|
286
|
-
if (copy.active)
|
|
287
|
-
await this.save(await this.caughtUp(copy));
|
|
288
|
-
}).catch(logged);
|
|
345
|
+
holding(keys, work) {
|
|
346
|
+
const [key, ...rest] = keys;
|
|
347
|
+
return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
|
|
289
348
|
}
|
|
290
349
|
async save(copy) {
|
|
291
|
-
const path =
|
|
350
|
+
const path = recordOf(copy.key);
|
|
292
351
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
293
352
|
const temporary = `${path}.${process.pid}`;
|
|
294
353
|
await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
|
|
@@ -296,8 +355,14 @@ export class LocalCopies {
|
|
|
296
355
|
this.copies.set(copy.key, copy);
|
|
297
356
|
return copy;
|
|
298
357
|
}
|
|
299
|
-
async
|
|
300
|
-
|
|
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)];
|
|
301
366
|
const discovery = await this.house.post('/kit/door/discover', { prefixes });
|
|
302
367
|
let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
|
|
303
368
|
if ('transfer' in bundle)
|
|
@@ -331,46 +396,13 @@ export class LocalCopies {
|
|
|
331
396
|
}
|
|
332
397
|
return read;
|
|
333
398
|
}
|
|
334
|
-
async
|
|
335
|
-
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
copy = await this.save({ ...copy, room_handle: scope.handle });
|
|
342
|
-
if (!copy.active) {
|
|
343
|
-
await this.save(await this.received({ ...copy, active: true }));
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
const discovery = await this.house.post('/kit/door/discover', {
|
|
347
|
-
prefixes: [rootOf(copy)],
|
|
348
|
-
});
|
|
349
|
-
const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
350
|
-
if (authority === undefined)
|
|
351
|
-
return;
|
|
352
|
-
if (authority.managed_replication !== 'admitted') {
|
|
353
|
-
logged(`operation_denied: ${rootOf(copy)} is not synchronized to this Environment`);
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
|
|
357
|
-
JSON.stringify([copy.write, copy.protected])) {
|
|
358
|
-
await this.save(await this.received(copy));
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
await this.save(await this.caughtUp(copy));
|
|
362
|
-
}
|
|
363
|
-
async materialize(scope) {
|
|
364
|
-
const read = await this.read({ private: scope.private, room_handle: scope.handle });
|
|
365
|
-
if (read === null) {
|
|
366
|
-
logged(`House serves nothing of ${rootOf({ private: scope.private, room_handle: scope.handle })} to copy`);
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
const repository = scope.private ? join(localCopies(), 'private') : join(localCopies(), 'rooms', read.anchor);
|
|
370
|
-
if (await exists(repository)) {
|
|
371
|
-
logged(`${repository} already exists and House Kit leaves it as it is`);
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
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`);
|
|
374
406
|
const staging = join(dirname(repository), `.${randomUUID()}`);
|
|
375
407
|
await mkdir(staging, { recursive: true });
|
|
376
408
|
let house;
|
|
@@ -378,7 +410,7 @@ export class LocalCopies {
|
|
|
378
410
|
try {
|
|
379
411
|
await git(staging, ['init', '-q', '-b', BRANCH]);
|
|
380
412
|
house = await importCommits(staging, [
|
|
381
|
-
{ message: `House ${rootOf(
|
|
413
|
+
{ message: `House ${rootOf(handle)}`, from: null, files: read.writable },
|
|
382
414
|
]);
|
|
383
415
|
await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
|
|
384
416
|
await git(staging, ['reset', '-q', '--hard']);
|
|
@@ -391,12 +423,10 @@ export class LocalCopies {
|
|
|
391
423
|
throw error;
|
|
392
424
|
}
|
|
393
425
|
await this.save({
|
|
394
|
-
key
|
|
395
|
-
private: scope.private,
|
|
426
|
+
key,
|
|
396
427
|
room_ref: read.anchor,
|
|
397
|
-
room_handle:
|
|
428
|
+
room_handle: handle,
|
|
398
429
|
repository,
|
|
399
|
-
active: true,
|
|
400
430
|
house,
|
|
401
431
|
position: read.position,
|
|
402
432
|
log_epoch: read.log_epoch,
|
|
@@ -406,13 +436,14 @@ export class LocalCopies {
|
|
|
406
436
|
manifests: { [house]: { parent: null, files: read.revisions } },
|
|
407
437
|
moves: [],
|
|
408
438
|
});
|
|
439
|
+
return `${rootOf(handle)}: copied to ${repository}`;
|
|
409
440
|
}
|
|
410
441
|
async received(copy) {
|
|
411
|
-
const read = await this.read(copy);
|
|
442
|
+
const read = await this.read(copy.room_handle);
|
|
412
443
|
if (read === null)
|
|
413
444
|
return copy;
|
|
414
445
|
const house = await importCommits(copy.repository, [
|
|
415
|
-
{ 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 },
|
|
416
447
|
]);
|
|
417
448
|
const removed = copy.read_only.filter((path) => !read.readOnly.has(path));
|
|
418
449
|
const readOnly = await readOnlyPlaced(copy.repository, copy.read_only, read.readOnly, removed);
|
|
@@ -429,30 +460,57 @@ export class LocalCopies {
|
|
|
429
460
|
await ignoreRules(copy.repository, [...read.writable.keys()], next);
|
|
430
461
|
return next;
|
|
431
462
|
}
|
|
432
|
-
async
|
|
433
|
-
|
|
434
|
-
|
|
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) {
|
|
435
487
|
let answer;
|
|
436
488
|
try {
|
|
437
489
|
answer = await this.house.post('/kit/door/enumerate', {
|
|
438
|
-
prefixes:
|
|
439
|
-
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 })),
|
|
440
492
|
});
|
|
441
493
|
}
|
|
442
494
|
catch (error) {
|
|
443
|
-
const
|
|
444
|
-
if (
|
|
445
|
-
return this.received(copy);
|
|
446
|
-
if (code !== 'replica_behind')
|
|
495
|
+
const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
|
|
496
|
+
if (expired === undefined)
|
|
447
497
|
throw error;
|
|
448
|
-
|
|
449
|
-
|
|
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);
|
|
450
510
|
}
|
|
451
|
-
|
|
452
|
-
if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
|
|
453
|
-
return copy;
|
|
454
|
-
copy = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
|
|
511
|
+
asking = whole ? advanced : [];
|
|
455
512
|
}
|
|
513
|
+
return copies.map((copy) => caught.get(copy.key));
|
|
456
514
|
}
|
|
457
515
|
async incorporated(copy, changes, position, logEpoch) {
|
|
458
516
|
const houseTree = await tree(copy.repository, copy.house);
|
|
@@ -485,7 +543,7 @@ export class LocalCopies {
|
|
|
485
543
|
let { house, manifests } = copy;
|
|
486
544
|
if (files.size > 0) {
|
|
487
545
|
house = await importCommits(copy.repository, [
|
|
488
|
-
{ 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 },
|
|
489
547
|
]);
|
|
490
548
|
manifests = { ...manifests, [house]: { parent: copy.house, files: revisions } };
|
|
491
549
|
}
|
|
@@ -502,6 +560,8 @@ export class LocalCopies {
|
|
|
502
560
|
}
|
|
503
561
|
async pushed(key, worktree, selector, caller) {
|
|
504
562
|
let copy = this.copies.get(key);
|
|
563
|
+
if (copy === undefined || copy.erasing)
|
|
564
|
+
return outside(worktree);
|
|
505
565
|
if (copy.push?.state === 'submitted')
|
|
506
566
|
return this.settle(copy, caller);
|
|
507
567
|
const selected = await run(worktree, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${selector}^{commit}`]);
|
|
@@ -515,9 +575,6 @@ export class LocalCopies {
|
|
|
515
575
|
}
|
|
516
576
|
copy = await this.save(without(copy));
|
|
517
577
|
}
|
|
518
|
-
if (!copy.active) {
|
|
519
|
-
return { refused: true, text: `${rootOf(copy)} is no longer synced to this Environment, so House takes no push from it` };
|
|
520
|
-
}
|
|
521
578
|
const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
|
|
522
579
|
const base = listed.find((ancestor) => ancestor in copy.manifests);
|
|
523
580
|
if (base === undefined) {
|
|
@@ -528,7 +585,7 @@ export class LocalCopies {
|
|
|
528
585
|
changes = await this.derived(copy, base, commit);
|
|
529
586
|
}
|
|
530
587
|
catch (error) {
|
|
531
|
-
return { refused: true, text:
|
|
588
|
+
return { refused: true, text: houseCause(error) };
|
|
532
589
|
}
|
|
533
590
|
if (changes.length === 0)
|
|
534
591
|
return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
|
|
@@ -550,12 +607,23 @@ export class LocalCopies {
|
|
|
550
607
|
return this.settle(submitted, caller);
|
|
551
608
|
}
|
|
552
609
|
async derived(copy, base, commit) {
|
|
553
|
-
const root = rootOf(copy);
|
|
610
|
+
const root = rootOf(copy.room_handle);
|
|
554
611
|
const before = await tree(copy.repository, base);
|
|
555
612
|
const after = await tree(copy.repository, commit);
|
|
556
613
|
const moved = movedPaths(copy.moves, before, after);
|
|
557
614
|
const arrived = new Set(moved.values());
|
|
558
|
-
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);
|
|
559
627
|
const changes = [];
|
|
560
628
|
for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
|
|
561
629
|
if (arrived.has(path))
|
|
@@ -568,7 +636,7 @@ export class LocalCopies {
|
|
|
568
636
|
changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
|
|
569
637
|
const moving = after.get(to);
|
|
570
638
|
if (moving !== prior) {
|
|
571
|
-
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) });
|
|
572
640
|
}
|
|
573
641
|
continue;
|
|
574
642
|
}
|
|
@@ -577,9 +645,9 @@ export class LocalCopies {
|
|
|
577
645
|
if (next === undefined)
|
|
578
646
|
changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
|
|
579
647
|
else if (prior === undefined)
|
|
580
|
-
changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next
|
|
648
|
+
changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
|
|
581
649
|
else
|
|
582
|
-
changes.push(...changed(`${root}/${path}`, path, known, await text(prior
|
|
650
|
+
changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
|
|
583
651
|
}
|
|
584
652
|
return changes;
|
|
585
653
|
}
|
|
@@ -606,12 +674,12 @@ export class LocalCopies {
|
|
|
606
674
|
const { upload, ...unstaged } = copy.push;
|
|
607
675
|
return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
|
|
608
676
|
}
|
|
609
|
-
if (outcome.refused.code === '
|
|
677
|
+
if (outcome.refused.code === 'expired')
|
|
610
678
|
return this.recovered(copy);
|
|
611
679
|
const rebased = rebaseFixes(outcome.refused);
|
|
612
680
|
let settled = await this.save(without(copy));
|
|
613
681
|
try {
|
|
614
|
-
settled =
|
|
682
|
+
settled = (await this.caughtUp([settled], true))[0];
|
|
615
683
|
}
|
|
616
684
|
catch {
|
|
617
685
|
return {
|
|
@@ -630,9 +698,9 @@ export class LocalCopies {
|
|
|
630
698
|
}
|
|
631
699
|
async recovered(copy) {
|
|
632
700
|
const push = copy.push;
|
|
633
|
-
const current =
|
|
701
|
+
const current = (await this.caughtUp([copy], true))[0];
|
|
634
702
|
const house = await tree(copy.repository, current.house);
|
|
635
|
-
const root = rootOf(current);
|
|
703
|
+
const root = rootOf(current.room_handle);
|
|
636
704
|
if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
|
|
637
705
|
await this.save(without(current));
|
|
638
706
|
return {
|
|
@@ -646,7 +714,7 @@ export class LocalCopies {
|
|
|
646
714
|
written.push({
|
|
647
715
|
path: `${root}/${path}`,
|
|
648
716
|
revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
|
|
649
|
-
content: entry === undefined ? null : await blobText(copy.repository, entry
|
|
717
|
+
content: entry === undefined ? null : await blobText(copy.repository, entry),
|
|
650
718
|
});
|
|
651
719
|
}
|
|
652
720
|
return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
|
|
@@ -674,7 +742,7 @@ export class LocalCopies {
|
|
|
674
742
|
const push = copy.push;
|
|
675
743
|
let written = push.written;
|
|
676
744
|
if (push.changes.some((change) => change.op === 'rename')) {
|
|
677
|
-
copy =
|
|
745
|
+
copy = (await this.caughtUp([copy], true))[0];
|
|
678
746
|
const house = await tree(copy.repository, copy.house);
|
|
679
747
|
const sent = await tree(copy.repository, push.house);
|
|
680
748
|
if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
|
|
@@ -688,9 +756,9 @@ export class LocalCopies {
|
|
|
688
756
|
written = [
|
|
689
757
|
...written,
|
|
690
758
|
{
|
|
691
|
-
path: `${rootOf(copy)}/${path}`,
|
|
759
|
+
path: `${rootOf(copy.room_handle)}/${path}`,
|
|
692
760
|
revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
|
|
693
|
-
content: entry === undefined ? null : await blobText(copy.repository, entry
|
|
761
|
+
content: entry === undefined ? null : await blobText(copy.repository, entry),
|
|
694
762
|
},
|
|
695
763
|
];
|
|
696
764
|
}
|
|
@@ -708,7 +776,7 @@ export class LocalCopies {
|
|
|
708
776
|
}
|
|
709
777
|
const edits = new Map([...files].filter(([path, content]) => (content === null ? selected.has(path) : selected.get(path) !== blobId(content))));
|
|
710
778
|
const received = [...files.keys()].filter((path) => revisionAt(copy, copy.house, path) === revisionAt(copy, push.house, path));
|
|
711
|
-
const accepted = `House ${rootOf(copy)} with ${push.commit} accepted`;
|
|
779
|
+
const accepted = `House ${rootOf(copy.room_handle)} with ${push.commit} accepted`;
|
|
712
780
|
const house = await importCommits(copy.repository, [
|
|
713
781
|
...(edits.size === 0 ? [] : [{ message: accepted, from: push.commit, files: edits }]),
|
|
714
782
|
{
|
|
@@ -735,7 +803,7 @@ export class LocalCopies {
|
|
|
735
803
|
async landed(repository, changes, house, selected) {
|
|
736
804
|
const text = async (path) => {
|
|
737
805
|
const entry = house.get(relative(path));
|
|
738
|
-
return entry === undefined ? undefined : blobText(repository, entry
|
|
806
|
+
return entry === undefined ? undefined : blobText(repository, entry);
|
|
739
807
|
};
|
|
740
808
|
for (const change of changes) {
|
|
741
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
|
+
});
|