clavarium 0.2.0-next.4 → 0.2.0-next.5
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/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
- package/server/api.mjs +2 -2
- package/server/gitkeeper.mjs +10 -5
- package/server/gitpush.mjs +21 -8
- package/web/app.js +447 -172
- package/web/device.html +3 -3
- package/web/index.html +13 -11
- package/web/links.js +24 -8
- package/web/style.css +6 -1
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clavarium",
|
|
3
|
-
"version": "0.2.0-next.
|
|
3
|
+
"version": "0.2.0-next.5",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "clavarium",
|
|
9
|
-
"version": "0.2.0-next.
|
|
9
|
+
"version": "0.2.0-next.5",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
|
13
13
|
"@xterm/xterm": "^6.0.0",
|
|
14
|
-
"clavarium-parley": "0.10.0-next.
|
|
14
|
+
"clavarium-parley": "0.10.0-next.3",
|
|
15
15
|
"mermaid": "11.17.2",
|
|
16
16
|
"node-pty": "^1.1.0",
|
|
17
17
|
"pdfjs-dist": "6.4.299",
|
|
@@ -686,9 +686,9 @@
|
|
|
686
686
|
}
|
|
687
687
|
},
|
|
688
688
|
"node_modules/clavarium-parley": {
|
|
689
|
-
"version": "0.10.0-next.
|
|
690
|
-
"resolved": "https://registry.npmjs.org/clavarium-parley/-/clavarium-parley-0.10.0-next.
|
|
691
|
-
"integrity": "sha512
|
|
689
|
+
"version": "0.10.0-next.3",
|
|
690
|
+
"resolved": "https://registry.npmjs.org/clavarium-parley/-/clavarium-parley-0.10.0-next.3.tgz",
|
|
691
|
+
"integrity": "sha512-a750ZvTUDngz8nMdmja3/slCYCUCxM24uFEdgM1X4dNZ666Pf3HEPBbN9GwGgU6F+Ozc1JML5o87tCAMqcRyGQ==",
|
|
692
692
|
"license": "MIT",
|
|
693
693
|
"bin": {
|
|
694
694
|
"parley": "bin/parley.mjs",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clavarium",
|
|
3
|
-
"version": "0.2.0-next.
|
|
3
|
+
"version": "0.2.0-next.5",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A conversation is a VM with a terminal in it. Everything else is a folder.",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@xterm/addon-fit": "^0.11.0",
|
|
45
45
|
"@xterm/xterm": "^6.0.0",
|
|
46
|
-
"clavarium-parley": "0.10.0-next.
|
|
46
|
+
"clavarium-parley": "0.10.0-next.3",
|
|
47
47
|
"mermaid": "11.17.2",
|
|
48
48
|
"node-pty": "^1.1.0",
|
|
49
49
|
"pdfjs-dist": "6.4.299",
|
package/server/api.mjs
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* POST /api/updates/back { version }: go back to a release that is kept (in the background)
|
|
39
39
|
* GET /api/git/<name>/outgoing?target=<branch>&branch=<branch> what would go from the agent's branch, to that branch of the remote
|
|
40
40
|
* GET /api/git/<name>/graph the branch diagram: branches and their last commits
|
|
41
|
-
* GET /api/git/<name>/commit?id=<sha> one commit, as a review shows it
|
|
41
|
+
* GET /api/git/<name>/commit?id=<sha> one commit, as a review shows it (&base=<sha>: the commits from base to it)
|
|
42
42
|
* POST /api/git/<name>/push {head, target?, sure?}: exactly that reviewed commit, to exactly that branch
|
|
43
43
|
* GET /api/git/<name>/keep origin kept up to date, checks, automatic sending, log
|
|
44
44
|
* POST /api/git/<name>/keep {refreshMinutes?, alert?, checkCommand?, checkSeconds?, runHook?, requireForSend?, fsck?}
|
|
@@ -218,7 +218,7 @@ export async function handleApi(request, response, conversations, headers, resou
|
|
|
218
218
|
/* A repository: where its commits go, and what would go now. */
|
|
219
219
|
const repo = /^\/api\/git\/([a-z0-9-]{1,32})(\/outgoing|\/keep)?$/.exec(pathname);
|
|
220
220
|
const drawn = /^\/api\/git\/([a-z0-9-]{1,32})\/(graph|commit)$/.exec(pathname);
|
|
221
|
-
if (drawn && gitReview) return send(200, drawn[2] === 'graph' ? await gitReview.graph(drawn[1]) : await gitReview.commit(drawn[1], query.get('id'))), true;
|
|
221
|
+
if (drawn && gitReview) return send(200, drawn[2] === 'graph' ? await gitReview.graph(drawn[1]) : await gitReview.commit(drawn[1], query.get('id'), query.get('base'))), true;
|
|
222
222
|
if (repo && repo[2] === '/keep' && gitKeeper) return send(200, gitKeeper.about(repo[1])), true;
|
|
223
223
|
if (repo && gitReview) return send(200, repo[2] ? await gitReview.outgoing(repo[1], { target: query.get('target') ?? undefined, branch: query.get('branch') ?? undefined }) : gitReview.settings(repo[1])), true;
|
|
224
224
|
const watched = /^\/api\/conversations\/([^/]+)\/signin$/.exec(pathname);
|
package/server/gitkeeper.mjs
CHANGED
|
@@ -198,6 +198,11 @@ const TODO = Object.freeze({
|
|
|
198
198
|
/* Reasons whose sending stays stuck until somebody looks. */
|
|
199
199
|
const GRAVE_WHY = new Set(['refused-forced', 'refused-unknown-unreachable', 'refused-workspace']);
|
|
200
200
|
const commitsOf = (n) => `${n} commit${n === 1 ? '' : 's'}`;
|
|
201
|
+
/* A commit as a line of the chat names it: its first seven characters (the page makes them a link to it). */
|
|
202
|
+
const short = (id) => (typeof id === 'string' && /^[0-9a-f]{40}$/.test(id) ? id.slice(0, 7) : '');
|
|
203
|
+
const upTo = (id) => (short(id) ? ` (up to ${short(id)})` : '');
|
|
204
|
+
/* "See the N commits": from the tip before to the tip now, as the review shows them; none for a new branch. */
|
|
205
|
+
const seeCommits = (name, head, base, n) => (short(head) && short(base) && Number.isInteger(n) && n > 0 ? [`commits:${name}.${head.slice(0, 12)}.${base.slice(0, 12)}.${Math.min(n, 9999)}`] : []);
|
|
201
206
|
|
|
202
207
|
export class GitKeeper {
|
|
203
208
|
#store; #review; #resources; #checks; #alerts; #now; #tellAgent; #thread; #held = new Map(); #results = new Map(); #running = new Set(); #telling = new Map();
|
|
@@ -366,7 +371,7 @@ export class GitKeeper {
|
|
|
366
371
|
#say(name, conversations, event) {
|
|
367
372
|
let attached;
|
|
368
373
|
try { attached = Object.keys(this.#resources.list().find((x) => x.name === name)?.attachments ?? {}); } catch { return; }
|
|
369
|
-
for (const c of conversations) if (attached.includes(c)) { try { this.#thread(c, { ...event, actions: [`review:${name}`] }); } catch { /* a chat never stops git */ } }
|
|
374
|
+
for (const c of conversations) if (attached.includes(c)) { try { this.#thread(c, { ...event, actions: [...(event.actions ?? []), `review:${name}`] }); } catch { /* a chat never stops git */ } }
|
|
370
375
|
}
|
|
371
376
|
#attached(name) { try { return Object.entries(this.#resources.list().find((x) => x.name === name)?.attachments ?? {}).filter(([, access]) => access && access !== 'none').map(([c]) => c).sort(); } catch { return []; } }
|
|
372
377
|
/* Marked as told only once somebody was: a line typed to no machine
|
|
@@ -427,8 +432,8 @@ export class GitKeeper {
|
|
|
427
432
|
const brought = result.ok ? (result.brought ?? []) : [];
|
|
428
433
|
if (brought.length) {
|
|
429
434
|
const total = brought.reduce((n, x) => n + x.commits, 0);
|
|
430
|
-
const where = brought.slice(0, 4).map((x) => `${x.commits} on ${x.branch}`).join(', ') + (brought.length > 4 ? `, and ${brought.length - 4} more branch(es)` : '');
|
|
431
|
-
this.#say(name, this.#attached(name), { kind: 'origin', text: `Origin of ${name} updated: ${total} new commit${total === 1 ? '' : 's'} (${where}).`, topic: 'Origin updated', brief: name });
|
|
435
|
+
const where = brought.slice(0, 4).map((x) => `${x.commits} on ${x.branch}${upTo(x.head)}`).join(', ') + (brought.length > 4 ? `, and ${brought.length - 4} more branch(es)` : '');
|
|
436
|
+
this.#say(name, this.#attached(name), { kind: 'origin', text: `Origin of ${name} updated: ${total} new commit${total === 1 ? '' : 's'} (${where}).`, topic: 'Origin updated', brief: name, actions: brought.length === 1 ? seeCommits(name, brought[0].head, brought[0].base, brought[0].commits) : [] });
|
|
432
437
|
}
|
|
433
438
|
if (result.ok && Object.keys(record.blocked ?? {}).length > 0) void this.#settle(name).catch(() => {});
|
|
434
439
|
}
|
|
@@ -490,7 +495,7 @@ export class GitKeeper {
|
|
|
490
495
|
}
|
|
491
496
|
try {
|
|
492
497
|
const sent = await this.#review.push(r.name, body);
|
|
493
|
-
if (!sent.already) this.#say(r.name, this.#attached(r.name), { kind: 'sent-by-hand', text: `Sent by hand to ${sent.branch} of ${r.name}: ${commitsOf(sent.commits)}${sent.made ? ' (a new branch)' : ''}.`, topic: 'Sent', brief: sent.branch });
|
|
498
|
+
if (!sent.already) this.#say(r.name, this.#attached(r.name), { kind: 'sent-by-hand', text: `Sent by hand to ${sent.branch} of ${r.name}: ${commitsOf(sent.commits)}${upTo(sent.head)}${sent.made ? ' (a new branch)' : ''}.`, topic: 'Sent', brief: sent.branch, actions: seeCommits(r.name, sent.head, sent.base, sent.commits) });
|
|
494
499
|
return sent;
|
|
495
500
|
} catch (error) {
|
|
496
501
|
if (error instanceof GitFailure) this.#sendFailed(r.name, error, { branch: body?.target, head: body?.head });
|
|
@@ -604,7 +609,7 @@ export class GitKeeper {
|
|
|
604
609
|
if (sent.already) { this.#log(name, { branch: b, head: tip.id, conversation, ok: true, what: `Already on the remote: ${b}.` }, { sent: true, by: conversation }); return true; }
|
|
605
610
|
this.#log(name, { branch: b, head: tip.id, conversation, ok: true, what: `Sent automatically for ${conversation}: ${sent.commits} commit(s) to ${sent.branch}${sent.made ? ' (a new branch)' : ''}, ${checked}.${sent.note ? ` ${sent.note}` : ''}` }, { sent: true, by: conversation });
|
|
606
611
|
if (alert) this.#tell(`${name}: sent to ${sent.branch}`, `${sent.commits} commit(s), automatically for ${conversation}, ${checked}.`);
|
|
607
|
-
this.#say(name, [conversation], { kind: 'sent', text: `Sent to ${sent.branch} of ${name}: ${commitsOf(sent.commits)}${sent.made ? ' (a new branch)' : ''}, ${checked}.`, topic: 'Sent', brief: sent.branch });
|
|
612
|
+
this.#say(name, [conversation], { kind: 'sent', text: `Sent to ${sent.branch} of ${name}: ${commitsOf(sent.commits)}${upTo(sent.head)}${sent.made ? ' (a new branch)' : ''}, ${checked}.`, topic: 'Sent', brief: sent.branch, actions: seeCommits(name, sent.head, sent.base, sent.commits) });
|
|
608
613
|
return true;
|
|
609
614
|
} catch (error) {
|
|
610
615
|
const kind = error instanceof GitFailure ? error.kind : 'error';
|
package/server/gitpush.mjs
CHANGED
|
@@ -259,7 +259,7 @@ export class GitReview {
|
|
|
259
259
|
if (before.get(b) === now || rewritten.some((w) => w.branch === b) || (ours && ours.branch === b && ours.head === now)) continue;
|
|
260
260
|
const counted = await this.#runner.in(reference, ['rev-list', '--count', '--stdin', '--end-of-options'], { input: `${now}\n${had}\n`, deadlineMs: 120_000 }).catch(() => null);
|
|
261
261
|
const commits = Number(counted?.code === 0 ? counted.stdout.toString('latin1').trim() : 0) || 0;
|
|
262
|
-
if (commits > 0) brought.push({ branch: b, commits });
|
|
262
|
+
if (commits > 0) brought.push({ branch: b, commits, head: now, base: before.get(b) ?? null });
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
if (made) await this.#resources.shareOrigin(r.name).catch(() => {});
|
|
@@ -456,18 +456,31 @@ export class GitReview {
|
|
|
456
456
|
}
|
|
457
457
|
|
|
458
458
|
/* ONE COMMIT, as a review shows it: against its first parent (or
|
|
459
|
-
nothing, for the first commit).
|
|
460
|
-
|
|
459
|
+
nothing, for the first commit). Or, given `base`, the commits from
|
|
460
|
+
base to it (what a line of the chat said was sent or brought). Named
|
|
461
|
+
by their identifier, whole or its first 7 characters at least, as a
|
|
462
|
+
line of the chat shows them. Never sent from here. */
|
|
463
|
+
async commit(name, id, base) {
|
|
461
464
|
const r = this.#repository(name);
|
|
462
465
|
const asked = String(id ?? '');
|
|
463
|
-
|
|
466
|
+
const from = base === null || base === undefined || base === '' ? '' : String(base);
|
|
467
|
+
const SHORT = /^[0-9a-f]{7,40}$/;
|
|
468
|
+
if (!SHORT.test(asked) || (from && !SHORT.test(from))) throw new Refusal(400, 'A commit is named by its identifier (7 to 40 hexadecimal characters).');
|
|
464
469
|
return this.#withRead(r, async (got) => {
|
|
465
470
|
const p = await this.#runner.in(got.path, ['rev-list', '--parents', '--max-count=1', '--end-of-options', asked], { deadlineMs: 60_000 });
|
|
466
471
|
const ids = p.stdout.toString('latin1').trim().split(' ');
|
|
467
|
-
if (p.code !== 0 || ids[0]
|
|
472
|
+
if (p.code !== 0 || !isId(ids[0]) || !ids[0].startsWith(asked)) throw new Refusal(404, 'This commit is not in the repository.');
|
|
473
|
+
const whole = ids[0];
|
|
474
|
+
if (from) {
|
|
475
|
+
const q = await this.#runner.in(got.path, ['rev-list', '--max-count=1', '--end-of-options', from], { deadlineMs: 60_000 });
|
|
476
|
+
const was = q.stdout.toString('latin1').trim();
|
|
477
|
+
if (q.code !== 0 || !isId(was) || !was.startsWith(from)) throw new Refusal(404, 'This commit is not in the repository.');
|
|
478
|
+
const shown = await this.#compare(got.path, { head: whole, base: was, range: [whole], negatives: [was] });
|
|
479
|
+
return { ...shown, id: whole, base: was, parents: [was], head: whole, state: 'commit', message: '' };
|
|
480
|
+
}
|
|
468
481
|
const parents = ids.slice(1).filter(isId);
|
|
469
|
-
const shown = await this.#compare(got.path, { head:
|
|
470
|
-
return { ...shown, id:
|
|
482
|
+
const shown = await this.#compare(got.path, { head: whole, base: parents[0] ?? EMPTY_TREE, range: [whole], negatives: parents });
|
|
483
|
+
return { ...shown, id: whole, parents, head: whole, state: 'commit', message: parents.length > 1 ? 'A merge: its changes against its first parent.' : '' };
|
|
471
484
|
});
|
|
472
485
|
}
|
|
473
486
|
|
|
@@ -578,7 +591,7 @@ export class GitReview {
|
|
|
578
591
|
if (!isId(head) || (review.remoteTip !== null && review.remoteTip !== undefined && !isId(review.remoteTip))) throw new GitFailure(409, 'look', 'Look again at what would be sent.');
|
|
579
592
|
let remoteTip = review.remoteTip ?? null;
|
|
580
593
|
let count = review.count;
|
|
581
|
-
const sent = (extra) => ({ ok: true, branch: target, from: branch, head, commits: count ?? 0, made: !remoteTip, already: false, ...extra });
|
|
594
|
+
const sent = (extra) => ({ ok: true, branch: target, from: branch, head, base: remoteTip, commits: count ?? 0, made: !remoteTip, already: false, ...extra });
|
|
582
595
|
/* (1) What the agent has now, read again into a fresh workspace. */
|
|
583
596
|
let got;
|
|
584
597
|
try { got = await this.#read(r); } catch (error) {
|