@astrosheep/keiyaku 4.1.0 → 4.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +3 -0
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +3 -0
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +3 -6
- package/build/src/akuma/akuma-errors.d.ts +6 -0
- package/build/src/akuma/akuma-errors.js +9 -0
- package/build/src/akuma/akuma-handle.d.ts +39 -0
- package/build/src/akuma/akuma-handle.js +263 -0
- package/build/src/akuma/akuma-observe.d.ts +19 -0
- package/build/src/akuma/akuma-observe.js +74 -0
- package/build/src/akuma/akuma-product-symbols.d.ts +1 -0
- package/build/src/akuma/akuma-product-symbols.js +1 -0
- package/build/src/akuma/akuma-product.d.ts +18 -0
- package/build/src/akuma/akuma-product.js +154 -0
- package/build/src/akuma/akuma.d.ts +37 -81
- package/build/src/akuma/akuma.js +39 -447
- package/build/src/akuma/allowed.d.ts +1 -0
- package/build/src/akuma/allowed.js +6 -0
- package/build/src/akuma/archetype.js +8 -5
- package/build/src/akuma/body-supervisor.d.ts +26 -0
- package/build/src/akuma/body-supervisor.js +89 -0
- package/build/src/akuma/body.d.ts +45 -4
- package/build/src/akuma/body.js +145 -35
- package/build/src/akuma/coordinate.js +17 -7
- package/build/src/akuma/heart/facts.js +1 -1
- package/build/src/akuma/heart/index.d.ts +1 -1
- package/build/src/akuma/heart/index.js +2 -4
- package/build/src/akuma/heart/request-authority.js +4 -4
- package/build/src/akuma/heart/request-rows.js +27 -16
- package/build/src/akuma/heart/rows.js +79 -41
- package/build/src/akuma/heart/soul.js +1 -1
- package/build/src/akuma/heart/storage.d.ts +1 -0
- package/build/src/akuma/heart/storage.js +76 -11
- package/build/src/akuma/heart/tells.js +35 -18
- package/build/src/akuma/heart/timeline.js +19 -11
- package/build/src/akuma/identity.d.ts +1 -0
- package/build/src/akuma/identity.js +1 -0
- package/build/src/akuma/index.d.ts +3 -3
- package/build/src/akuma/index.js +1 -1
- package/build/src/akuma/nuke.js +106 -14
- package/build/src/akuma/projection-read.d.ts +17 -0
- package/build/src/akuma/projection-read.js +135 -0
- package/build/src/akuma/projection.d.ts +1 -16
- package/build/src/akuma/projection.js +77 -154
- package/build/src/akuma/provider-recipe.js +11 -5
- package/build/src/akuma/provider.js +105 -62
- package/build/src/akuma/providers/acp/config.js +5 -6
- package/build/src/akuma/providers/acp/core.js +17 -10
- package/build/src/akuma/providers/acp/events.js +38 -29
- package/build/src/akuma/providers/acp/index.js +13 -9
- package/build/src/akuma/providers/claude/events.js +8 -12
- package/build/src/akuma/providers/claude/index.d.ts +1 -0
- package/build/src/akuma/providers/claude/index.js +55 -26
- package/build/src/akuma/providers/claude/input.js +6 -2
- package/build/src/akuma/providers/codex-app-server/events.js +61 -36
- package/build/src/akuma/providers/codex-app-server/index.js +25 -13
- package/build/src/akuma/providers/grok-build/index.js +22 -25
- package/build/src/akuma/providers/index.js +2 -18
- package/build/src/akuma/providers/opencode-sdk/events.js +32 -18
- package/build/src/akuma/providers/opencode-sdk/index.js +26 -17
- package/build/src/akuma/providers/opencode-sdk/session.js +21 -5
- package/build/src/akuma/providers/pi/events.js +26 -19
- package/build/src/akuma/providers/pi/index.js +58 -40
- package/build/src/akuma/publication.js +9 -9
- package/build/src/akuma/request-execution.d.ts +6 -0
- package/build/src/akuma/request-execution.js +86 -0
- package/build/src/akuma/request-serve.d.ts +12 -1
- package/build/src/akuma/request-serve.js +19 -122
- package/build/src/akuma/request-wire.d.ts +2 -2
- package/build/src/akuma/request-wire.js +75 -102
- package/build/src/akuma/requests.js +2 -2
- package/build/src/akuma/{body-turn.d.ts → turn-drive.d.ts} +2 -25
- package/build/src/akuma/{body-turn.js → turn-drive.js} +20 -103
- package/build/src/akuma-body.js +6 -5
- package/build/src/body/amend.d.ts +4 -1
- package/build/src/body/amend.js +14 -6
- package/build/src/body/decode.js +2 -4
- package/build/src/body/envelope.js +2 -4
- package/build/src/body/region.d.ts +1 -2
- package/build/src/body/region.js +2 -70
- package/build/src/body/render.js +17 -12
- package/build/src/body/shape.js +1 -7
- package/build/src/cli/accepted.d.ts +1 -1
- package/build/src/cli/accepted.js +8 -1
- package/build/src/cli/commands/akuma-invoke.js +18 -6
- package/build/src/cli/commands/akuma.d.ts +0 -6
- package/build/src/cli/commands/akuma.js +11 -24
- package/build/src/cli/commands/amend.d.ts +2 -2
- package/build/src/cli/commands/amend.js +2 -4
- package/build/src/cli/commands/bind.d.ts +4 -4
- package/build/src/cli/commands/bind.js +16 -6
- package/build/src/cli/commands/contract-invoke.d.ts +25 -0
- package/build/src/cli/commands/contract-invoke.js +193 -0
- package/build/src/cli/commands/contract.d.ts +109 -10
- package/build/src/cli/commands/contract.js +339 -16
- package/build/src/cli/commands/install.d.ts +1 -1
- package/build/src/cli/commands/install.js +23 -15
- package/build/src/cli/commands/task-invoke.js +187 -67
- package/build/src/cli/commands/task-query.js +48 -24
- package/build/src/cli/commands/task.js +150 -38
- package/build/src/cli/coordinates.js +2 -4
- package/build/src/cli/invoke.js +92 -208
- package/build/src/cli/main.js +4 -2
- package/build/src/cli/parse.d.ts +3 -90
- package/build/src/cli/parse.js +27 -233
- package/build/src/cli/render/akuma-activity.d.ts +38 -0
- package/build/src/cli/render/akuma-activity.js +362 -0
- package/build/src/cli/render/akuma-tool-command.js +6 -7
- package/build/src/cli/render/akuma-tool.js +10 -8
- package/build/src/cli/render/akuma.d.ts +1 -1
- package/build/src/cli/render/akuma.js +61 -353
- package/build/src/cli/render/audit.js +5 -15
- package/build/src/cli/render/board.d.ts +1 -0
- package/build/src/cli/render/board.js +17 -0
- package/build/src/cli/render/catalog.js +81 -8
- package/build/src/cli/render/contract-history.d.ts +2 -0
- package/build/src/cli/render/contract-history.js +83 -0
- package/build/src/cli/render/contract-observation.d.ts +10 -0
- package/build/src/cli/render/contract-observation.js +58 -0
- package/build/src/cli/render/contract.d.ts +1 -2
- package/build/src/cli/render/contract.js +41 -142
- package/build/src/cli/render/kanshi.js +239 -215
- package/build/src/cli/render/receipt.js +2 -9
- package/build/src/cli/render/refusal.js +10 -15
- package/build/src/cli/render/region.js +22 -6
- package/build/src/cli/render/settings.js +4 -4
- package/build/src/cli/render/task.js +34 -14
- package/build/src/cli/render/terminal.js +16 -5
- package/build/src/cli/result.d.ts +3 -3
- package/build/src/cli/runtime.d.ts +6 -1
- package/build/src/cli/runtime.js +84 -63
- package/build/src/cli/selectors.js +2 -4
- package/build/src/cli/usage.js +6 -5
- package/build/src/contract-worktree.d.ts +1 -23
- package/build/src/contract-worktree.js +79 -155
- package/build/src/coordination/durable-file.js +7 -3
- package/build/src/coordination/sqlite-transaction-lock.js +29 -12
- package/build/src/core/facts/codec.js +7 -3
- package/build/src/core/facts/types.d.ts +1 -1
- package/build/src/core/subject.js +16 -9
- package/build/src/core/verbs/abandon.d.ts +1 -1
- package/build/src/core/verbs/abandon.js +1 -1
- package/build/src/core/verbs/amend.d.ts +1 -1
- package/build/src/core/verbs/amend.js +7 -7
- package/build/src/core/verbs/attestation.js +4 -1
- package/build/src/core/verbs/bind.d.ts +1 -1
- package/build/src/core/verbs/bind.js +1 -1
- package/build/src/core/verbs/placement.d.ts +1 -1
- package/build/src/core/verbs/placement.js +10 -8
- package/build/src/dispatch/index.js +8 -6
- package/build/src/duration.js +5 -1
- package/build/src/git/admission.js +5 -3
- package/build/src/git/hooks.d.ts +8 -0
- package/build/src/git/hooks.js +63 -14
- package/build/src/git/identity.js +1 -4
- package/build/src/git/integration.d.ts +2 -3
- package/build/src/git/integration.js +32 -39
- package/build/src/git/nuke.d.ts +1 -1
- package/build/src/git/nuke.js +110 -57
- package/build/src/git/observe.d.ts +13 -1
- package/build/src/git/observe.js +51 -44
- package/build/src/git/process.js +4 -2
- package/build/src/git/read-observation.js +10 -8
- package/build/src/git/reconcile.d.ts +22 -2
- package/build/src/git/reconcile.js +38 -274
- package/build/src/git/repository.d.ts +2 -0
- package/build/src/git/repository.js +13 -22
- package/build/src/git/scratch.d.ts +8 -5
- package/build/src/git/scratch.js +29 -2
- package/build/src/git/target-placement.d.ts +9 -10
- package/build/src/git/target-placement.js +59 -62
- package/build/src/git/tender.js +13 -46
- package/build/src/git/terminal-reconcile.d.ts +21 -0
- package/build/src/git/terminal-reconcile.js +278 -0
- package/build/src/git/terminal-seal.js +15 -12
- package/build/src/git/tree.js +5 -2
- package/build/src/git/workspace.d.ts +24 -2
- package/build/src/git/workspace.js +120 -16
- package/build/src/identity/coordinates.js +3 -1
- package/build/src/identity/selector.js +6 -3
- package/build/src/kanshi/fleet.d.ts +6 -0
- package/build/src/kanshi/fleet.js +27 -0
- package/build/src/kanshi/index.d.ts +1 -1
- package/build/src/kanshi/read.js +60 -48
- package/build/src/kanshi/report.d.ts +11 -23
- package/build/src/kanshi/select.js +25 -24
- package/build/src/library/address.d.ts +2 -1
- package/build/src/library/address.js +7 -8
- package/build/src/library/akuma-creation.js +20 -12
- package/build/src/library/audit.d.ts +0 -1
- package/build/src/library/audit.js +1 -2
- package/build/src/library/bind.d.ts +53 -5
- package/build/src/library/bind.js +98 -22
- package/build/src/library/catalog.d.ts +1 -0
- package/build/src/library/catalog.js +6 -3
- package/build/src/library/configuration.js +3 -1
- package/build/src/library/continuation.d.ts +15 -4
- package/build/src/library/continuation.js +24 -16
- package/build/src/library/contract-bind.d.ts +26 -0
- package/build/src/library/contract-bind.js +106 -0
- package/build/src/library/contract-operations.d.ts +77 -0
- package/build/src/library/contract-operations.js +130 -0
- package/build/src/library/contract-types.d.ts +95 -0
- package/build/src/library/contract-types.js +1 -0
- package/build/src/library/contract.d.ts +19 -154
- package/build/src/library/contract.js +46 -261
- package/build/src/library/fleet.d.ts +1 -1
- package/build/src/library/fleet.js +19 -11
- package/build/src/library/input.d.ts +2 -0
- package/build/src/library/input.js +14 -0
- package/build/src/library/keiyaku.d.ts +8 -7
- package/build/src/library/keiyaku.js +3 -3
- package/build/src/library/nuke.js +14 -5
- package/build/src/library/reconcile.d.ts +9 -4
- package/build/src/library/reconcile.js +30 -24
- package/build/src/library/refusal.d.ts +13 -5
- package/build/src/library/refusal.js +7 -0
- package/build/src/library/region.d.ts +8 -7
- package/build/src/library/region.js +31 -11
- package/build/src/library/repo.js +2 -2
- package/build/src/markdown/lex.js +9 -2
- package/build/src/markdown/parse.js +11 -7
- package/build/src/markdown/query.js +4 -2
- package/build/src/protocol/amend.js +3 -1
- package/build/src/protocol/attempt.d.ts +7 -3
- package/build/src/protocol/attempt.js +9 -5
- package/build/src/protocol/audit.d.ts +1 -1
- package/build/src/protocol/audit.js +4 -14
- package/build/src/protocol/bind.d.ts +19 -6
- package/build/src/protocol/bind.js +28 -13
- package/build/src/protocol/completion.d.ts +0 -1
- package/build/src/protocol/completion.js +1 -3
- package/build/src/protocol/deliver.d.ts +1 -3
- package/build/src/protocol/deliver.js +15 -39
- package/build/src/protocol/intent.d.ts +1 -0
- package/build/src/protocol/intent.js +9 -7
- package/build/src/protocol/operations.d.ts +3 -7
- package/build/src/protocol/operations.js +5 -3
- package/build/src/protocol/placement.d.ts +0 -1
- package/build/src/protocol/placement.js +6 -6
- package/build/src/protocol/read/observation.d.ts +11 -0
- package/build/src/protocol/read/observation.js +37 -0
- package/build/src/protocol/read/status.d.ts +23 -12
- package/build/src/protocol/read/status.js +90 -72
- package/build/src/protocol/reconcile.d.ts +1 -0
- package/build/src/protocol/reconcile.js +2 -3
- package/build/src/protocol/reintegrate.js +1 -1
- package/build/src/protocol/review.d.ts +0 -1
- package/build/src/protocol/review.js +15 -22
- package/build/src/protocol/run.d.ts +1 -0
- package/build/src/protocol/run.js +4 -5
- package/build/src/runtime/proc/launch.d.ts +33 -0
- package/build/src/runtime/proc/launch.js +98 -0
- package/build/src/runtime/proc/line-rpc.js +9 -3
- package/build/src/runtime/proc/run.d.ts +12 -6
- package/build/src/runtime/proc/run.js +78 -21
- package/build/src/runtime/proc/stdio.js +7 -10
- package/build/src/settings.js +6 -2
- package/build/src/settlement/holder.js +13 -10
- package/build/src/settlement/settle.js +39 -6
- package/build/src/task/board.js +12 -6
- package/build/src/task/compose.js +85 -22
- package/build/src/task/context.js +21 -8
- package/build/src/task/document.js +46 -10
- package/build/src/task/identity.js +5 -2
- package/build/src/task/index.d.ts +0 -1
- package/build/src/task/index.js +119 -34
- package/build/src/task/input.js +29 -7
- package/build/src/task/mutation.js +61 -19
- package/build/src/task/operations.d.ts +6 -1
- package/build/src/task/operations.js +83 -29
- package/build/src/task/query.js +55 -26
- package/build/src/task/store.d.ts +3 -1
- package/build/src/task/store.js +81 -17
- package/build/src/verification/execution.js +14 -11
- package/build/src/workspace-place.js +19 -7
- package/build/src/world.js +16 -6
- package/package.json +8 -3
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { isKeiyakuOwnedRef, readRef, writeCommit, worktreeGitDirectory } from "./repository.js";
|
|
3
|
+
import { gitObjectIdForSnapshot, mintSnapshotId } from "./identity.js";
|
|
4
|
+
import { runDestroyHooks } from "./hooks.js";
|
|
5
|
+
import { terminalSealExpectations as decodeTerminalSealExpectations, terminalSealSnapshots, observeTerminalWorkspace, } from "./terminal-seal.js";
|
|
6
|
+
import { GitPlumbingError, runGit } from "./process.js";
|
|
7
|
+
import { worktreePath } from "./workspace.js";
|
|
8
|
+
const pathExists = (path) => access(path).then(() => true, () => false);
|
|
9
|
+
async function removeTerminalWorktree(repository, topology, path) {
|
|
10
|
+
const registered = topology.paths.has(path);
|
|
11
|
+
if (!registered) {
|
|
12
|
+
return {
|
|
13
|
+
effect: { kind: "worktree", path, action: "unchanged" },
|
|
14
|
+
retained: await pathExists(path),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (!(await pathExists(path))) {
|
|
18
|
+
await runGit(repository, ["worktree", "remove", path]);
|
|
19
|
+
topology.paths.delete(path);
|
|
20
|
+
return { effect: { kind: "worktree", path, action: "removed" }, retained: false };
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
await runGit(repository, ["worktree", "remove", "--force", path]);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { effect: { kind: "worktree", path, action: "unchanged" }, retained: true };
|
|
27
|
+
}
|
|
28
|
+
topology.paths.delete(path);
|
|
29
|
+
return { effect: { kind: "worktree", path, action: "removed" }, retained: false };
|
|
30
|
+
}
|
|
31
|
+
export async function updateRef(repository, ref, desired) {
|
|
32
|
+
const before = await readRef(repository, ref);
|
|
33
|
+
if (before === desired)
|
|
34
|
+
return { kind: "ref", name: ref, action: "unchanged", before, after: desired };
|
|
35
|
+
await runGit(repository, [
|
|
36
|
+
"update-ref",
|
|
37
|
+
"--no-deref",
|
|
38
|
+
ref,
|
|
39
|
+
gitObjectIdForSnapshot(desired),
|
|
40
|
+
before ?? "0".repeat(desired.length),
|
|
41
|
+
]);
|
|
42
|
+
return { kind: "ref", name: ref, action: before === null ? "created" : "updated", before, after: desired };
|
|
43
|
+
}
|
|
44
|
+
export async function removeRef(repository, ref) {
|
|
45
|
+
const before = await readRef(repository, ref);
|
|
46
|
+
if (before === null)
|
|
47
|
+
return { kind: "ref", name: ref, action: "unchanged", before: null, after: null };
|
|
48
|
+
await runGit(repository, ["update-ref", "--no-deref", "-d", ref, before]);
|
|
49
|
+
return { kind: "ref", name: ref, action: "removed", before, after: null };
|
|
50
|
+
}
|
|
51
|
+
async function removeRefWithCustody(repository, ref, custodian, expectedCustodian) {
|
|
52
|
+
const before = await readRef(repository, ref);
|
|
53
|
+
if (before === null)
|
|
54
|
+
return { kind: "ref", name: ref, action: "unchanged", before: null, after: null };
|
|
55
|
+
if ((await readRef(repository, custodian)) !== expectedCustodian) {
|
|
56
|
+
return { kind: "ref", name: ref, action: "unchanged", before, after: before };
|
|
57
|
+
}
|
|
58
|
+
await runGit(repository, ["update-ref", "--stdin", "--no-deref"], [
|
|
59
|
+
"start",
|
|
60
|
+
`verify ${custodian} ${gitObjectIdForSnapshot(expectedCustodian)}`,
|
|
61
|
+
`delete ${ref} ${before}`,
|
|
62
|
+
"prepare",
|
|
63
|
+
"commit",
|
|
64
|
+
"",
|
|
65
|
+
].join("\n"));
|
|
66
|
+
return { kind: "ref", name: ref, action: "removed", before, after: null };
|
|
67
|
+
}
|
|
68
|
+
function retainedRefEffect(ref, snapshot) {
|
|
69
|
+
return { kind: "ref", name: ref, action: "unchanged", before: snapshot, after: snapshot };
|
|
70
|
+
}
|
|
71
|
+
async function refRows(repository) {
|
|
72
|
+
const output = (await runGit(repository, ["for-each-ref", "--format=%(refname)%00%(objectname)"])).toString("utf8");
|
|
73
|
+
if (output.length === 0)
|
|
74
|
+
return [];
|
|
75
|
+
return output
|
|
76
|
+
.trimEnd()
|
|
77
|
+
.split("\n")
|
|
78
|
+
.map((row) => {
|
|
79
|
+
const [ref, oid, extra] = row.split("\0");
|
|
80
|
+
if (ref === undefined || oid === undefined || extra !== undefined)
|
|
81
|
+
throw new Error("Git ref row is malformed");
|
|
82
|
+
return { ref, oid };
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async function commitIsAncestor(repository, ancestor, descendant) {
|
|
86
|
+
try {
|
|
87
|
+
await runGit(repository, ["merge-base", "--is-ancestor", gitObjectIdForSnapshot(ancestor), descendant]);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error instanceof GitPlumbingError && error.status === 1)
|
|
92
|
+
return false;
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function snapshotCustodian(repository, snapshot) {
|
|
97
|
+
for (const row of await refRows(repository)) {
|
|
98
|
+
if (isKeiyakuOwnedRef(row.ref))
|
|
99
|
+
continue;
|
|
100
|
+
if (await commitIsAncestor(repository, snapshot, row.oid))
|
|
101
|
+
return { ref: row.ref, oid: mintSnapshotId(row.oid) };
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
function fromPrimaryWorktree(repository) {
|
|
106
|
+
return { ...repository, effectiveCwd: repository.primaryWorktree };
|
|
107
|
+
}
|
|
108
|
+
function complete(effects = [], lag = []) {
|
|
109
|
+
return { effects, lag };
|
|
110
|
+
}
|
|
111
|
+
function retainTerminalWorktree(path, retainedLag, { effects, lag }) {
|
|
112
|
+
effects.push({ kind: "worktree", path, action: "unchanged" });
|
|
113
|
+
lag.push(retainedLag);
|
|
114
|
+
return complete(effects, lag);
|
|
115
|
+
}
|
|
116
|
+
async function terminalSealExpectations(channel, state) {
|
|
117
|
+
const snapshots = terminalSealSnapshots(state);
|
|
118
|
+
const objects = await channel.readObjects(snapshots.map(gitObjectIdForSnapshot));
|
|
119
|
+
return decodeTerminalSealExpectations(state, objects);
|
|
120
|
+
}
|
|
121
|
+
async function removeSealedTerminalWorktree({ repository, topology, path, state, expected, hooks, retryHooks, acc, }) {
|
|
122
|
+
if (topology.paths.has(path) && (await pathExists(path))) {
|
|
123
|
+
const terminal = state.terminal;
|
|
124
|
+
if (terminal === null)
|
|
125
|
+
throw new Error(`terminal worktree cleanup received active Contract ${state.id}`);
|
|
126
|
+
const canRecover = terminal.kind === "abandoned";
|
|
127
|
+
let recovery = null;
|
|
128
|
+
const recordRecovery = async (workspace) => {
|
|
129
|
+
const snapshot = mintSnapshotId(await writeCommit({
|
|
130
|
+
repository,
|
|
131
|
+
tree: workspace.tree,
|
|
132
|
+
parent: gitObjectIdForSnapshot(recovery?.snapshot ?? workspace.head),
|
|
133
|
+
message: `${state.id}: ephemeral abandoned-workspace recovery\n\nKeiyaku-Contract: ${state.id}`,
|
|
134
|
+
actor: "Keiyaku Recovery",
|
|
135
|
+
at: terminal.at,
|
|
136
|
+
}));
|
|
137
|
+
const effect = {
|
|
138
|
+
kind: "recovery-snapshot",
|
|
139
|
+
action: "created",
|
|
140
|
+
snapshot,
|
|
141
|
+
retention: "ephemeral",
|
|
142
|
+
};
|
|
143
|
+
const existing = acc.effects.findIndex((item) => item.kind === "recovery-snapshot");
|
|
144
|
+
if (existing < 0)
|
|
145
|
+
acc.effects.push(effect);
|
|
146
|
+
else
|
|
147
|
+
acc.effects[existing] = effect;
|
|
148
|
+
return { snapshot, workspace };
|
|
149
|
+
};
|
|
150
|
+
const before = await observeTerminalWorkspace(repository, path, expected);
|
|
151
|
+
const beforeWorkspace = before.workspace;
|
|
152
|
+
const beforeHooks = before.unsealed;
|
|
153
|
+
if (beforeHooks !== null) {
|
|
154
|
+
if (!canRecover || beforeWorkspace.submodules.length > 0) {
|
|
155
|
+
return retainTerminalWorktree(path, beforeHooks, acc);
|
|
156
|
+
}
|
|
157
|
+
recovery = await recordRecovery(beforeWorkspace);
|
|
158
|
+
}
|
|
159
|
+
const hookLag = await runDestroyHooks(path, await worktreeGitDirectory(repository, path), hooks, retryHooks);
|
|
160
|
+
if (hookLag !== null)
|
|
161
|
+
return retainTerminalWorktree(path, hookLag, acc);
|
|
162
|
+
const after = await observeTerminalWorkspace(repository, path, expected);
|
|
163
|
+
const afterWorkspace = after.workspace;
|
|
164
|
+
const afterHooks = after.unsealed;
|
|
165
|
+
if (afterHooks !== null) {
|
|
166
|
+
if (!canRecover || afterWorkspace.submodules.length > 0) {
|
|
167
|
+
return retainTerminalWorktree(path, afterHooks, acc);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (canRecover &&
|
|
171
|
+
(recovery === null ||
|
|
172
|
+
recovery.workspace.head !== afterWorkspace.head ||
|
|
173
|
+
recovery.workspace.tree !== afterWorkspace.tree) &&
|
|
174
|
+
(recovery !== null ||
|
|
175
|
+
beforeWorkspace.head !== afterWorkspace.head ||
|
|
176
|
+
beforeWorkspace.tree !== afterWorkspace.tree)) {
|
|
177
|
+
recovery = await recordRecovery(afterWorkspace);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const removal = await removeTerminalWorktree(repository, topology, path);
|
|
181
|
+
if (removal.retained)
|
|
182
|
+
return retainTerminalWorktree(path, { kind: "worktree-retained", path }, acc);
|
|
183
|
+
acc.effects.push(removal.effect);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
async function targetCustodyForClaimedIntegration(repository, state, integration) {
|
|
187
|
+
if (state.terminal?.kind !== "claimed" || state.coordinates.target === undefined)
|
|
188
|
+
return null;
|
|
189
|
+
const target = await readRef(repository, state.coordinates.target);
|
|
190
|
+
if (target === null)
|
|
191
|
+
return null;
|
|
192
|
+
const oid = mintSnapshotId(target);
|
|
193
|
+
return (await commitIsAncestor(repository, integration, oid)) ? { ref: state.coordinates.target, oid } : null;
|
|
194
|
+
}
|
|
195
|
+
function sealedTree(expected, snapshot) {
|
|
196
|
+
const tree = expected.treeBySnapshot.get(snapshot);
|
|
197
|
+
if (tree === undefined)
|
|
198
|
+
throw new Error(`terminal seal tree was not resolved: ${snapshot}`);
|
|
199
|
+
return tree;
|
|
200
|
+
}
|
|
201
|
+
async function releaseTerminalCustody({ repository, state, resolveSeal, ref, pin, acc: { effects }, }) {
|
|
202
|
+
const [deliveryRef, candidatePin] = await Promise.all([readRef(repository, ref), readRef(repository, pin)]);
|
|
203
|
+
if (deliveryRef === null && candidatePin === null)
|
|
204
|
+
return;
|
|
205
|
+
if (state.delivery === null) {
|
|
206
|
+
const custodian = await snapshotCustodian(repository, state.coordinates.start);
|
|
207
|
+
if (deliveryRef !== null) {
|
|
208
|
+
effects.push(custodian === null
|
|
209
|
+
? retainedRefEffect(ref, deliveryRef)
|
|
210
|
+
: await removeRefWithCustody(repository, ref, custodian.ref, custodian.oid));
|
|
211
|
+
}
|
|
212
|
+
if (candidatePin !== null)
|
|
213
|
+
effects.push(await removeRef(repository, pin));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const tender = state.delivery.data.tenderSnapshot;
|
|
217
|
+
const integration = state.currentIntegration?.snapshot ?? state.delivery.data.integration.snapshot;
|
|
218
|
+
const target = await targetCustodyForClaimedIntegration(repository, state, integration);
|
|
219
|
+
if (deliveryRef !== null && target !== null) {
|
|
220
|
+
if (tender === integration) {
|
|
221
|
+
effects.push(await removeRefWithCustody(repository, ref, target.ref, target.oid));
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const expected = await resolveSeal();
|
|
225
|
+
if (sealedTree(expected, tender) === sealedTree(expected, integration)) {
|
|
226
|
+
effects.push(await removeRefWithCustody(repository, ref, target.ref, target.oid));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
effects.push(retainedRefEffect(ref, deliveryRef));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else if (deliveryRef !== null) {
|
|
234
|
+
effects.push(retainedRefEffect(ref, deliveryRef));
|
|
235
|
+
}
|
|
236
|
+
if (candidatePin !== null) {
|
|
237
|
+
if (target !== null || (tender === integration && deliveryRef !== null)) {
|
|
238
|
+
effects.push(target !== null
|
|
239
|
+
? await removeRefWithCustody(repository, pin, target.ref, target.oid)
|
|
240
|
+
: await removeRefWithCustody(repository, pin, ref, tender));
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
effects.push(retainedRefEffect(pin, candidatePin));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export async function reconcileTerminalManagedWorktree({ repository, channel, hooks, retryHooks, retainTerminalWorktree, place }, state, topology, acc, { ref, pin }) {
|
|
248
|
+
const primary = fromPrimaryWorktree(repository);
|
|
249
|
+
const path = place === undefined ? undefined : worktreePath(repository, place);
|
|
250
|
+
const resolveSeal = async () => await terminalSealExpectations(channel, state);
|
|
251
|
+
if (path === undefined) {
|
|
252
|
+
if (retainTerminalWorktree === true)
|
|
253
|
+
return complete(acc.effects, acc.lag);
|
|
254
|
+
await releaseTerminalCustody({ repository: primary, state, resolveSeal, ref, pin, acc });
|
|
255
|
+
return complete(acc.effects, acc.lag);
|
|
256
|
+
}
|
|
257
|
+
const expected = await resolveSeal();
|
|
258
|
+
acc.effects.push(await updateRef(primary, ref, state.delivery?.data.tenderSnapshot ?? state.coordinates.start));
|
|
259
|
+
if (state.delivery !== null) {
|
|
260
|
+
acc.effects.push(await updateRef(primary, pin, state.currentIntegration?.snapshot ?? state.delivery.data.integration.snapshot));
|
|
261
|
+
}
|
|
262
|
+
if (retainTerminalWorktree === true)
|
|
263
|
+
return complete(acc.effects, acc.lag);
|
|
264
|
+
const retained = await removeSealedTerminalWorktree({
|
|
265
|
+
repository: primary,
|
|
266
|
+
topology,
|
|
267
|
+
path,
|
|
268
|
+
state,
|
|
269
|
+
expected,
|
|
270
|
+
hooks,
|
|
271
|
+
retryHooks,
|
|
272
|
+
acc,
|
|
273
|
+
});
|
|
274
|
+
if (retained !== null)
|
|
275
|
+
return retained;
|
|
276
|
+
await releaseTerminalCustody({ repository: primary, state, resolveSeal: async () => expected, ref, pin, acc });
|
|
277
|
+
return complete(acc.effects, acc.lag);
|
|
278
|
+
}
|
|
@@ -2,13 +2,17 @@ import { gitObjectId, gitObjectIdForSnapshot, mintSnapshotId } from "./identity.
|
|
|
2
2
|
import { runGit } from "./process.js";
|
|
3
3
|
import { captureWorkspaceTree } from "./workspace.js";
|
|
4
4
|
export function terminalSealSnapshots(state) {
|
|
5
|
-
return [
|
|
5
|
+
return [
|
|
6
|
+
...new Set([
|
|
6
7
|
state.coordinates.start,
|
|
7
|
-
...(state.delivery === null
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
...(state.delivery === null
|
|
9
|
+
? []
|
|
10
|
+
: [
|
|
11
|
+
state.delivery.data.tenderSnapshot,
|
|
12
|
+
...(state.currentIntegration === null ? [] : [state.currentIntegration.snapshot]),
|
|
13
|
+
]),
|
|
14
|
+
]),
|
|
15
|
+
];
|
|
12
16
|
}
|
|
13
17
|
function commitMetadata(snapshot, result) {
|
|
14
18
|
if (result === undefined || result.kind === "missing") {
|
|
@@ -33,10 +37,7 @@ function commitMetadata(snapshot, result) {
|
|
|
33
37
|
}
|
|
34
38
|
export function terminalSealExpectations(state, objects) {
|
|
35
39
|
const snapshots = terminalSealSnapshots(state);
|
|
36
|
-
const metadata = new Map(snapshots.map((snapshot) => [
|
|
37
|
-
snapshot,
|
|
38
|
-
commitMetadata(snapshot, objects.get(gitObjectIdForSnapshot(snapshot))),
|
|
39
|
-
]));
|
|
40
|
+
const metadata = new Map(snapshots.map((snapshot) => [snapshot, commitMetadata(snapshot, objects.get(gitObjectIdForSnapshot(snapshot)))]));
|
|
40
41
|
const treeBySnapshot = new Map([...metadata].map(([snapshot, commit]) => [snapshot, commit.tree]));
|
|
41
42
|
if (state.delivery === null) {
|
|
42
43
|
return { heads: [state.coordinates.start], trees: [...treeBySnapshot.values()], treeBySnapshot };
|
|
@@ -47,12 +48,14 @@ export function terminalSealExpectations(state, objects) {
|
|
|
47
48
|
throw new Error(`terminal seal metadata was not resolved: ${tender}`);
|
|
48
49
|
const [tenderParent] = tenderMetadata.parents;
|
|
49
50
|
return {
|
|
50
|
-
heads: [
|
|
51
|
+
heads: [
|
|
52
|
+
...new Set([
|
|
51
53
|
state.coordinates.start,
|
|
52
54
|
...(tenderParent === undefined ? [] : [tenderParent]),
|
|
53
55
|
tender,
|
|
54
56
|
state.currentIntegration?.snapshot ?? state.delivery.data.integration.snapshot,
|
|
55
|
-
])
|
|
57
|
+
]),
|
|
58
|
+
],
|
|
56
59
|
trees: [...treeBySnapshot.values()],
|
|
57
60
|
treeBySnapshot,
|
|
58
61
|
};
|
package/build/src/git/tree.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { gitObjectId } from "./identity.js";
|
|
3
3
|
export function validPath(path) {
|
|
4
|
-
if (path.length === 0 ||
|
|
5
|
-
|
|
4
|
+
if (path.length === 0 ||
|
|
5
|
+
path.startsWith("/") ||
|
|
6
|
+
path.endsWith("/") ||
|
|
7
|
+
path.includes("\0") ||
|
|
8
|
+
path.split("/").some((part) => part.length === 0 || part === "." || part === ".."))
|
|
6
9
|
throw new Error(`invalid Git path: ${path}`);
|
|
7
10
|
}
|
|
8
11
|
function addEntry(root, path, change) {
|
|
@@ -15,9 +15,28 @@ export type WorkspaceTree = Readonly<{
|
|
|
15
15
|
changes: WorkspaceChanges;
|
|
16
16
|
}>;
|
|
17
17
|
export declare function worktreePath(repository: GitRepository, place: string): string;
|
|
18
|
+
export type ManagedWorktreeFollow = Readonly<{
|
|
19
|
+
kind: "followed";
|
|
20
|
+
before: SnapshotId;
|
|
21
|
+
after: SnapshotId;
|
|
22
|
+
}> | Readonly<{
|
|
23
|
+
kind: "unchanged";
|
|
24
|
+
}> | Readonly<{
|
|
25
|
+
kind: "retained";
|
|
26
|
+
head: SnapshotId;
|
|
27
|
+
reason: "head-moved" | "head-attached" | "operation-in-progress" | "unsupported-parent-shape";
|
|
28
|
+
}>;
|
|
29
|
+
/** Follow an accepted tender only through the native index-and-HEAD transition. */
|
|
30
|
+
export declare function followManagedWorktree(repository: GitRepository, workspace: string, tender: SnapshotId): Promise<ManagedWorktreeFollow>;
|
|
18
31
|
export declare function withPrivateGitIndex<Value>(action: (environment: Readonly<{
|
|
19
32
|
GIT_INDEX_FILE: string;
|
|
20
33
|
}>) => Value | PromiseLike<Value>): Promise<Value>;
|
|
34
|
+
/** Observe MERGE_HEAD in an appointed workspace, or undefined when absent. */
|
|
35
|
+
export declare function workspaceMergeHead(repository: GitRepository, workspace: string): Promise<SnapshotId | undefined>;
|
|
36
|
+
/** True when the appointed workspace already has Git MERGE_HEAD. */
|
|
37
|
+
export declare function workspaceMergeStatePresent(repository: GitRepository, workspace: string): Promise<boolean>;
|
|
38
|
+
/** Sorted unique unmerged index paths from porcelain status. */
|
|
39
|
+
export declare function unmergedWorkspacePaths(repository: GitRepository, workspace: string): Promise<readonly string[]>;
|
|
21
40
|
export type WorkspaceChangeCounts = Readonly<{
|
|
22
41
|
staged: number;
|
|
23
42
|
unstaged: number;
|
|
@@ -27,13 +46,16 @@ export type WorkspaceChangeCounts = Readonly<{
|
|
|
27
46
|
export type ContractWorkspaceLocation = Readonly<{
|
|
28
47
|
kind: "worktree";
|
|
29
48
|
path: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
49
|
+
}>;
|
|
50
|
+
export type ContractWorkspaceMerge = Readonly<{
|
|
51
|
+
head: SnapshotId;
|
|
52
|
+
unmergedPaths: readonly string[];
|
|
32
53
|
}>;
|
|
33
54
|
export type ContractWorkspaceObservation = Readonly<{
|
|
34
55
|
kind: "clean" | "dirty";
|
|
35
56
|
location: ContractWorkspaceLocation;
|
|
36
57
|
counts: WorkspaceChangeCounts;
|
|
58
|
+
merge: ContractWorkspaceMerge | null;
|
|
37
59
|
}> | Readonly<{
|
|
38
60
|
kind: "unavailable";
|
|
39
61
|
location: ContractWorkspaceLocation;
|
|
@@ -1,11 +1,74 @@
|
|
|
1
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
1
|
+
import { access, mkdtemp, rm } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { gitObjectId, mintSnapshotId } from "./identity.js";
|
|
5
|
-
import { runGit, runGitWithEnvironment } from "./process.js";
|
|
6
|
-
|
|
5
|
+
import { GitPlumbingError, runGit, runGitWithEnvironment } from "./process.js";
|
|
6
|
+
import { worktreeGitDirectory } from "./repository.js";
|
|
7
|
+
const WORKTREE_DIRECTORY = [".keiyaku", "wt"];
|
|
7
8
|
export function worktreePath(repository, place) {
|
|
8
|
-
return resolve(repository.
|
|
9
|
+
return resolve(repository.primaryWorktree, ...WORKTREE_DIRECTORY, place);
|
|
10
|
+
}
|
|
11
|
+
async function workspaceRevision(repository, workspace, revision) {
|
|
12
|
+
try {
|
|
13
|
+
const value = (await runGit(repository, ["-C", workspace, "rev-parse", "--verify", "--quiet", revision]))
|
|
14
|
+
.toString("utf8")
|
|
15
|
+
.trim();
|
|
16
|
+
return value.length === 0 ? null : mintSnapshotId(value);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error instanceof GitPlumbingError && error.status === 1)
|
|
20
|
+
return null;
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function workspaceOperationState(repository, workspace, gitDirectory) {
|
|
25
|
+
const paths = ["rebase-merge", "rebase-apply", "CHERRY_PICK_HEAD", "REVERT_HEAD"];
|
|
26
|
+
const other = await Promise.all(paths.map(async (path) => {
|
|
27
|
+
try {
|
|
28
|
+
await access(join(gitDirectory, path));
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}));
|
|
35
|
+
const unmerged = (await runGit(repository, ["-C", workspace, "ls-files", "--unmerged", "-z"])).length > 0;
|
|
36
|
+
return { other: other.some(Boolean), unmerged };
|
|
37
|
+
}
|
|
38
|
+
/** Follow an accepted tender only through the native index-and-HEAD transition. */
|
|
39
|
+
export async function followManagedWorktree(repository, workspace, tender) {
|
|
40
|
+
const head = await workspaceRevision(repository, workspace, "HEAD");
|
|
41
|
+
if (head === null)
|
|
42
|
+
throw new Error("managed worktree HEAD is missing");
|
|
43
|
+
const attached = await runGit(repository, ["-C", workspace, "symbolic-ref", "--quiet", "HEAD"]).then(() => true, (error) => {
|
|
44
|
+
if (error instanceof GitPlumbingError && error.status === 1)
|
|
45
|
+
return false;
|
|
46
|
+
throw error;
|
|
47
|
+
});
|
|
48
|
+
if (attached)
|
|
49
|
+
return { kind: "retained", head, reason: "head-attached" };
|
|
50
|
+
if (head === tender)
|
|
51
|
+
return { kind: "unchanged" };
|
|
52
|
+
const parents = (await runGit(repository, ["show", "-s", "--format=%P", gitObjectId(tender)]))
|
|
53
|
+
.toString("utf8")
|
|
54
|
+
.trim()
|
|
55
|
+
.split(" ")
|
|
56
|
+
.filter((parent) => parent.length > 0)
|
|
57
|
+
.map((parent) => mintSnapshotId(parent));
|
|
58
|
+
if (parents.length > 0 && head !== parents[0])
|
|
59
|
+
return { kind: "retained", head, reason: "head-moved" };
|
|
60
|
+
if (parents.length !== 1 && parents.length !== 2)
|
|
61
|
+
return { kind: "retained", head, reason: "unsupported-parent-shape" };
|
|
62
|
+
const gitDirectory = await worktreeGitDirectory(repository, workspace);
|
|
63
|
+
const mergeHead = await workspaceRevision(repository, workspace, "MERGE_HEAD");
|
|
64
|
+
const operation = await workspaceOperationState(repository, workspace, gitDirectory);
|
|
65
|
+
const admitted = parents.length === 1
|
|
66
|
+
? mergeHead === null && !operation.other && !operation.unmerged
|
|
67
|
+
: mergeHead === parents[1] && !operation.other && !operation.unmerged;
|
|
68
|
+
if (!admitted)
|
|
69
|
+
return { kind: "retained", head, reason: "operation-in-progress" };
|
|
70
|
+
await runGit(repository, ["-C", workspace, "reset", "--mixed", gitObjectId(tender)]);
|
|
71
|
+
return { kind: "followed", before: head, after: tender };
|
|
9
72
|
}
|
|
10
73
|
export async function withPrivateGitIndex(action) {
|
|
11
74
|
const directory = await mkdtemp(join(tmpdir(), "keiyaku-v4-index-"));
|
|
@@ -29,15 +92,23 @@ function fields(record, count) {
|
|
|
29
92
|
result.push(record.slice(start));
|
|
30
93
|
return result;
|
|
31
94
|
}
|
|
32
|
-
async function
|
|
95
|
+
async function workspaceStatus(repository, workspace) {
|
|
33
96
|
const records = (await runGit(repository, [
|
|
34
|
-
"-C",
|
|
35
|
-
|
|
36
|
-
|
|
97
|
+
"-C",
|
|
98
|
+
workspace,
|
|
99
|
+
"status",
|
|
100
|
+
"--porcelain=v2",
|
|
101
|
+
"-z",
|
|
102
|
+
"--untracked-files=all",
|
|
103
|
+
"--ignore-submodules=none",
|
|
104
|
+
]))
|
|
105
|
+
.toString("utf8")
|
|
106
|
+
.split("\0");
|
|
37
107
|
const staged = new Set();
|
|
38
108
|
const unstaged = new Set();
|
|
39
109
|
const untracked = new Set();
|
|
40
110
|
const submodules = new Set();
|
|
111
|
+
const unmerged = new Set();
|
|
41
112
|
for (let index = 0; index < records.length - 1; index += 1) {
|
|
42
113
|
const record = records[index];
|
|
43
114
|
const kind = record[0];
|
|
@@ -50,6 +121,8 @@ async function workspaceChanges(repository, workspace) {
|
|
|
50
121
|
const xy = parts[1];
|
|
51
122
|
const sub = parts[2];
|
|
52
123
|
const path = parts.at(-1);
|
|
124
|
+
if (kind === "u")
|
|
125
|
+
unmerged.add(path);
|
|
53
126
|
if (xy[0] !== ".")
|
|
54
127
|
staged.add(path);
|
|
55
128
|
if (xy[1] !== ".")
|
|
@@ -75,8 +148,34 @@ async function workspaceChanges(repository, workspace) {
|
|
|
75
148
|
unstaged: [...unstaged].sort(),
|
|
76
149
|
untracked: [...untracked].sort(),
|
|
77
150
|
submodules: [...submodules].sort(),
|
|
151
|
+
unmergedPaths: [...unmerged].sort(),
|
|
78
152
|
};
|
|
79
153
|
}
|
|
154
|
+
async function workspaceChanges(repository, workspace) {
|
|
155
|
+
const { unmergedPaths: _unmergedPaths, ...changes } = await workspaceStatus(repository, workspace);
|
|
156
|
+
return changes;
|
|
157
|
+
}
|
|
158
|
+
/** Observe MERGE_HEAD in an appointed workspace, or undefined when absent. */
|
|
159
|
+
export async function workspaceMergeHead(repository, workspace) {
|
|
160
|
+
try {
|
|
161
|
+
return mintSnapshotId((await runGit(repository, ["-C", workspace, "rev-parse", "-q", "--verify", "MERGE_HEAD"]))
|
|
162
|
+
.toString("utf8")
|
|
163
|
+
.trim());
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (error instanceof GitPlumbingError && error.status === 1)
|
|
167
|
+
return undefined;
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** True when the appointed workspace already has Git MERGE_HEAD. */
|
|
172
|
+
export async function workspaceMergeStatePresent(repository, workspace) {
|
|
173
|
+
return (await workspaceMergeHead(repository, workspace)) !== undefined;
|
|
174
|
+
}
|
|
175
|
+
/** Sorted unique unmerged index paths from porcelain status. */
|
|
176
|
+
export async function unmergedWorkspacePaths(repository, workspace) {
|
|
177
|
+
return (await workspaceStatus(repository, workspace)).unmergedPaths;
|
|
178
|
+
}
|
|
80
179
|
function countsOf(changes) {
|
|
81
180
|
return {
|
|
82
181
|
staged: changes.staged.length,
|
|
@@ -87,9 +186,12 @@ function countsOf(changes) {
|
|
|
87
186
|
}
|
|
88
187
|
export async function observeWorkspace(repository, location, workspace) {
|
|
89
188
|
try {
|
|
90
|
-
const
|
|
189
|
+
const status = await workspaceStatus(repository, workspace);
|
|
190
|
+
const counts = countsOf(status);
|
|
91
191
|
const dirty = counts.staged > 0 || counts.unstaged > 0 || counts.untracked > 0 || counts.submodules > 0;
|
|
92
|
-
|
|
192
|
+
const head = await workspaceMergeHead(repository, workspace);
|
|
193
|
+
const merge = head === undefined ? null : { head, unmergedPaths: status.unmergedPaths };
|
|
194
|
+
return { kind: dirty ? "dirty" : "clean", location, counts, merge };
|
|
93
195
|
}
|
|
94
196
|
catch {
|
|
95
197
|
return { kind: "unavailable", location };
|
|
@@ -117,11 +219,11 @@ export async function captureWorkspaceTree(repository, workspace) {
|
|
|
117
219
|
const identities = (await runGit(repository, ["-C", workspace, "show", "-s", "--format=%H%n%T%n%cI", "HEAD"]))
|
|
118
220
|
.toString("utf8")
|
|
119
221
|
.split("\n");
|
|
120
|
-
if (identities.length !== 4
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
222
|
+
if (identities.length !== 4 ||
|
|
223
|
+
identities[0] === undefined ||
|
|
224
|
+
identities[1] === undefined ||
|
|
225
|
+
identities[2] === undefined ||
|
|
226
|
+
identities[3] !== "") {
|
|
125
227
|
throw new Error("workspace HEAD/tree/time resolution returned an unexpected shape");
|
|
126
228
|
}
|
|
127
229
|
const head = mintSnapshotId(identities[0]);
|
|
@@ -137,7 +239,9 @@ export async function captureWorkspaceTree(repository, workspace) {
|
|
|
137
239
|
await runGitWithEnvironment(repository, ["-C", workspace, "read-tree", "HEAD"], undefined, environment);
|
|
138
240
|
await runGitWithEnvironment(repository, ["-C", workspace, "add", "--all"], undefined, environment);
|
|
139
241
|
return {
|
|
140
|
-
tree: gitObjectId((await runGitWithEnvironment(repository, ["-C", workspace, "write-tree"], undefined, environment))
|
|
242
|
+
tree: gitObjectId((await runGitWithEnvironment(repository, ["-C", workspace, "write-tree"], undefined, environment))
|
|
243
|
+
.toString("utf8")
|
|
244
|
+
.trim(), "workspace tree"),
|
|
141
245
|
head,
|
|
142
246
|
at,
|
|
143
247
|
dirty,
|
|
@@ -10,9 +10,12 @@ export function parseAkumaGlob(value) {
|
|
|
10
10
|
const segments = value.split("/");
|
|
11
11
|
const archetype = segments[1] ?? "";
|
|
12
12
|
const archetypeProbe = archetype.replaceAll("*", "x");
|
|
13
|
-
if (segments.length !== 3 ||
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
if (segments.length !== 3 ||
|
|
14
|
+
segments[0] !== "aku" ||
|
|
15
|
+
!value.includes("*") ||
|
|
16
|
+
archetype.length === 0 ||
|
|
17
|
+
normalizeIdentityStem({ source: archetypeProbe }) !== archetypeProbe ||
|
|
18
|
+
!AKUMA_GLOB_SUFFIX.test(segments[2] ?? "")) {
|
|
16
19
|
throw new TypeError("Akuma glob must be aku/<akuma-pattern>/<hex-pattern> and contain *");
|
|
17
20
|
}
|
|
18
21
|
return value;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AkumaKanshiRow } from "./report.js";
|
|
2
|
+
export declare const FLEET_VISIBLE_ROWS = 10;
|
|
3
|
+
export declare const FLEET_SNAPSHOT_ROWS = 3;
|
|
4
|
+
export declare function fleetUpdatedAt(row: AkumaKanshiRow): string | null;
|
|
5
|
+
/** The Fleet aperture and snapshot readers share this one visible-row order. */
|
|
6
|
+
export declare function visibleFleetRows(rows: readonly AkumaKanshiRow[]): readonly AkumaKanshiRow[];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const FLEET_VISIBLE_ROWS = 10;
|
|
2
|
+
export const FLEET_SNAPSHOT_ROWS = 3;
|
|
3
|
+
export function fleetUpdatedAt(row) {
|
|
4
|
+
const lifeAt = "lifeAt" in row ? row.lifeAt : null;
|
|
5
|
+
const lastActivityAt = "lastActivityAt" in row ? row.lastActivityAt : null;
|
|
6
|
+
if (lifeAt === null)
|
|
7
|
+
return lastActivityAt;
|
|
8
|
+
if (lastActivityAt === null)
|
|
9
|
+
return lifeAt;
|
|
10
|
+
return lifeAt > lastActivityAt ? lifeAt : lastActivityAt;
|
|
11
|
+
}
|
|
12
|
+
/** The Fleet aperture and snapshot readers share this one visible-row order. */
|
|
13
|
+
export function visibleFleetRows(rows) {
|
|
14
|
+
return [...rows]
|
|
15
|
+
.sort((left, right) => {
|
|
16
|
+
const leftAt = fleetUpdatedAt(left);
|
|
17
|
+
const rightAt = fleetUpdatedAt(right);
|
|
18
|
+
if (leftAt === rightAt)
|
|
19
|
+
return 0;
|
|
20
|
+
if (leftAt === null)
|
|
21
|
+
return 1;
|
|
22
|
+
if (rightAt === null)
|
|
23
|
+
return -1;
|
|
24
|
+
return leftAt > rightAt ? -1 : 1;
|
|
25
|
+
})
|
|
26
|
+
.slice(0, FLEET_VISIBLE_ROWS);
|
|
27
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { kanshi, observeKanshi } from "./read.js";
|
|
2
2
|
export { selectKanshi, selectRegion } from "./select.js";
|
|
3
3
|
export type { KanshiInput } from "./read.js";
|
|
4
|
-
export type { AkumaKanshiRow, AkumaKanshiWorld, ContractEndpointObservation, ContractHolderObservation, ContractKanshiBoard, ContractKanshiRow, KanshiReport, Section, TaskKanshiRow, TaskKanshiWorld, KanshiRegionSelection, RegionDeclaration,
|
|
4
|
+
export type { AkumaKanshiRow, AkumaKanshiWorld, ContractEndpointObservation, ContractHolderObservation, ContractKanshiBoard, ContractKanshiRow, CurrentPhysicalIssue, KanshiReport, Section, TaskKanshiRow, TaskKanshiWorld, KanshiRegionSelection, RegionDeclaration, RegionOverlap, RegionRead, } from "./report.js";
|
|
5
5
|
export type { KanshiSelection } from "./select.js";
|