@astrosheep/keiyaku 4.0.0 → 4.0.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.
Files changed (308) hide show
  1. package/README.md +133 -8
  2. package/build/integrations/marketplace/plugins/keiyaku/.claude-plugin/plugin.json +2 -2
  3. package/build/integrations/marketplace/plugins/keiyaku/.codex-plugin/plugin.json +4 -4
  4. package/build/integrations/marketplace/plugins/keiyaku/opencode.js +2 -2
  5. package/build/integrations/marketplace/plugins/keiyaku/package.json +2 -2
  6. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +13 -5
  7. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +91 -32
  8. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +134 -0
  9. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/agents/openai.yaml +4 -0
  10. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-task/SKILL.md +41 -6
  11. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +262 -46
  12. package/build/src/akuma/abort.d.ts +2 -0
  13. package/build/src/akuma/abort.js +50 -0
  14. package/build/src/akuma/akuma.d.ts +50 -33
  15. package/build/src/akuma/akuma.js +175 -127
  16. package/build/src/akuma/archetype.d.ts +14 -5
  17. package/build/src/akuma/archetype.js +85 -39
  18. package/build/src/akuma/body.d.ts +3 -5
  19. package/build/src/akuma/body.js +490 -183
  20. package/build/src/akuma/coordinate.d.ts +9 -0
  21. package/build/src/akuma/coordinate.js +22 -0
  22. package/build/src/akuma/heart/facts.d.ts +88 -45
  23. package/build/src/akuma/heart/facts.js +19 -8
  24. package/build/src/akuma/heart/index.d.ts +64 -55
  25. package/build/src/akuma/heart/index.js +172 -252
  26. package/build/src/akuma/heart/rows.d.ts +38 -83
  27. package/build/src/akuma/heart/rows.js +117 -155
  28. package/build/src/akuma/heart/schema.d.ts +1 -1
  29. package/build/src/akuma/heart/schema.js +61 -22
  30. package/build/src/akuma/heart/soul.d.ts +8 -0
  31. package/build/src/akuma/heart/soul.js +136 -0
  32. package/build/src/akuma/heart/storage.d.ts +47 -0
  33. package/build/src/akuma/heart/storage.js +224 -0
  34. package/build/src/akuma/heart/tells.d.ts +12 -0
  35. package/build/src/akuma/heart/tells.js +79 -0
  36. package/build/src/akuma/heart/timeline.d.ts +15 -0
  37. package/build/src/akuma/heart/timeline.js +86 -0
  38. package/build/src/akuma/identity.d.ts +2 -2
  39. package/build/src/akuma/identity.js +9 -9
  40. package/build/src/akuma/index.d.ts +10 -3
  41. package/build/src/akuma/index.js +7 -1
  42. package/build/src/akuma/projection.d.ts +176 -0
  43. package/build/src/akuma/projection.js +256 -0
  44. package/build/src/akuma/provider-recipe.d.ts +24 -0
  45. package/build/src/akuma/provider-recipe.js +100 -0
  46. package/build/src/akuma/provider.d.ts +67 -13
  47. package/build/src/akuma/provider.js +172 -105
  48. package/build/src/akuma/providers/acp/core.d.ts +19 -0
  49. package/build/src/akuma/providers/acp/core.js +159 -0
  50. package/build/src/akuma/providers/acp/events.d.ts +20 -0
  51. package/build/src/akuma/providers/acp/events.js +73 -0
  52. package/build/src/akuma/providers/acp/index.d.ts +12 -0
  53. package/build/src/akuma/providers/acp/index.js +94 -0
  54. package/build/src/akuma/providers/{claude.d.ts → claude/events.d.ts} +9 -41
  55. package/build/src/akuma/providers/claude/events.js +238 -0
  56. package/build/src/akuma/providers/claude/index.d.ts +47 -0
  57. package/build/src/akuma/providers/claude/index.js +279 -0
  58. package/build/src/akuma/providers/claude/input.d.ts +13 -0
  59. package/build/src/akuma/providers/claude/input.js +109 -0
  60. package/build/src/akuma/providers/codex-app-server/events.d.ts +1 -1
  61. package/build/src/akuma/providers/codex-app-server/events.js +15 -12
  62. package/build/src/akuma/providers/codex-app-server/index.d.ts +15 -13
  63. package/build/src/akuma/providers/codex-app-server/index.js +77 -33
  64. package/build/src/akuma/providers/grok-build/index.d.ts +4 -0
  65. package/build/src/akuma/providers/grok-build/index.js +70 -0
  66. package/build/src/akuma/providers/index.d.ts +5 -3
  67. package/build/src/akuma/providers/index.js +35 -56
  68. package/build/src/akuma/providers/opencode-sdk/events.d.ts +54 -0
  69. package/build/src/akuma/providers/opencode-sdk/events.js +235 -0
  70. package/build/src/akuma/providers/opencode-sdk/index.d.ts +33 -0
  71. package/build/src/akuma/providers/opencode-sdk/index.js +308 -0
  72. package/build/src/akuma/providers/opencode-sdk/session.d.ts +26 -0
  73. package/build/src/akuma/providers/opencode-sdk/session.js +63 -0
  74. package/build/src/akuma/providers/pi/events.d.ts +33 -0
  75. package/build/src/akuma/providers/pi/events.js +166 -0
  76. package/build/src/akuma/providers/pi/index.d.ts +11 -0
  77. package/build/src/akuma/providers/pi/index.js +191 -0
  78. package/build/src/akuma/publication.d.ts +3 -3
  79. package/build/src/akuma/publication.js +28 -40
  80. package/build/src/akuma/requests.d.ts +9 -6
  81. package/build/src/akuma/requests.js +95 -65
  82. package/build/src/alias/index.d.ts +4 -3
  83. package/build/src/alias/index.js +12 -13
  84. package/build/src/body/decode.js +7 -1
  85. package/build/src/body/region.d.ts +2 -0
  86. package/build/src/body/region.js +77 -7
  87. package/build/src/body/render.js +2 -1
  88. package/build/src/body/shape.js +7 -1
  89. package/build/src/body/verification.js +19 -4
  90. package/build/src/cli/accepted.d.ts +18 -12
  91. package/build/src/cli/accepted.js +73 -34
  92. package/build/src/cli/actor.js +3 -3
  93. package/build/src/cli/commands/akuma-invoke.d.ts +44 -19
  94. package/build/src/cli/commands/akuma-invoke.js +101 -52
  95. package/build/src/cli/commands/akuma.d.ts +27 -9
  96. package/build/src/cli/commands/akuma.js +151 -80
  97. package/build/src/cli/commands/contract.d.ts +155 -0
  98. package/build/src/cli/commands/contract.js +20 -0
  99. package/build/src/cli/commands/install.js +2 -0
  100. package/build/src/cli/commands/task-invoke.d.ts +19 -4
  101. package/build/src/cli/commands/task-invoke.js +65 -17
  102. package/build/src/cli/commands/task-query.d.ts +5 -0
  103. package/build/src/cli/commands/task-query.js +247 -0
  104. package/build/src/cli/commands/task.d.ts +3 -1
  105. package/build/src/cli/commands/task.js +52 -14
  106. package/build/src/cli/coordinates.d.ts +18 -0
  107. package/build/src/cli/coordinates.js +120 -0
  108. package/build/src/cli/draft.d.ts +8 -0
  109. package/build/src/cli/draft.js +96 -0
  110. package/build/src/cli/invoke.d.ts +9 -3
  111. package/build/src/cli/invoke.js +287 -108
  112. package/build/src/cli/main.js +54 -7
  113. package/build/src/cli/parse.d.ts +21 -117
  114. package/build/src/cli/parse.js +79 -84
  115. package/build/src/cli/render/akuma-tool-command.d.ts +6 -0
  116. package/build/src/cli/render/akuma-tool-command.js +144 -0
  117. package/build/src/cli/render/akuma-tool.d.ts +4 -2
  118. package/build/src/cli/render/akuma-tool.js +35 -5
  119. package/build/src/cli/render/akuma.d.ts +3 -1
  120. package/build/src/cli/render/akuma.js +247 -212
  121. package/build/src/cli/render/audit.d.ts +3 -0
  122. package/build/src/cli/render/audit.js +104 -0
  123. package/build/src/cli/render/catalog.d.ts +2 -0
  124. package/build/src/cli/render/catalog.js +16 -0
  125. package/build/src/cli/render/contract.d.ts +3 -3
  126. package/build/src/cli/render/contract.js +200 -54
  127. package/build/src/cli/render/kanshi.js +272 -129
  128. package/build/src/cli/render/receipt.d.ts +19 -0
  129. package/build/src/cli/render/receipt.js +106 -0
  130. package/build/src/cli/render/refusal.d.ts +16 -2
  131. package/build/src/cli/render/refusal.js +88 -3
  132. package/build/src/cli/render/region.d.ts +2 -0
  133. package/build/src/cli/render/region.js +17 -0
  134. package/build/src/cli/render/task.d.ts +2 -1
  135. package/build/src/cli/render/task.js +223 -60
  136. package/build/src/cli/render/terminal.d.ts +20 -0
  137. package/build/src/cli/render/terminal.js +121 -0
  138. package/build/src/cli/render/text.js +11 -3
  139. package/build/src/cli/result.d.ts +126 -19
  140. package/build/src/cli/usage.d.ts +1 -0
  141. package/build/src/cli/usage.js +3 -0
  142. package/build/src/contract-worktree.d.ts +44 -0
  143. package/build/src/contract-worktree.js +251 -0
  144. package/build/src/coordination/durable-file.d.ts +5 -1
  145. package/build/src/coordination/durable-file.js +57 -10
  146. package/build/src/coordination/sqlite-transaction-lock.d.ts +4 -0
  147. package/build/src/coordination/sqlite-transaction-lock.js +22 -5
  148. package/build/src/core/facts/codec.js +23 -4
  149. package/build/src/core/facts/eligibility.d.ts +1 -10
  150. package/build/src/core/facts/eligibility.js +0 -49
  151. package/build/src/core/facts/fold.js +1 -6
  152. package/build/src/core/facts/gate.d.ts +1 -0
  153. package/build/src/core/facts/gate.js +1 -0
  154. package/build/src/core/facts/observation.d.ts +3 -0
  155. package/build/src/core/facts/observation.js +12 -0
  156. package/build/src/core/facts/types.d.ts +10 -3
  157. package/build/src/core/subject.js +2 -2
  158. package/build/src/core/verbs/amend.d.ts +1 -1
  159. package/build/src/core/verbs/amend.js +3 -18
  160. package/build/src/core/verbs/bind.js +2 -16
  161. package/build/src/core/verbs/deliver.d.ts +1 -1
  162. package/build/src/core/verbs/deliver.js +14 -4
  163. package/build/src/core/verbs/placement.d.ts +1 -1
  164. package/build/src/core/verbs/placement.js +7 -9
  165. package/build/src/dispatch/index.d.ts +5 -3
  166. package/build/src/dispatch/index.js +30 -21
  167. package/build/src/duration.d.ts +10 -0
  168. package/build/src/duration.js +22 -0
  169. package/build/src/git/admission.d.ts +2 -2
  170. package/build/src/git/admission.js +33 -23
  171. package/build/src/git/hooks.d.ts +15 -0
  172. package/build/src/git/hooks.js +70 -18
  173. package/build/src/git/identity.d.ts +5 -1
  174. package/build/src/git/identity.js +23 -4
  175. package/build/src/git/integration.d.ts +48 -0
  176. package/build/src/git/integration.js +250 -0
  177. package/build/src/git/observe.d.ts +29 -15
  178. package/build/src/git/observe.js +185 -76
  179. package/build/src/git/read-observation.d.ts +41 -0
  180. package/build/src/git/read-observation.js +361 -0
  181. package/build/src/git/reconcile.d.ts +17 -5
  182. package/build/src/git/reconcile.js +305 -109
  183. package/build/src/git/repository.d.ts +41 -18
  184. package/build/src/git/repository.js +220 -114
  185. package/build/src/git/scratch.d.ts +17 -0
  186. package/build/src/git/scratch.js +73 -0
  187. package/build/src/git/target-placement.d.ts +92 -0
  188. package/build/src/git/target-placement.js +375 -0
  189. package/build/src/git/tender.d.ts +49 -0
  190. package/build/src/git/tender.js +63 -0
  191. package/build/src/git/terminal-seal.d.ts +24 -0
  192. package/build/src/git/terminal-seal.js +89 -0
  193. package/build/src/git/tree.d.ts +2 -2
  194. package/build/src/git/tree.js +4 -0
  195. package/build/src/git/workspace.d.ts +53 -0
  196. package/build/src/git/workspace.js +139 -0
  197. package/build/src/identity/selector.d.ts +6 -0
  198. package/build/src/identity/selector.js +19 -0
  199. package/build/src/index.d.ts +4 -2
  200. package/build/src/index.js +2 -1
  201. package/build/src/kanshi/index.d.ts +2 -2
  202. package/build/src/kanshi/index.js +1 -1
  203. package/build/src/kanshi/read.d.ts +7 -3
  204. package/build/src/kanshi/read.js +245 -45
  205. package/build/src/kanshi/report.d.ts +81 -6
  206. package/build/src/kanshi/select.d.ts +5 -0
  207. package/build/src/kanshi/select.js +33 -1
  208. package/build/src/library/address.d.ts +57 -0
  209. package/build/src/library/address.js +165 -0
  210. package/build/src/library/akuma-creation.d.ts +16 -7
  211. package/build/src/library/akuma-creation.js +96 -22
  212. package/build/src/library/audit.d.ts +18 -0
  213. package/build/src/library/audit.js +44 -0
  214. package/build/src/library/bind.d.ts +21 -0
  215. package/build/src/library/bind.js +56 -0
  216. package/build/src/library/catalog.d.ts +58 -0
  217. package/build/src/library/catalog.js +69 -0
  218. package/build/src/library/configuration.d.ts +6 -15
  219. package/build/src/library/configuration.js +14 -17
  220. package/build/src/library/contract.d.ts +23 -49
  221. package/build/src/library/contract.js +171 -185
  222. package/build/src/library/delivery.d.ts +24 -0
  223. package/build/src/library/delivery.js +21 -0
  224. package/build/src/library/fleet.d.ts +65 -0
  225. package/build/src/library/fleet.js +153 -0
  226. package/build/src/library/input.d.ts +3 -0
  227. package/build/src/library/input.js +19 -0
  228. package/build/src/library/keiyaku.d.ts +18 -3
  229. package/build/src/library/keiyaku.js +12 -1
  230. package/build/src/library/mutation.d.ts +39 -0
  231. package/build/src/library/mutation.js +37 -0
  232. package/build/src/library/reconcile.d.ts +29 -0
  233. package/build/src/library/reconcile.js +179 -0
  234. package/build/src/library/refusal.d.ts +21 -0
  235. package/build/src/library/refusal.js +29 -0
  236. package/build/src/library/region.d.ts +14 -1
  237. package/build/src/library/region.js +18 -4
  238. package/build/src/library/repo.d.ts +4 -13
  239. package/build/src/library/repo.js +14 -24
  240. package/build/src/protocol/attempt.d.ts +16 -3
  241. package/build/src/protocol/attempt.js +41 -10
  242. package/build/src/protocol/bind.d.ts +9 -2
  243. package/build/src/protocol/bind.js +48 -40
  244. package/build/src/protocol/intent.d.ts +57 -20
  245. package/build/src/protocol/intent.js +55 -79
  246. package/build/src/protocol/operations.d.ts +152 -43
  247. package/build/src/protocol/operations.js +482 -166
  248. package/build/src/protocol/outcome.d.ts +14 -3
  249. package/build/src/protocol/outcome.js +11 -3
  250. package/build/src/protocol/placement.d.ts +41 -0
  251. package/build/src/protocol/placement.js +147 -0
  252. package/build/src/protocol/read/documents.d.ts +2 -2
  253. package/build/src/protocol/read/documents.js +4 -4
  254. package/build/src/protocol/read/status.d.ts +18 -5
  255. package/build/src/protocol/read/status.js +73 -13
  256. package/build/src/protocol/run.d.ts +49 -7
  257. package/build/src/protocol/run.js +53 -25
  258. package/build/src/runtime/proc/line-rpc.d.ts +4 -8
  259. package/build/src/runtime/proc/line-rpc.js +22 -28
  260. package/build/src/runtime/proc/run.d.ts +23 -21
  261. package/build/src/runtime/proc/run.js +123 -89
  262. package/build/src/runtime/proc/stdio.d.ts +18 -0
  263. package/build/src/runtime/proc/stdio.js +58 -0
  264. package/build/src/settings.d.ts +7 -1
  265. package/build/src/settings.js +32 -17
  266. package/build/src/settlement/fence.d.ts +5 -0
  267. package/build/src/settlement/fence.js +14 -0
  268. package/build/src/settlement/holder.d.ts +35 -4
  269. package/build/src/settlement/holder.js +127 -28
  270. package/build/src/settlement/settle.d.ts +9 -1
  271. package/build/src/settlement/settle.js +74 -37
  272. package/build/src/task/board.d.ts +23 -5
  273. package/build/src/task/board.js +17 -10
  274. package/build/src/task/compose.d.ts +2 -2
  275. package/build/src/task/compose.js +18 -10
  276. package/build/src/task/context.d.ts +3 -3
  277. package/build/src/task/context.js +15 -20
  278. package/build/src/task/document.d.ts +2 -1
  279. package/build/src/task/document.js +15 -5
  280. package/build/src/task/index.d.ts +27 -15
  281. package/build/src/task/index.js +57 -35
  282. package/build/src/task/operations.d.ts +38 -13
  283. package/build/src/task/operations.js +80 -46
  284. package/build/src/task/query.d.ts +67 -0
  285. package/build/src/task/query.js +279 -0
  286. package/build/src/task/store.d.ts +5 -7
  287. package/build/src/task/store.js +43 -28
  288. package/build/src/verification/declaration.d.ts +1 -0
  289. package/build/src/verification/execution.d.ts +51 -0
  290. package/build/src/verification/execution.js +108 -0
  291. package/build/src/workspace-place.d.ts +41 -0
  292. package/build/src/workspace-place.js +386 -0
  293. package/build/src/world.d.ts +24 -0
  294. package/build/src/world.js +136 -0
  295. package/package.json +8 -4
  296. package/build/src/akuma/activity.d.ts +0 -66
  297. package/build/src/akuma/activity.js +0 -139
  298. package/build/src/akuma/providers/claude.js +0 -305
  299. package/build/src/context-root.d.ts +0 -4
  300. package/build/src/context-root.js +0 -15
  301. package/build/src/git/delivery.d.ts +0 -20
  302. package/build/src/git/delivery.js +0 -109
  303. package/build/src/git/verification.d.ts +0 -14
  304. package/build/src/git/verification.js +0 -56
  305. package/build/src/protocol/read/audit.d.ts +0 -25
  306. package/build/src/protocol/read/audit.js +0 -39
  307. package/build/src/verification/producer.d.ts +0 -23
  308. package/build/src/verification/producer.js +0 -54
@@ -1,13 +1,14 @@
1
- import { execFileSync } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
  import { isAbsolute, resolve } from "node:path";
3
3
  import { AuthorityCorruptionError } from "../core/facts/errors.js";
4
+ import { consumeProcessStdout } from "../runtime/proc/run.js";
4
5
  import { gitObjectId } from "./identity.js";
5
6
  import { parseTreeObject, prepareTreeUpdate, treeUpdate, validPath, } from "./tree.js";
6
7
  export const GIT_REF = "refs/heads/keiyaku-state";
7
8
  export const DELIVERY_REF_NAMESPACE = "refs/heads/keiyaku-delivery";
8
9
  export const CANDIDATE_PIN_REF_NAMESPACE = "refs/heads/keiyaku-candidate";
9
10
  export const GIT_FORMAT_PATH = "meta/format.json";
10
- const CURRENT_FORMAT_VERSION = 3;
11
+ const CURRENT_FORMAT_VERSION = 4;
11
12
  export const GIT_FORMAT_BYTES = `{"version":${CURRENT_FORMAT_VERSION}}\n`;
12
13
  export function isKeiyakuOwnedRef(ref) {
13
14
  return ref === GIT_REF
@@ -25,20 +26,23 @@ export class NoGitWorldError extends Error {
25
26
  }
26
27
  }
27
28
  export class GitPlumbingError extends Error {
29
+ stdout;
28
30
  stderr;
29
31
  status;
30
32
  pid;
31
33
  constructor(input) {
32
34
  super(input.message);
33
35
  this.name = "GitPlumbingError";
36
+ this.stdout = Buffer.from(input.stdout ?? "");
34
37
  this.stderr = Buffer.from(input.stderr);
35
38
  this.status = input.status;
36
39
  this.pid = input.pid ?? null;
37
40
  }
38
41
  }
39
- function worktreePathsFromPorcelain(output) {
42
+ const GIT_STDERR_BYTES = 16 * 1024;
43
+ function worktreesFromPorcelain(output) {
40
44
  const fields = output.toString("utf8").split("\0");
41
- const paths = [];
45
+ const worktrees = [];
42
46
  let record = [];
43
47
  const finishRecord = () => {
44
48
  if (record.length === 0)
@@ -50,7 +54,11 @@ function worktreePathsFromPorcelain(output) {
50
54
  const path = worktree.slice("worktree ".length);
51
55
  if (path.length === 0)
52
56
  throw new Error("Git worktree porcelain output has an empty worktree path");
53
- paths.push(resolve(path));
57
+ const branchField = record.find((field) => field.startsWith("branch "));
58
+ const branch = branchField === undefined ? null : branchField.slice("branch ".length);
59
+ if (branch !== null && !branch.startsWith("refs/"))
60
+ throw new Error("Git worktree porcelain output has an invalid branch");
61
+ worktrees.push({ path: resolve(path), branch });
54
62
  record = [];
55
63
  };
56
64
  for (const field of fields) {
@@ -61,22 +69,31 @@ function worktreePathsFromPorcelain(output) {
61
69
  record.push(field);
62
70
  }
63
71
  finishRecord();
64
- if (paths.length === 0)
72
+ if (worktrees.length === 0)
65
73
  throw new Error("Git worktree porcelain output has no worktrees");
66
- return paths;
74
+ return worktrees;
67
75
  }
68
- export function registeredWorktreePaths(repository) {
69
- return worktreePathsFromPorcelain(runGit(repository, ["worktree", "list", "--porcelain", "-z"]));
76
+ export async function registeredWorktrees(repository) {
77
+ return worktreesFromPorcelain(await runGit(repository, ["worktree", "list", "--porcelain", "-z"]));
70
78
  }
71
- export function repositoryAt(cwd) {
79
+ export async function registeredWorktreePaths(repository) {
80
+ return (await registeredWorktrees(repository)).map((worktree) => worktree.path);
81
+ }
82
+ export async function repositoryAt(cwd) {
72
83
  if (typeof cwd !== "string" || cwd.length === 0) {
73
84
  throw new Error("repository path must be a nonempty string");
74
85
  }
75
86
  const effectiveCwd = resolve(cwd);
76
- const provisional = { effectiveCwd, primaryWorktree: effectiveCwd };
87
+ const provisional = {
88
+ effectiveCwd,
89
+ primaryWorktree: effectiveCwd,
90
+ commonDirectory: effectiveCwd,
91
+ };
77
92
  let primaryWorktree;
93
+ let commonDirectory;
78
94
  try {
79
- primaryWorktree = registeredWorktreePaths(provisional)[0];
95
+ primaryWorktree = (await registeredWorktreePaths(provisional))[0];
96
+ commonDirectory = await absoluteGitPath(provisional, ["--git-common-dir"], "common Git directory");
80
97
  }
81
98
  catch (error) {
82
99
  if (error instanceof GitPlumbingError && error.status === 128) {
@@ -84,58 +101,100 @@ export function repositoryAt(cwd) {
84
101
  }
85
102
  throw error;
86
103
  }
87
- return { effectiveCwd, primaryWorktree };
104
+ return { effectiveCwd, primaryWorktree, commonDirectory };
88
105
  }
89
- function absoluteGitPath(repository, args, label) {
90
- const value = runGit(repository, ["rev-parse", "--path-format=absolute", ...args]).toString("utf8").trim();
106
+ async function absoluteGitPath(repository, args, label) {
107
+ const value = (await runGit(repository, ["rev-parse", "--path-format=absolute", ...args])).toString("utf8").trim();
91
108
  if (value.length === 0)
92
109
  throw new Error(`${label} is empty`);
93
110
  return resolve(isAbsolute(value) ? value : resolve(repository.effectiveCwd, value));
94
111
  }
95
112
  export function commonGitDirectory(repository) {
96
- return absoluteGitPath(repository, ["--git-common-dir"], "common Git directory");
113
+ return repository.commonDirectory;
97
114
  }
98
- export function worktreeGitDirectory(repository, worktree) {
115
+ export async function worktreeGitDirectory(repository, worktree) {
99
116
  const scoped = { ...repository, effectiveCwd: worktree };
100
- return absoluteGitPath(scoped, ["--git-dir"], "worktree Git directory");
117
+ return await absoluteGitPath(scoped, ["--git-dir"], "worktree Git directory");
118
+ }
119
+ export async function worktreeRoot(repository) {
120
+ return await absoluteGitPath(repository, ["--show-toplevel"], "worktree root");
101
121
  }
102
122
  function commandError(command, error) {
103
123
  const candidate = error;
104
124
  const stderr = candidate.stderr === undefined ? Buffer.alloc(0) : Buffer.from(candidate.stderr);
125
+ const stdout = candidate.stdout === undefined ? Buffer.alloc(0) : Buffer.from(candidate.stdout);
105
126
  const detail = stderr.length === 0 ? candidate.message ?? "git command failed" : stderr.toString("utf8");
106
127
  return new GitPlumbingError({
128
+ stdout,
107
129
  stderr,
108
130
  status: candidate.status ?? null,
109
131
  message: `${command.join(" ")}: ${detail}`,
110
132
  pid: candidate.pid ?? null,
111
133
  });
112
134
  }
113
- export function runGit(repository, args, input) {
114
- try {
115
- const output = execFileSync("git", [...args], {
116
- cwd: repository.effectiveCwd,
117
- input,
118
- stdio: ["pipe", "pipe", "pipe"],
119
- });
120
- return Buffer.isBuffer(output) ? output : Buffer.from(output);
121
- }
122
- catch (error) {
123
- throw commandError(args, error);
124
- }
135
+ async function executeGit(repository, args, input, environment) {
136
+ const stdout = [];
137
+ const stderr = [];
138
+ let stderrBytes = 0;
139
+ let inputError;
140
+ const child = spawn("git", [...args], {
141
+ cwd: repository.effectiveCwd,
142
+ ...(environment === undefined ? {} : { env: { ...process.env, ...environment } }),
143
+ stdio: ["pipe", "pipe", "pipe"],
144
+ windowsHide: true,
145
+ });
146
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
147
+ child.stderr.on("data", (chunk) => {
148
+ if (stderrBytes >= GIT_STDERR_BYTES)
149
+ return;
150
+ const retained = chunk.subarray(0, GIT_STDERR_BYTES - stderrBytes);
151
+ stderr.push(retained);
152
+ stderrBytes += retained.length;
153
+ });
154
+ child.stdin.on("error", (error) => { inputError = error; });
155
+ child.stdin.end(input);
156
+ const terminal = await new Promise((resolveTerminal) => {
157
+ child.once("error", (error) => resolveTerminal({ code: null, error }));
158
+ child.once("close", (code) => resolveTerminal({ code }));
159
+ });
160
+ const output = Buffer.concat(stdout);
161
+ if (terminal.code === 0 && inputError === undefined)
162
+ return output;
163
+ throw commandError(args, {
164
+ message: terminal.error?.message ?? inputError?.message ?? `git exited with status ${terminal.code ?? "unknown"}`,
165
+ stdout: output,
166
+ stderr: Buffer.concat(stderr),
167
+ status: terminal.code,
168
+ pid: child.pid,
169
+ });
125
170
  }
126
- export function runGitWithEnvironment(repository, args, input, environment) {
127
- try {
128
- const output = execFileSync("git", [...args], {
129
- cwd: repository.effectiveCwd,
130
- input,
131
- env: { ...process.env, ...environment },
132
- stdio: ["pipe", "pipe", "pipe"],
133
- });
134
- return Buffer.isBuffer(output) ? output : Buffer.from(output);
135
- }
136
- catch (error) {
137
- throw commandError(args, error);
138
- }
171
+ export async function runGit(repository, args, input) {
172
+ return await executeGit(repository, args, input);
173
+ }
174
+ export async function consumeGitStdout(repository, args, consume) {
175
+ const result = await consumeProcessStdout({
176
+ argv: ["git", ...args],
177
+ cwd: repository.effectiveCwd,
178
+ }, consume);
179
+ const { outcome } = result;
180
+ if (outcome.kind === "terminal" && outcome.code === 0)
181
+ return;
182
+ const status = outcome.kind === "terminal" ? outcome.code : null;
183
+ const stderr = outcome.kind === "terminal" ? outcome.stderr : "";
184
+ const message = outcome.kind === "terminal"
185
+ ? `git exited with status ${outcome.code}`
186
+ : outcome.kind === "spawn-error" || outcome.kind === "stream-error"
187
+ ? outcome.diagnostic
188
+ : `git process ended with ${outcome.kind}`;
189
+ throw commandError(args, {
190
+ message,
191
+ stderr,
192
+ status,
193
+ pid: result.pid ?? undefined,
194
+ });
195
+ }
196
+ export async function runGitWithEnvironment(repository, args, input, environment) {
197
+ return await executeGit(repository, args, input, environment);
139
198
  }
140
199
  function assertOid(oid, label) {
141
200
  gitObjectId(oid, label);
@@ -145,10 +204,14 @@ function assertRef(ref) {
145
204
  throw new Error(`invalid Git ref: ${ref}`);
146
205
  }
147
206
  }
148
- export function readRef(repository, ref) {
149
- assertRef(ref);
207
+ function assertAssertionRef(ref) {
208
+ if (ref !== "HEAD")
209
+ assertRef(ref);
210
+ }
211
+ export async function readRef(repository, ref) {
212
+ assertAssertionRef(ref);
150
213
  try {
151
- const oid = runGit(repository, ["rev-parse", "--verify", "--quiet", ref]).toString("utf8").trim();
214
+ const oid = (await runGit(repository, ["rev-parse", "--verify", "--quiet", ref])).toString("utf8").trim();
152
215
  if (oid.length === 0)
153
216
  return null;
154
217
  assertOid(oid, `ref ${ref}`);
@@ -160,9 +223,9 @@ export function readRef(repository, ref) {
160
223
  throw error;
161
224
  }
162
225
  }
163
- function readTreeForCommit(repository, commit) {
226
+ async function readTreeForCommit(repository, commit) {
164
227
  assertOid(commit, "Git commit");
165
- const tree = runGit(repository, ["rev-parse", "--verify", `${commit}^{tree}`]).toString("utf8").trim();
228
+ const tree = (await runGit(repository, ["show", "-s", "--format=%T", commit])).toString("utf8").trim();
166
229
  assertOid(tree, "Git tree");
167
230
  return tree;
168
231
  }
@@ -184,30 +247,30 @@ function parseTreeEntries(output) {
184
247
  }
185
248
  return entries;
186
249
  }
187
- function readTreeEntries(repository, tree) {
250
+ export async function readTreeEntries(repository, tree) {
188
251
  assertOid(tree, "tree");
189
- const output = runGit(repository, ["ls-tree", "-r", "-z", "--full-tree", tree]);
252
+ const output = await runGit(repository, ["ls-tree", "-r", "-z", "--full-tree", tree]);
190
253
  return parseTreeEntries(output);
191
254
  }
192
- export function readGit(repository) {
193
- const commit = readRef(repository, GIT_REF);
255
+ export async function readGit(repository) {
256
+ const commit = await readRef(repository, GIT_REF);
194
257
  if (commit === null) {
195
258
  return { commit: null, tree: null, paths: new Map() };
196
259
  }
197
- const tree = readTreeForCommit(repository, commit);
198
- const paths = readTreeEntries(repository, tree);
199
- validateGitFormat(repository, paths);
260
+ const tree = await readTreeForCommit(repository, commit);
261
+ const paths = await readTreeEntries(repository, tree);
262
+ await validateGitFormat(repository, paths);
200
263
  return { commit, tree, paths };
201
264
  }
202
265
  /** Read only the requested private Git paths from one immutable Git tree. */
203
- export function readGitPaths(repository, requestedPaths) {
266
+ export async function readGitPaths(repository, requestedPaths) {
204
267
  for (const path of requestedPaths)
205
268
  validPath(path);
206
- const commit = readRef(repository, GIT_REF);
269
+ const commit = await readRef(repository, GIT_REF);
207
270
  if (commit === null)
208
271
  return { commit: null, tree: null, paths: new Map() };
209
- const tree = readTreeForCommit(repository, commit);
210
- const paths = parseTreeEntries(runGit(repository, [
272
+ const tree = await readTreeForCommit(repository, commit);
273
+ const paths = parseTreeEntries(await runGit(repository, [
211
274
  "ls-tree",
212
275
  "-z",
213
276
  "--full-tree",
@@ -216,16 +279,16 @@ export function readGitPaths(repository, requestedPaths) {
216
279
  GIT_FORMAT_PATH,
217
280
  ...requestedPaths,
218
281
  ]));
219
- validateGitFormat(repository, paths);
282
+ await validateGitFormat(repository, paths);
220
283
  return { commit, tree, paths };
221
284
  }
222
285
  /** Add paths from the same immutable Git tree without rereading its ref. */
223
- export function extendGitPaths(repository, snapshot, requestedPaths) {
286
+ export async function extendGitPaths(repository, snapshot, requestedPaths) {
224
287
  for (const path of requestedPaths)
225
288
  validPath(path);
226
289
  if (snapshot.tree === null || requestedPaths.length === 0)
227
290
  return snapshot;
228
- const additions = parseTreeEntries(runGit(repository, [
291
+ const additions = parseTreeEntries(await runGit(repository, [
229
292
  "ls-tree",
230
293
  "-z",
231
294
  "--full-tree",
@@ -236,19 +299,19 @@ export function extendGitPaths(repository, snapshot, requestedPaths) {
236
299
  return { ...snapshot, paths: new Map([...snapshot.paths, ...additions]) };
237
300
  }
238
301
  /** Expand a targeted immutable snapshot to its complete already-pinned tree. */
239
- export function expandGitSnapshot(repository, snapshot) {
302
+ export async function expandGitSnapshot(repository, snapshot) {
240
303
  if (snapshot.tree === null)
241
304
  return snapshot;
242
- return { ...snapshot, paths: readTreeEntries(repository, snapshot.tree) };
305
+ return { ...snapshot, paths: await readTreeEntries(repository, snapshot.tree) };
243
306
  }
244
- export function writeBlob(repository, bytes) {
245
- const oid = runGit(repository, ["hash-object", "-w", "--stdin"], bytes).toString("utf8").trim();
307
+ export async function writeBlob(repository, bytes) {
308
+ const oid = (await runGit(repository, ["hash-object", "-w", "--stdin"], bytes)).toString("utf8").trim();
246
309
  assertOid(oid, "written blob");
247
310
  return oid;
248
311
  }
249
- export function readBlob(repository, oid) {
312
+ export async function readBlob(repository, oid) {
250
313
  assertOid(oid, "blob");
251
- return runGit(repository, ["cat-file", "blob", oid]);
314
+ return await runGit(repository, ["cat-file", "blob", oid]);
252
315
  }
253
316
  function malformedBatchOutput(detail) {
254
317
  throw new GitPlumbingError({
@@ -257,13 +320,13 @@ function malformedBatchOutput(detail) {
257
320
  message: `malformed cat-file --batch output: ${detail}`,
258
321
  });
259
322
  }
260
- function readObjects(repository, oids) {
323
+ async function readObjects(repository, oids) {
261
324
  const unique = [...new Set(oids)];
262
325
  for (const oid of unique)
263
326
  assertOid(oid, "Git object");
264
327
  if (unique.length === 0)
265
328
  return new Map();
266
- const output = runGit(repository, ["cat-file", "--batch"], `${unique.join("\n")}\n`);
329
+ const output = await runGit(repository, ["cat-file", "--batch"], `${unique.join("\n")}\n`);
267
330
  const objects = new Map();
268
331
  let offset = 0;
269
332
  for (const oid of unique) {
@@ -299,32 +362,22 @@ function readObjects(repository, oids) {
299
362
  malformedBatchOutput("trailing bytes");
300
363
  return objects;
301
364
  }
302
- /** Read immutable Git objects through one structured, length-delimited batch. */
303
- export function readBlobs(repository, oids) {
304
- const blobs = new Map();
305
- for (const [oid, object] of readObjects(repository, oids)) {
306
- if (object.type !== "blob")
307
- malformedBatchOutput(`expected blob for ${oid}, found ${object.type}`);
308
- blobs.set(oid, object.bytes);
309
- }
310
- return blobs;
311
- }
312
- function validateGitFormat(repository, paths) {
365
+ async function validateGitFormat(repository, paths) {
313
366
  const format = paths.get(GIT_FORMAT_PATH);
314
367
  if (format === undefined)
315
368
  throw new AuthorityCorruptionError(`Git is missing ${GIT_FORMAT_PATH}`);
316
369
  if (format.type !== "blob")
317
370
  throw new AuthorityCorruptionError(`Git format is not a blob: ${GIT_FORMAT_PATH}`);
318
- const bytes = readBlob(repository, format.oid);
371
+ const bytes = await readBlob(repository, format.oid);
319
372
  if (bytes.toString("utf8") !== GIT_FORMAT_BYTES) {
320
373
  throw new AuthorityCorruptionError(`Git format is not current: ${GIT_FORMAT_PATH}`);
321
374
  }
322
375
  }
323
- function baseTreeEntries(repository, baseTree, changedPaths, nodePaths) {
376
+ async function baseTreeEntries(repository, baseTree, changedPaths, nodePaths) {
324
377
  const byPath = new Map();
325
378
  if (baseTree === null)
326
379
  return byPath;
327
- const ancestry = parseTreeEntries(runGit(repository, [
380
+ const ancestry = parseTreeEntries(await runGit(repository, [
328
381
  "ls-tree",
329
382
  "-z",
330
383
  "-t",
@@ -345,7 +398,7 @@ function baseTreeEntries(repository, baseTree, changedPaths, nodePaths) {
345
398
  throw new Error(`Git path is both file and directory: ${path}`);
346
399
  treeByPath.set(path, entry.oid);
347
400
  }
348
- const objects = readObjects(repository, [...treeByPath.values()]);
401
+ const objects = await readObjects(repository, [...treeByPath.values()]);
349
402
  const oidBytes = baseTree.length / 2;
350
403
  for (const [path, oid] of treeByPath) {
351
404
  const object = objects.get(oid);
@@ -356,8 +409,8 @@ function baseTreeEntries(repository, baseTree, changedPaths, nodePaths) {
356
409
  }
357
410
  return byPath;
358
411
  }
359
- function writePreparedTrees(repository, prepared) {
360
- const output = runGit(repository, ["mktree", "--batch"], prepared.map(({ records }) => `${records}\n`).join(""));
412
+ async function writePreparedTrees(repository, prepared) {
413
+ const output = await runGit(repository, ["mktree", "--batch"], prepared.map(({ records }) => `${records}\n`).join(""));
361
414
  const written = output.toString("ascii").trimEnd().split("\n");
362
415
  if (written.length !== prepared.length) {
363
416
  throw new GitPlumbingError({ stderr: output, status: null, message: "malformed mktree --batch output" });
@@ -372,17 +425,31 @@ function writePreparedTrees(repository, prepared) {
372
425
  }
373
426
  }
374
427
  /** Copy only the changed paths' tree ancestors, preserving untouched subtrees by object ID. */
375
- export function updateGitTree(repository, baseTree, changes) {
428
+ export async function updateGitTree(repository, baseTree, changes) {
376
429
  const update = treeUpdate(changes);
377
- const bases = baseTreeEntries(repository, baseTree, [...changes.keys()], update.nodePaths);
378
- const representativeOid = baseTree ?? changes.values().next().value?.oid;
430
+ const bases = await baseTreeEntries(repository, baseTree, [...changes.keys()], update.nodePaths);
431
+ const representativeOid = baseTree ?? [...changes.values()].find((change) => change !== null)?.oid;
379
432
  if (representativeOid === undefined)
380
433
  throw new Error("cannot write an empty Git tree without an object format");
381
434
  const prepared = prepareTreeUpdate({ update, bases, oidBytes: representativeOid.length / 2 });
382
- writePreparedTrees(repository, prepared.trees);
435
+ await writePreparedTrees(repository, prepared.trees);
436
+ return prepared.root;
437
+ }
438
+ /** Write a tree update from directory entries frozen with the admitted base tree. */
439
+ export async function updateGitTreeFromFrozenDirectories(repository, baseTree, directories, changes) {
440
+ const update = treeUpdate(changes);
441
+ const representativeOid = baseTree ?? [...changes.values()].find((change) => change !== null)?.oid;
442
+ if (representativeOid === undefined)
443
+ throw new Error("cannot write an empty Git tree without an object format");
444
+ const prepared = prepareTreeUpdate({
445
+ update,
446
+ bases: directories,
447
+ oidBytes: representativeOid.length / 2,
448
+ });
449
+ await writePreparedTrees(repository, prepared.trees);
383
450
  return prepared.root;
384
451
  }
385
- function writeCommitObject(input) {
452
+ async function writeCommitObject(input) {
386
453
  const { repository, tree, parents, message, actor, email, at } = input;
387
454
  assertOid(tree, "commit tree");
388
455
  for (const parent of parents)
@@ -390,20 +457,20 @@ function writeCommitObject(input) {
390
457
  const args = ["commit-tree", tree];
391
458
  for (const parent of parents)
392
459
  args.push("-p", parent);
393
- const commit = runGitWithEnvironment(repository, args, `${message}\n`, {
460
+ const commit = (await runGitWithEnvironment(repository, args, `${message}\n`, {
394
461
  GIT_AUTHOR_NAME: actor,
395
462
  GIT_AUTHOR_EMAIL: email,
396
463
  GIT_COMMITTER_NAME: actor,
397
464
  GIT_COMMITTER_EMAIL: email,
398
465
  ...(at === undefined ? {} : { GIT_AUTHOR_DATE: at, GIT_COMMITTER_DATE: at }),
399
- })
466
+ }))
400
467
  .toString("utf8")
401
468
  .trim();
402
469
  assertOid(commit, "written Git commit");
403
470
  return commit;
404
471
  }
405
- export function writeCommit(input) {
406
- return writeCommitObject({
472
+ export async function writeCommit(input) {
473
+ return await writeCommitObject({
407
474
  repository: input.repository,
408
475
  tree: input.tree,
409
476
  parents: input.parent === null ? [] : [input.parent],
@@ -413,7 +480,23 @@ export function writeCommit(input) {
413
480
  ...(input.at === undefined ? {} : { at: input.at }),
414
481
  });
415
482
  }
416
- export function updateRefsAtomically(repository, updates) {
483
+ function refAssertionLine(assertion) {
484
+ assertAssertionRef(assertion.ref);
485
+ assertOid(assertion.oid, `expected oid for ${assertion.ref}`);
486
+ return `${assertion.ref === "HEAD" ? "" : "option no-deref\n"}verify ${assertion.ref} ${assertion.oid}`;
487
+ }
488
+ function refUpdateLine(update, index) {
489
+ assertRef(update.ref);
490
+ if (index > 0 && update.ref === GIT_REF)
491
+ throw new Error(`duplicate ref update: ${update.ref}`);
492
+ if (index > 0 && update.expectedOid === null)
493
+ throw new Error("target ref updates require non-null OIDs");
494
+ assertOid(update.newOid, `new oid for ${update.ref}`);
495
+ if (update.expectedOid !== null)
496
+ assertOid(update.expectedOid, `expected oid for ${update.ref}`);
497
+ return `option no-deref\nupdate ${update.ref} ${update.newOid} ${update.expectedOid ?? "0".repeat(update.newOid.length)}`;
498
+ }
499
+ export async function updateRefsAtomically(repository, updates, assertions = []) {
417
500
  if (updates.length === 0)
418
501
  throw new Error("an atomic ref transaction needs a Git update");
419
502
  if (updates.length > 2)
@@ -422,24 +505,16 @@ export function updateRefsAtomically(repository, updates) {
422
505
  if (git === undefined || git.ref !== GIT_REF) {
423
506
  throw new Error(`the first atomic update must be the Git ref: ${GIT_REF}`);
424
507
  }
425
- const lines = ["start"];
426
- for (const [index, update] of updates.entries()) {
427
- assertRef(update.ref);
428
- if (index > 0 && update.ref === GIT_REF) {
429
- throw new Error(`duplicate ref update: ${update.ref}`);
430
- }
431
- if (index > 0 && (update.newOid === null || update.expectedOid === null)) {
432
- throw new Error("target ref updates require non-null OIDs");
433
- }
434
- assertOid(update.newOid, `new oid for ${update.ref}`);
435
- if (update.expectedOid !== null)
436
- assertOid(update.expectedOid, `expected oid for ${update.ref}`);
437
- const expectedOid = update.expectedOid ?? "0".repeat(update.newOid.length);
438
- lines.push(`update ${update.ref} ${update.newOid} ${expectedOid}`);
439
- }
440
- lines.push("prepare", "commit", "");
508
+ const lines = [
509
+ "start",
510
+ ...assertions.map(refAssertionLine),
511
+ ...updates.map(refUpdateLine),
512
+ "prepare",
513
+ "commit",
514
+ "",
515
+ ];
441
516
  try {
442
- runGit(repository, ["update-ref", "--stdin", "--no-deref"], lines.join("\n"));
517
+ await runGit(repository, ["update-ref", "--stdin"], lines.join("\n"));
443
518
  return { kind: "published" };
444
519
  }
445
520
  catch (error) {
@@ -454,3 +529,34 @@ export function updateRefsAtomically(repository, updates) {
454
529
  throw error;
455
530
  }
456
531
  }
532
+ export function decodeGitNumstat(output) {
533
+ const fields = output.toString("utf8").split("\0");
534
+ if (fields.at(-1) !== "")
535
+ throw new Error("Git numstat output is not NUL terminated");
536
+ let filesChanged = 0;
537
+ let insertions = 0;
538
+ let deletions = 0;
539
+ for (let index = 0; index < fields.length - 1; index += 1) {
540
+ const field = fields[index];
541
+ const [added, deleted, path, extra] = field.split("\t");
542
+ if (added === undefined || deleted === undefined || path === undefined || extra !== undefined) {
543
+ throw new Error("Git numstat output is malformed");
544
+ }
545
+ if (path.length === 0) {
546
+ if (fields[index + 1] === undefined || fields[index + 2] === undefined) {
547
+ throw new Error("Git rename numstat output is missing paths");
548
+ }
549
+ index += 2;
550
+ }
551
+ filesChanged += 1;
552
+ insertions += added === "-" ? 0 : Number.parseInt(added, 10);
553
+ deletions += deleted === "-" ? 0 : Number.parseInt(deleted, 10);
554
+ }
555
+ return { filesChanged, insertions, deletions };
556
+ }
557
+ export function decodeGitNameOnly(output) {
558
+ const names = output.toString("utf8").split("\0");
559
+ if (names.at(-1) !== "")
560
+ throw new Error("Git name-only output is not NUL terminated");
561
+ return names.slice(0, -1);
562
+ }
@@ -0,0 +1,17 @@
1
+ import type { SnapshotId } from "../core/facts/types.js";
2
+ import { type GitRepository } from "./repository.js";
3
+ export type MaterializedScratchCandidate = Readonly<{
4
+ cwd: string;
5
+ dispose: () => Promise<WorktreeLeak | null>;
6
+ }>;
7
+ export type CollectableScratchWorktree = Readonly<{
8
+ path: string;
9
+ release(): void;
10
+ }>;
11
+ export type WorktreeLeak = Readonly<{
12
+ path: string;
13
+ diagnostic: string;
14
+ }>;
15
+ export declare function collectableScratchWorktrees(paths: Iterable<string>): Promise<readonly CollectableScratchWorktree[]>;
16
+ /** Materialize a disposable candidate worktree and own only its removal. */
17
+ export declare function materializeScratchCandidate(repository: GitRepository, candidate: SnapshotId): Promise<MaterializedScratchCandidate>;
@@ -0,0 +1,73 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { realpath } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, dirname, join } from "node:path";
5
+ import { tryAcquireSqliteTransactionLock, } from "../coordination/sqlite-transaction-lock.js";
6
+ import { gitObjectIdForSnapshot } from "./identity.js";
7
+ import { runGit } from "./repository.js";
8
+ const SCRATCH_PREFIX = "keiyaku-v4-verify-";
9
+ const SCRATCH_ROOT = await realpath(tmpdir());
10
+ const SCRATCH_PATTERN = /^keiyaku-v4-verify-([0-9a-f]{24})$/;
11
+ function scratchPath() {
12
+ return join(SCRATCH_ROOT, `${SCRATCH_PREFIX}${randomBytes(12).toString("hex")}`);
13
+ }
14
+ function ownershipLockPath(path) {
15
+ if (dirname(path) !== SCRATCH_ROOT)
16
+ return null;
17
+ const match = SCRATCH_PATTERN.exec(basename(path));
18
+ return match === null ? null : join(SCRATCH_ROOT, `.${SCRATCH_PREFIX}${match[1]}.owner.sqlite`);
19
+ }
20
+ export async function collectableScratchWorktrees(paths) {
21
+ const collectable = [];
22
+ for (const path of paths) {
23
+ const lockPath = ownershipLockPath(path);
24
+ if (lockPath === null)
25
+ continue;
26
+ const lock = await tryAcquireSqliteTransactionLock({ path: lockPath, mode: "exclusive" });
27
+ if (lock !== null)
28
+ collectable.push({ path, release: () => lock.close() });
29
+ }
30
+ return collectable;
31
+ }
32
+ /** Materialize a disposable candidate worktree and own only its removal. */
33
+ export async function materializeScratchCandidate(repository, candidate) {
34
+ const cwd = scratchPath();
35
+ const lockPath = ownershipLockPath(cwd);
36
+ if (lockPath === null)
37
+ throw new Error("scratch ownership lock path is invalid");
38
+ const ownership = await tryAcquireSqliteTransactionLock({
39
+ path: lockPath,
40
+ mode: "exclusive",
41
+ });
42
+ if (ownership === null)
43
+ throw new Error("new scratch ownership lock is unexpectedly held");
44
+ try {
45
+ await runGit(repository, ["worktree", "add", "--detach", cwd, gitObjectIdForSnapshot(candidate)]);
46
+ }
47
+ catch (error) {
48
+ ownership.close();
49
+ throw error;
50
+ }
51
+ let disposed = false;
52
+ return {
53
+ cwd,
54
+ dispose: async () => {
55
+ if (disposed)
56
+ return null;
57
+ disposed = true;
58
+ try {
59
+ await runGit(repository, ["worktree", "remove", "--force", cwd]);
60
+ return null;
61
+ }
62
+ catch (error) {
63
+ return {
64
+ path: cwd,
65
+ diagnostic: error instanceof Error ? error.message : String(error),
66
+ };
67
+ }
68
+ finally {
69
+ ownership.close();
70
+ }
71
+ },
72
+ };
73
+ }