@astrosheep/keiyaku 2.9.8 → 2.9.10
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 +50 -182
- package/build/.tsbuildinfo +1 -1
- package/build/agents/harness/control-types.js +18 -1
- package/build/agents/harness/index.js +2 -0
- package/build/agents/harness/outcome-error.js +81 -0
- package/build/agents/harness/projection.js +10 -7
- package/build/agents/harness/router.js +5 -1
- package/build/agents/providers/claude-agent-sdk/adapter.js +18 -4
- package/build/agents/providers/claude-agent-sdk/registration.js +11 -1
- package/build/agents/providers/claude-agent-sdk/session.js +19 -5
- package/build/agents/providers/codex-app-server/adapter.js +11 -6
- package/build/agents/providers/codex-app-server/events.js +67 -6
- package/build/agents/providers/codex-app-server/registration.js +11 -1
- package/build/agents/providers/opencode-sdk/adapter.js +8 -1
- package/build/agents/providers/opencode-sdk/events.js +24 -2
- package/build/agents/providers/opencode-sdk/registration.js +11 -1
- package/build/agents/providers/pi/checkpoint.js +9 -1
- package/build/agents/providers/pi/events.js +2 -0
- package/build/agents/providers/pi/registration.js +14 -1
- package/build/cli/commands/akuma/akuma/handler.js +1 -1
- package/build/cli/commands/akuma/akuma/meta.js +3 -3
- package/build/cli/commands/akuma/catalog.js +2 -2
- package/build/cli/commands/akuma/{view → show}/handler.js +5 -5
- package/build/cli/commands/akuma/{view → show}/meta.js +3 -3
- package/build/cli/commands/akuma.js +1 -1
- package/build/cli/commands/contract/amend/handler.js +5 -4
- package/build/cli/commands/contract/arc/handler.js +5 -4
- package/build/cli/commands/contract/audit/handler.js +2 -2
- package/build/cli/commands/contract/bind/handler.js +12 -3
- package/build/cli/commands/contract/bind/meta.js +45 -15
- package/build/cli/commands/contract/forfeit/handler.js +9 -5
- package/build/cli/commands/contract/log/handler.js +2 -2
- package/build/cli/commands/contract/petition/handler.js +34 -7
- package/build/cli/commands/contract/renew/handler.js +7 -7
- package/build/cli/commands/contract/renew/meta.js +3 -3
- package/build/cli/commands/metadata.js +10 -6
- package/build/cli/commands/projection/call/meta.js +1 -1
- package/build/cli/commands/projection/catalog.js +2 -0
- package/build/cli/commands/projection/history/handler.js +21 -0
- package/build/cli/commands/projection/history/meta.js +9 -0
- package/build/cli/commands/projection/shared.js +3 -4
- package/build/cli/commands/projection/status/handler.js +3 -3
- package/build/cli/commands/projection/wait/handler.js +20 -11
- package/build/cli/commands/task/add/handler.js +14 -5
- package/build/cli/commands/task/add/meta.js +2 -2
- package/build/cli/commands/task/catalog.js +4 -0
- package/build/cli/commands/task/done/handler.js +1 -4
- package/build/cli/commands/task/done/meta.js +1 -1
- package/build/cli/commands/task/drop/meta.js +1 -1
- package/build/cli/commands/task/hold/handler.js +3 -0
- package/build/cli/commands/task/hold/meta.js +8 -0
- package/build/cli/commands/task/log/handler.js +1 -1
- package/build/cli/commands/task/log/meta.js +2 -2
- package/build/cli/commands/task/resume/handler.js +3 -0
- package/build/cli/commands/task/resume/meta.js +8 -0
- package/build/cli/commands/task/shared.js +39 -14
- package/build/cli/commands/task/show/handler.js +1 -1
- package/build/cli/commands/task/show/meta.js +1 -1
- package/build/cli/commands/task/start/meta.js +1 -1
- package/build/cli/commands/task/stop/handler.js +1 -4
- package/build/cli/commands/task/stop/meta.js +1 -1
- package/build/cli/commands/task/update/handler.js +11 -3
- package/build/cli/commands/task/update/meta.js +3 -3
- package/build/cli/completion.js +3 -2
- package/build/cli/flags.js +51 -14
- package/build/cli/help.js +4 -1
- package/build/cli/index.js +40 -25
- package/build/cli/parse-flags.js +40 -7
- package/build/cli/parse-metadata.js +5 -2
- package/build/cli/parse-selectors.js +2 -3
- package/build/cli/parse.js +40 -6
- package/build/cli/projection-address.js +12 -10
- package/build/cli/render/amendment-warning.js +8 -0
- package/build/cli/render/arc.js +6 -2
- package/build/cli/render/audit.js +400 -81
- package/build/cli/render/kanshi.js +242 -79
- package/build/cli/render/lifecycle.js +56 -0
- package/build/cli/render/misc.js +24 -28
- package/build/cli/render/petition.js +5 -0
- package/build/cli/render/projection-activity.js +65 -29
- package/build/cli/render/projection-history.js +64 -0
- package/build/cli/render/shared.js +23 -9
- package/build/cli/render/status.js +13 -35
- package/build/cli/render/task.js +51 -26
- package/build/cli/render/terminal-failure.js +3 -1
- package/build/cli/render/wait.js +45 -69
- package/build/cli/subagent-guard.js +1 -1
- package/build/cli/types.js +1 -1
- package/build/config/env-keys.js +3 -1
- package/build/config/env.js +24 -1
- package/build/config/settings/schema.js +3 -1
- package/build/core/address-carrier.js +89 -0
- package/build/core/addressing.js +255 -196
- package/build/core/amend.js +115 -38
- package/build/core/amendment-criteria.js +17 -0
- package/build/core/amendment-warning.js +10 -0
- package/build/core/arc.js +184 -83
- package/build/core/audit/candidate.js +110 -9
- package/build/core/audit/coordinates.js +120 -65
- package/build/core/audit/evidence.js +19 -23
- package/build/core/audit/facade.js +34 -11
- package/build/core/audit/report.js +5 -5
- package/build/core/bind-reconciliation.js +390 -0
- package/build/core/bind-workspace.js +12 -20
- package/build/core/bind.js +242 -273
- package/build/core/call/call.js +1 -0
- package/build/core/call/context.js +46 -71
- package/build/core/call/prompt.js +6 -3
- package/build/core/context.js +49 -35
- package/build/core/contract-carrier-runtime.js +408 -0
- package/build/core/contract-carrier.js +270 -0
- package/build/core/contract-view.js +33 -27
- package/build/core/contract.js +5 -1
- package/build/core/derived-replay.js +207 -0
- package/build/core/draft.js +125 -57
- package/build/core/forfeit.js +113 -56
- package/build/core/identifier-slug.js +63 -13
- package/build/core/ids.js +4 -4
- package/build/core/lifecycle-history.js +35 -0
- package/build/core/lifecycle-recovery.js +117 -0
- package/build/core/lifecycle-runner.js +377 -0
- package/build/core/log.js +37 -11
- package/build/core/outcome-base.js +181 -0
- package/build/core/process-group.js +139 -0
- package/build/core/projection/generation/database.js +82 -7
- package/build/core/projection/generation/model.js +9 -36
- package/build/core/projection/generation/projection-generation-continuation.js +10 -22
- package/build/core/projection/generation/projection-generation-execution.js +13 -6
- package/build/core/projection/index.js +5 -4
- package/build/core/projection/projection-activity.js +29 -8
- package/build/core/projection/projection-alias.js +23 -10
- package/build/core/projection/projection-core.js +21 -7
- package/build/core/projection/projection-execution-observer.js +91 -39
- package/build/core/projection/projection-history.js +474 -0
- package/build/core/projection/projection-runner-lock.js +12 -0
- package/build/core/projection/projection-status-observation.js +365 -0
- package/build/core/projection/projection-status.js +35 -337
- package/build/core/projection/projection-terminal-failure.js +13 -8
- package/build/core/projection/projection-wait.js +7 -2
- package/build/core/projection/tell/launch-store.js +244 -0
- package/build/core/projection/tell/model.js +3 -3
- package/build/core/projection/tell/store.js +70 -118
- package/build/core/registry.js +22 -87
- package/build/core/renew-build.js +424 -0
- package/build/core/renew-plan.js +239 -0
- package/build/core/renew-rewrite.js +77 -0
- package/build/core/renew.js +295 -520
- package/build/core/repository-ledger/accepted-fold-read.js +19 -0
- package/build/core/repository-ledger/accepted-tail.js +116 -0
- package/build/core/repository-ledger/admission.js +38 -0
- package/build/core/repository-ledger/authoritative-transaction.js +1 -0
- package/build/core/repository-ledger/claim-evidence.js +147 -0
- package/build/core/repository-ledger/codec.js +297 -0
- package/build/core/repository-ledger/current-state-store.js +853 -0
- package/build/core/repository-ledger/fold-repository.js +375 -0
- package/build/core/repository-ledger/fold.js +248 -0
- package/build/core/repository-ledger/git-object-store.js +243 -0
- package/build/core/repository-ledger/history-read.js +15 -0
- package/build/core/repository-ledger/incremental-read.js +153 -0
- package/build/core/repository-ledger/index.js +15 -0
- package/build/core/repository-ledger/inventory-plan.js +75 -0
- package/build/core/repository-ledger/inventory.js +310 -0
- package/build/core/repository-ledger/publication-admission.js +125 -0
- package/build/core/repository-ledger/publication.js +353 -0
- package/build/core/repository-ledger/read-model.js +469 -0
- package/build/core/repository-ledger/transaction-membership.js +78 -0
- package/build/core/repository-ledger/traversal.js +404 -0
- package/build/core/repository-ledger/write-transaction.js +339 -0
- package/build/core/seal.js +168 -161
- package/build/core/settlement/claim-delivery.js +42 -215
- package/build/core/settlement/claim.js +235 -153
- package/build/core/settlement/index.js +4 -4
- package/build/core/settlement/petition-claim-gates.js +52 -33
- package/build/core/settlement/petition-forfeit.js +12 -5
- package/build/core/settlement/petition-head-guard.js +8 -5
- package/build/core/settlement/petition-preview.js +68 -56
- package/build/core/settlement/petition-window.js +50 -0
- package/build/core/settlement/petition.js +252 -131
- package/build/core/settlement/queue-read-model.js +72 -95
- package/build/core/settlement/queue-seat-allocation.js +35 -126
- package/build/core/settlement/queue.js +1 -1
- package/build/core/settlement/settlement.js +172 -80
- package/build/core/settlement/verdict.js +112 -66
- package/build/core/settlement/verification-coordination.js +305 -0
- package/build/core/settlement/verification-supervisor.js +275 -0
- package/build/core/settlement/verification.js +398 -31
- package/build/core/status/board.js +125 -127
- package/build/core/status/lifecycle.js +7 -80
- package/build/core/structured-query.js +23 -0
- package/build/core/target-ref.js +1 -1
- package/build/core/task/board.js +59 -24
- package/build/core/task/commands.js +181 -33
- package/build/core/task/coordinate.js +56 -0
- package/build/core/task/document.js +25 -9
- package/build/core/task/index.js +7 -4
- package/build/core/task/query.js +17 -27
- package/build/core/task/settlement-git.js +164 -179
- package/build/core/task/settlement-policy.js +15 -11
- package/build/core/task/source-board.js +31 -13
- package/build/core/task/task-bind-preparation.js +193 -0
- package/build/core/task/task-contract.js +102 -193
- package/build/core/task/task-file-transaction.js +171 -0
- package/build/core/task/task-git-runtime.js +76 -14
- package/build/core/task/task-git-store.js +68 -169
- package/build/core/task/task-store-repository.js +13 -6
- package/build/core/task/task-worktree.js +6 -33
- package/build/core/task/task.js +1 -1
- package/build/core/task/tree.js +3 -3
- package/build/core/task/validation.js +4 -3
- package/build/core/transcripts.js +93 -1
- package/build/core/worktree-bootstrap.js +44 -36
- package/build/core/worktree-path.js +210 -77
- package/build/flow-error.js +7 -7
- package/build/generated/version.js +2 -2
- package/build/git/core.js +76 -13
- package/build/git/refs.js +168 -46
- package/build/git/streaming-batch.js +73 -13
- package/package.json +1 -1
- package/skills/keiyaku/SKILL.md +1 -1
- package/skills/keiyaku-akuma/SKILL.md +3 -3
- package/skills/keiyaku-task/SKILL.md +4 -2
- package/skills/keiyaku-workflow/SKILL.md +88 -203
- package/build/core/entry.js +0 -305
- package/build/core/ledger-batch.js +0 -194
- package/build/core/ledger.js +0 -84
- package/build/core/projection/leash.js +0 -87
- package/build/core/ref-log.js +0 -119
- package/build/core/renew-session.js +0 -128
- package/build/core/status/ledger-batch.js +0 -1
- package/build/core/status/reconciliation.js +0 -137
package/build/flow-error.js
CHANGED
|
@@ -64,6 +64,8 @@ export function resolveFlowErrorHints(code, context, facts) {
|
|
|
64
64
|
return [`Next: open an arc with \`${context.arcToolName}\`, then use \`${context.renewToolName}\` when delivery must follow the current base, or \`${context.closeToolName}\` to settle it.`];
|
|
65
65
|
case "CONTRACT_AMBIGUOUS":
|
|
66
66
|
return ["Address a specific contract workspace, or use `call --bare` to run without contract attachment."];
|
|
67
|
+
case "TASK_AMBIGUOUS":
|
|
68
|
+
return ["Pass one complete task coordinate from the listed candidates."];
|
|
67
69
|
case "UNKNOWN_COMMAND":
|
|
68
70
|
return ["Run `keiyaku --help` to inspect available commands."];
|
|
69
71
|
case "EMPTY_PARAM":
|
|
@@ -77,13 +79,11 @@ export function resolveFlowErrorHints(code, context, facts) {
|
|
|
77
79
|
case "MISSING_PROTOCOL_FILES":
|
|
78
80
|
return ["Required protocol file is missing (`KEIYAKU.md`)."];
|
|
79
81
|
case "CLOSE_MERGE_CONFLICT":
|
|
80
|
-
return [
|
|
82
|
+
return [];
|
|
81
83
|
case "RENEW_COORDINATE_DRIFT":
|
|
82
84
|
return [`Restore the delivery to its ledger-recorded history with Git reflog, then retry \`keiyaku ${context.renewToolName}\`.`];
|
|
83
|
-
case "
|
|
84
|
-
return [`
|
|
85
|
-
case "RENEW_SESSION_NOT_FOUND":
|
|
86
|
-
return [`No recovery session is open; run \`keiyaku ${context.renewToolName}\` normally.`];
|
|
85
|
+
case "RENEW_SCRATCH_NOT_FOUND":
|
|
86
|
+
return [`No renew conflict scratch is open; run \`keiyaku ${context.renewToolName}\` normally.`];
|
|
87
87
|
case "CLOSE_QUALITY_GATE_FAILED":
|
|
88
88
|
return [`CLAIM denied: a configured verification command failed. Use \`${context.arcToolName}\` after addressing the failure before petitioning again.`];
|
|
89
89
|
case "INVALID_BRANCH_TITLE":
|
|
@@ -95,9 +95,9 @@ export function resolveFlowErrorHints(code, context, facts) {
|
|
|
95
95
|
case "FROM_DRAFT_NOT_FOUND":
|
|
96
96
|
return ["The path in `from_draft` does not exist. `from_draft` must reference an existing file path (absolute or relative to `cwd`)."];
|
|
97
97
|
case "INVALID_KEIYAKU_DRAFT":
|
|
98
|
-
return ["Invalid draft/input. Use a `# <title>` heading with non-empty `## Objective
|
|
98
|
+
return ["Invalid draft/input. Use a `# <title>` heading with non-empty `## Context`, `## Objective`, `## Design`, `## Scope`, `## Criteria`, and `## Verification` sections (or the equivalent shape in `from_draft`)."];
|
|
99
99
|
case "BIND_TASK_INCOMPLETE":
|
|
100
|
-
return ["Task supplies
|
|
100
|
+
return ["Task supplies title/objective defaults; pipe only the missing sections as partial contract Markdown on stdin."];
|
|
101
101
|
case "KEIYAKU_FILE_EXISTS":
|
|
102
102
|
return [`\`${context.bindToolName}\` requires a clean slate. \`KEIYAKU.md\` must be removed before starting.`];
|
|
103
103
|
case "DETACHED_HEAD":
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated into build output by scripts/build.mjs
|
|
2
|
-
export const VERSION = "2.9.
|
|
3
|
-
export const GIT_HASH = "
|
|
2
|
+
export const VERSION = "2.9.10";
|
|
3
|
+
export const GIT_HASH = "b4c38cc";
|
package/build/git/core.js
CHANGED
|
@@ -4,6 +4,28 @@ import { normalizeFilesystemCoordinate } from "../fs/path-coordinate.js";
|
|
|
4
4
|
import { getConfig } from "../config/env.js";
|
|
5
5
|
import { appendDebugBlock } from "../telemetry/debug-log.js";
|
|
6
6
|
const GIT_ERROR_COMPACT_MAX_CHARS = 4000;
|
|
7
|
+
export const GIT_ABORT_FORCE_KILL_MS = 250;
|
|
8
|
+
/** Terminate one non-detached Git child and wait until Node drains its stdio close. */
|
|
9
|
+
export async function terminateGitChild(child, closed, graceMs = GIT_ABORT_FORCE_KILL_MS) {
|
|
10
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
11
|
+
child.kill("SIGTERM");
|
|
12
|
+
let grace;
|
|
13
|
+
const exited = await Promise.race([
|
|
14
|
+
closed.then(() => true),
|
|
15
|
+
new Promise((resolve) => {
|
|
16
|
+
grace = setTimeout(() => resolve(false), graceMs);
|
|
17
|
+
grace.unref();
|
|
18
|
+
}),
|
|
19
|
+
]);
|
|
20
|
+
if (grace)
|
|
21
|
+
clearTimeout(grace);
|
|
22
|
+
// Only the close event proves process exit and stdio drain. `killed` and
|
|
23
|
+
// `signalCode` merely report signal delivery and can be set while a handler
|
|
24
|
+
// keeps the child alive.
|
|
25
|
+
if (!exited)
|
|
26
|
+
child.kill("SIGKILL");
|
|
27
|
+
await closed;
|
|
28
|
+
}
|
|
7
29
|
export const MISSING_GIT_CONFIG_PATTERNS = ["No such section or key"];
|
|
8
30
|
export const NOT_GIT_REPOSITORY_PATTERNS = ["not a git repository"];
|
|
9
31
|
export const MISSING_HEAD_PATTERNS = [
|
|
@@ -12,6 +34,10 @@ export const MISSING_HEAD_PATTERNS = [
|
|
|
12
34
|
"unknown revision or path not in the working tree",
|
|
13
35
|
"bad revision 'HEAD'",
|
|
14
36
|
];
|
|
37
|
+
/** Accept only a complete object id from either supported Git object format. */
|
|
38
|
+
export function isFullGitObjectId(value) {
|
|
39
|
+
return /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(value);
|
|
40
|
+
}
|
|
15
41
|
/** Git is an optional adapter for directory-local product surfaces. */
|
|
16
42
|
export function isGitExecutableUnavailable(error) {
|
|
17
43
|
const source = (error ?? {});
|
|
@@ -94,11 +120,18 @@ export function createGit(cwd) {
|
|
|
94
120
|
const git = simpleGit(normalizeFilesystemCoordinate(cwd), {
|
|
95
121
|
timeout: { block: timeoutMs, stdErr: true, stdOut: true },
|
|
96
122
|
});
|
|
97
|
-
git
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
git.env(
|
|
123
|
+
// simple-git replaces its child environment when configured through env().
|
|
124
|
+
// Preserve trace controls explicitly without admitting unrelated ambient Git
|
|
125
|
+
// behavior such as a pager into command execution.
|
|
126
|
+
const traceEnvironment = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.startsWith("GIT_TRACE2")));
|
|
127
|
+
git.env({
|
|
128
|
+
...traceEnvironment,
|
|
129
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
130
|
+
GCM_INTERACTIVE: "Never",
|
|
131
|
+
GIT_MERGE_AUTOEDIT: "no",
|
|
132
|
+
LC_ALL: "C",
|
|
133
|
+
PATH: process.env.PATH ?? "",
|
|
134
|
+
});
|
|
102
135
|
return git;
|
|
103
136
|
}
|
|
104
137
|
/**
|
|
@@ -107,7 +140,8 @@ export function createGit(cwd) {
|
|
|
107
140
|
* soft status codes or stdin (simple-git drops empty-stderr non-zero exits).
|
|
108
141
|
* Long-lived multi-request stdin/stdout sessions use `openGitStreamingBatch`.
|
|
109
142
|
*/
|
|
110
|
-
export async function
|
|
143
|
+
export async function runGitProcessBytes(cwd, args, options) {
|
|
144
|
+
options?.signal?.throwIfAborted();
|
|
111
145
|
const timeoutMs = resolveGitTimeoutMs();
|
|
112
146
|
const env = gitProcessEnv(options?.env);
|
|
113
147
|
return await new Promise((resolve) => {
|
|
@@ -121,19 +155,35 @@ export async function runGitProcess(cwd, args, options) {
|
|
|
121
155
|
const stderrChunks = [];
|
|
122
156
|
let settled = false;
|
|
123
157
|
let timedOut = false;
|
|
158
|
+
let aborting = false;
|
|
159
|
+
let terminationPromise;
|
|
160
|
+
const closed = new Promise((resolveClosed) => child.once("close", () => resolveClosed()));
|
|
124
161
|
const finish = (result) => {
|
|
125
162
|
if (settled)
|
|
126
163
|
return;
|
|
127
164
|
settled = true;
|
|
128
165
|
clearTimeout(timeout);
|
|
166
|
+
options?.signal?.removeEventListener("abort", abort);
|
|
129
167
|
resolve(result);
|
|
130
168
|
};
|
|
169
|
+
const abort = () => {
|
|
170
|
+
if (aborting)
|
|
171
|
+
return;
|
|
172
|
+
aborting = true;
|
|
173
|
+
terminationPromise = terminateGitChild(child, closed);
|
|
174
|
+
};
|
|
175
|
+
options?.signal?.addEventListener("abort", abort, { once: true });
|
|
176
|
+
if (options?.signal?.aborted)
|
|
177
|
+
abort();
|
|
131
178
|
const timeout = setTimeout(() => {
|
|
132
179
|
timedOut = true;
|
|
133
180
|
child.kill("SIGKILL");
|
|
134
181
|
}, timeoutMs);
|
|
135
182
|
if (options?.input !== undefined) {
|
|
136
|
-
|
|
183
|
+
if (typeof options.input === "string")
|
|
184
|
+
child.stdin.end(options.input, "utf8");
|
|
185
|
+
else
|
|
186
|
+
child.stdin.end(options.input);
|
|
137
187
|
}
|
|
138
188
|
else {
|
|
139
189
|
child.stdin.end();
|
|
@@ -142,15 +192,17 @@ export async function runGitProcess(cwd, args, options) {
|
|
|
142
192
|
child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
|
143
193
|
child.on("error", (error) => {
|
|
144
194
|
finish({
|
|
145
|
-
stdout: Buffer.concat(stdoutChunks)
|
|
146
|
-
stderr: Buffer.concat(stderrChunks)
|
|
195
|
+
stdout: Buffer.concat(stdoutChunks),
|
|
196
|
+
stderr: Buffer.concat(stderrChunks),
|
|
147
197
|
status: null,
|
|
148
198
|
error,
|
|
149
199
|
});
|
|
150
200
|
});
|
|
151
|
-
child.on("close", (code, signal) => {
|
|
152
|
-
|
|
153
|
-
|
|
201
|
+
child.on("close", async (code, signal) => {
|
|
202
|
+
if (terminationPromise)
|
|
203
|
+
await terminationPromise;
|
|
204
|
+
const stdout = Buffer.concat(stdoutChunks);
|
|
205
|
+
const stderr = Buffer.concat(stderrChunks);
|
|
154
206
|
if (timedOut) {
|
|
155
207
|
finish({
|
|
156
208
|
stdout,
|
|
@@ -165,7 +217,9 @@ export async function runGitProcess(cwd, args, options) {
|
|
|
165
217
|
stdout,
|
|
166
218
|
stderr,
|
|
167
219
|
status: null,
|
|
168
|
-
error:
|
|
220
|
+
error: options?.signal?.aborted
|
|
221
|
+
? options.signal.reason instanceof Error ? options.signal.reason : new DOMException("The operation was aborted", "AbortError")
|
|
222
|
+
: new Error(`git exited from signal ${signal}`),
|
|
169
223
|
});
|
|
170
224
|
return;
|
|
171
225
|
}
|
|
@@ -173,3 +227,12 @@ export async function runGitProcess(cwd, args, options) {
|
|
|
173
227
|
});
|
|
174
228
|
});
|
|
175
229
|
}
|
|
230
|
+
export async function runGitProcess(cwd, args, options) {
|
|
231
|
+
const result = await runGitProcessBytes(cwd, args, options);
|
|
232
|
+
return {
|
|
233
|
+
stdout: result.stdout.toString("utf8"),
|
|
234
|
+
stderr: result.stderr.toString("utf8"),
|
|
235
|
+
status: result.status,
|
|
236
|
+
...(result.error ? { error: result.error } : {}),
|
|
237
|
+
};
|
|
238
|
+
}
|
package/build/git/refs.js
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
const ZERO_OBJECT_ID = "0000000000000000000000000000000000000000";
|
|
1
|
+
import { isFullGitObjectId, runGitProcess, wrapGitError, } from "./core.js";
|
|
3
2
|
/** Normalize a configured branch to the sole local-branch ref spelling. */
|
|
4
3
|
export function normalizeLocalBranchRef(branch) {
|
|
5
4
|
return branch.startsWith("refs/heads/") ? branch : `refs/heads/${branch}`;
|
|
6
5
|
}
|
|
6
|
+
export const REF_TRANSACTION_CAS_MISMATCH = "REF_TRANSACTION_CAS_MISMATCH";
|
|
7
|
+
export const REF_TRANSACTION_NOT_COMMITTED = "REF_TRANSACTION_NOT_COMMITTED";
|
|
8
|
+
export const REF_TRANSACTION_INDETERMINATE = "REF_TRANSACTION_INDETERMINATE";
|
|
9
|
+
export class RefTransactionError extends Error {
|
|
10
|
+
code = "REF_TRANSACTION_FAILED";
|
|
11
|
+
constructor(message, options) {
|
|
12
|
+
super(message, options);
|
|
13
|
+
this.name = "RefTransactionError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
7
16
|
function isMissingExactRef(result) {
|
|
8
|
-
return result.
|
|
17
|
+
return !result.error
|
|
18
|
+
&& result.stdout === ""
|
|
19
|
+
&& result.status === 1;
|
|
9
20
|
}
|
|
10
21
|
function parseShowRefRows(output) {
|
|
11
22
|
const lines = output.split(/\r?\n/);
|
|
@@ -15,14 +26,18 @@ function parseShowRefRows(output) {
|
|
|
15
26
|
return null;
|
|
16
27
|
const rows = [];
|
|
17
28
|
for (const line of lines) {
|
|
18
|
-
const
|
|
19
|
-
|
|
29
|
+
const separator = line.indexOf(" ");
|
|
30
|
+
const objectId = separator < 0 ? "" : line.slice(0, separator);
|
|
31
|
+
const ref = separator < 0 ? "" : line.slice(separator + 1);
|
|
32
|
+
if (!isFullGitObjectId(objectId) || !/^refs\/\S+$/.test(ref))
|
|
20
33
|
return null;
|
|
21
|
-
rows.push({ objectId
|
|
34
|
+
rows.push({ objectId, ref });
|
|
22
35
|
}
|
|
23
36
|
return rows;
|
|
24
37
|
}
|
|
25
38
|
function throwGitProcessFailure(commandLabel, result, cwd) {
|
|
39
|
+
if (result.error?.name === "AbortError")
|
|
40
|
+
throw result.error;
|
|
26
41
|
const failure = result.error ?? Object.assign(new Error(result.stderr || `git ${commandLabel} exited ${result.status}`), {
|
|
27
42
|
stderr: result.stderr,
|
|
28
43
|
stdout: result.stdout,
|
|
@@ -46,58 +61,165 @@ export async function resolveLocalBranchHead(cwd, branch, run = runGitProcess) {
|
|
|
46
61
|
}
|
|
47
62
|
return throwGitProcessFailure(`show-ref ${ref}`, result, cwd);
|
|
48
63
|
}
|
|
49
|
-
async function
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
async function validateTransactionOperation(cwd, operation, objectIdLength) {
|
|
65
|
+
const refCheck = await runGitProcess(cwd, ["check-ref-format", operation.ref]);
|
|
66
|
+
if (refCheck.status !== 0 || refCheck.error) {
|
|
67
|
+
throw new RefTransactionError(`invalid transaction ref ${JSON.stringify(operation.ref)}`, { cause: refCheck.error });
|
|
53
68
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
const objectIds = operation.kind === "update"
|
|
70
|
+
? [["new", operation.newOid], ["old", operation.expectedOldOid]]
|
|
71
|
+
: [["old", operation.expectedOldOid]];
|
|
72
|
+
for (const [label, oid] of objectIds) {
|
|
73
|
+
if (oid !== null && (oid.length !== objectIdLength || !isFullGitObjectId(oid))) {
|
|
74
|
+
throw new RefTransactionError(`invalid ${label} object ID for ${operation.ref}`);
|
|
75
|
+
}
|
|
58
76
|
}
|
|
59
77
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
function formatTransactionOperation(operation, zero) {
|
|
79
|
+
if (operation.kind === "delete")
|
|
80
|
+
return `delete ${operation.ref} ${operation.expectedOldOid}`;
|
|
81
|
+
if (operation.kind === "verify")
|
|
82
|
+
return `verify ${operation.ref} ${operation.expectedOldOid ?? zero}`;
|
|
83
|
+
return `update ${operation.ref} ${operation.newOid} ${operation.expectedOldOid ?? zero}`;
|
|
84
|
+
}
|
|
85
|
+
function isUnambiguousCasRefusal(result) {
|
|
86
|
+
if (result.status === null || result.status === 0 || result.error)
|
|
87
|
+
return false;
|
|
88
|
+
return /cannot lock ref '[^']+': (?:is at [0-9a-f]+ but expected [0-9a-f]+|reference already exists|reference is missing but expected [0-9a-f]+)/i.test(result.stderr);
|
|
89
|
+
}
|
|
90
|
+
function isUnableToResolveReference(result) {
|
|
91
|
+
return result.status !== null
|
|
92
|
+
&& result.status !== 0
|
|
93
|
+
&& !result.error
|
|
94
|
+
&& /cannot lock ref '[^']+': unable to resolve reference/i.test(result.stderr);
|
|
95
|
+
}
|
|
96
|
+
function hasConclusiveMissingExpectedReference(observations) {
|
|
97
|
+
return observations.some((observation) => (observation.expectedOldOid !== null
|
|
98
|
+
&& observation.observed.status === "absent"));
|
|
99
|
+
}
|
|
100
|
+
function transactionFailureCause(result) {
|
|
101
|
+
return result.stderr.trim() || result.error?.message || `exit ${String(result.status)}`;
|
|
102
|
+
}
|
|
103
|
+
function requestedNewOid(operation) {
|
|
104
|
+
if (operation.kind === "update")
|
|
105
|
+
return operation.newOid;
|
|
106
|
+
if (operation.kind === "delete")
|
|
107
|
+
return null;
|
|
108
|
+
return operation.expectedOldOid;
|
|
109
|
+
}
|
|
110
|
+
function observationMatches(observation, expectedOid) {
|
|
111
|
+
if (observation.observed.status === "unknown")
|
|
112
|
+
return false;
|
|
113
|
+
if (expectedOid === null)
|
|
114
|
+
return observation.observed.status === "absent";
|
|
115
|
+
return observation.observed.status === "present" && observation.observed.objectId === expectedOid;
|
|
116
|
+
}
|
|
117
|
+
async function observeExactRef(cwd, ref) {
|
|
69
118
|
try {
|
|
70
|
-
await
|
|
119
|
+
const symbolic = await runGitProcess(cwd, ["symbolic-ref", "--quiet", ref]);
|
|
120
|
+
if (symbolic.status === 0 && !symbolic.error) {
|
|
121
|
+
const target = symbolic.stdout.trim();
|
|
122
|
+
return { status: "unknown", cause: `symbolic ref targets ${target || "an unreported ref"}` };
|
|
123
|
+
}
|
|
124
|
+
if (symbolic.error || (symbolic.status !== 1 && symbolic.status !== 0)) {
|
|
125
|
+
const cause = symbolic.stderr.trim() || symbolic.error?.message || `symbolic-ref exited ${String(symbolic.status)}`;
|
|
126
|
+
return { status: "unknown", cause };
|
|
127
|
+
}
|
|
128
|
+
const exact = await runGitProcess(cwd, ["show-ref", ref]);
|
|
129
|
+
if (exact.status === 0 && !exact.error) {
|
|
130
|
+
const exactRows = parseShowRefRows(exact.stdout)?.filter((row) => row.ref === ref);
|
|
131
|
+
if (exactRows?.length === 1) {
|
|
132
|
+
return { status: "present", objectId: exactRows[0].objectId };
|
|
133
|
+
}
|
|
134
|
+
return { status: "unknown", cause: `show-ref returned an invalid exact value for ${ref}` };
|
|
135
|
+
}
|
|
136
|
+
if (!exact.error && exact.status === 1 && exact.stdout === "")
|
|
137
|
+
return { status: "absent" };
|
|
138
|
+
const cause = exact.stderr.trim() || exact.error?.message || `show-ref exited ${String(exact.status)}`;
|
|
139
|
+
return { status: "unknown", cause };
|
|
71
140
|
}
|
|
72
141
|
catch (error) {
|
|
73
|
-
|
|
74
|
-
if (await resolveRefOrNull(cwd, ref) === objectId)
|
|
75
|
-
return;
|
|
76
|
-
throw wrapGitError(`update-ref ${ref}`, error, cwd);
|
|
142
|
+
return { status: "unknown", cause: error instanceof Error ? error.message : String(error) };
|
|
77
143
|
}
|
|
78
144
|
}
|
|
79
|
-
|
|
80
|
-
|
|
145
|
+
async function observeTransactionRefs(cwd, operations) {
|
|
146
|
+
return await Promise.all(operations.map(async (operation) => ({
|
|
147
|
+
ref: operation.ref,
|
|
148
|
+
expectedOldOid: operation.expectedOldOid,
|
|
149
|
+
requestedNewOid: requestedNewOid(operation),
|
|
150
|
+
observed: await observeExactRef(cwd, operation.ref),
|
|
151
|
+
})));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Atomically apply a closed set of ref operations. Every old-OID assertion is
|
|
155
|
+
* submitted inside the same update-ref transaction as its operation; no
|
|
156
|
+
* preflight read is used as authority.
|
|
157
|
+
*/
|
|
158
|
+
export async function commitRefTransaction(cwd, operations, objectIdLength, options = {}) {
|
|
159
|
+
if (operations.length === 0)
|
|
160
|
+
throw new RefTransactionError("a ref transaction requires at least one operation");
|
|
161
|
+
const seen = new Set();
|
|
162
|
+
for (const operation of operations) {
|
|
163
|
+
await validateTransactionOperation(cwd, operation, objectIdLength);
|
|
164
|
+
if (seen.has(operation.ref))
|
|
165
|
+
throw new RefTransactionError(`duplicate transaction ref ${operation.ref}`);
|
|
166
|
+
seen.add(operation.ref);
|
|
167
|
+
}
|
|
168
|
+
const zero = "0".repeat(objectIdLength);
|
|
169
|
+
const input = [
|
|
170
|
+
"start",
|
|
171
|
+
...operations.map((operation) => formatTransactionOperation(operation, zero)),
|
|
172
|
+
"prepare",
|
|
173
|
+
"commit",
|
|
174
|
+
"",
|
|
175
|
+
].join("\n");
|
|
176
|
+
let result;
|
|
81
177
|
try {
|
|
82
|
-
|
|
83
|
-
if (existing === null)
|
|
84
|
-
return { ref, status: "already-absent" };
|
|
85
|
-
if (existing !== objectId) {
|
|
86
|
-
return {
|
|
87
|
-
ref,
|
|
88
|
-
status: "retained",
|
|
89
|
-
diagnostic: `ref resolves to ${existing}, not expected object ${objectId}`,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
const git = createGit(cwd);
|
|
93
|
-
await git.raw(["update-ref", "-d", ref, objectId]);
|
|
94
|
-
return { ref, status: "removed" };
|
|
178
|
+
result = await (options.run ?? runGitProcess)(cwd, ["update-ref", "--no-deref", "--stdin"], { input });
|
|
95
179
|
}
|
|
96
180
|
catch (error) {
|
|
181
|
+
result = {
|
|
182
|
+
stdout: "",
|
|
183
|
+
stderr: "",
|
|
184
|
+
status: null,
|
|
185
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (result.status === 0 && !result.error) {
|
|
189
|
+
return { status: "committed", proof: "process-success", operations, observations: [] };
|
|
190
|
+
}
|
|
191
|
+
const observations = await observeTransactionRefs(cwd, operations);
|
|
192
|
+
const allPre = observations.every((observation) => observationMatches(observation, observation.expectedOldOid));
|
|
193
|
+
const allPost = observations.every((observation) => observationMatches(observation, observation.requestedNewOid));
|
|
194
|
+
const changesAuthority = operations.some((operation) => requestedNewOid(operation) !== operation.expectedOldOid);
|
|
195
|
+
const cause = transactionFailureCause(result);
|
|
196
|
+
if (allPost && changesAuthority) {
|
|
197
|
+
return { status: "committed", proof: "observed-post-state", operations, observations };
|
|
198
|
+
}
|
|
199
|
+
if (isUnambiguousCasRefusal(result)
|
|
200
|
+
|| (isUnableToResolveReference(result) && hasConclusiveMissingExpectedReference(observations))) {
|
|
201
|
+
return {
|
|
202
|
+
status: "refused",
|
|
203
|
+
code: REF_TRANSACTION_CAS_MISMATCH,
|
|
204
|
+
operations,
|
|
205
|
+
observations,
|
|
206
|
+
cause,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (allPre && changesAuthority) {
|
|
97
210
|
return {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
211
|
+
status: "refused",
|
|
212
|
+
code: REF_TRANSACTION_NOT_COMMITTED,
|
|
213
|
+
operations,
|
|
214
|
+
observations,
|
|
215
|
+
cause,
|
|
101
216
|
};
|
|
102
217
|
}
|
|
218
|
+
return {
|
|
219
|
+
status: "indeterminate",
|
|
220
|
+
code: REF_TRANSACTION_INDETERMINATE,
|
|
221
|
+
operations,
|
|
222
|
+
observations,
|
|
223
|
+
cause,
|
|
224
|
+
};
|
|
103
225
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { isFullGitObjectId } from "./core.js";
|
|
2
3
|
import { normalizeFilesystemCoordinate } from "../fs/path-coordinate.js";
|
|
3
|
-
import { gitProcessEnv, resolveGitTimeoutMs, } from "./core.js";
|
|
4
|
+
import { gitProcessEnv, resolveGitTimeoutMs, terminateGitChild, } from "./core.js";
|
|
4
5
|
/** Default long-lived batch argv. One-shot calls must not use this gate. */
|
|
5
6
|
export const GIT_STREAMING_BATCH_DEFAULT_ARGS = ["cat-file", "--batch"];
|
|
6
7
|
/** Safety cap for a single response header line before the body frame. */
|
|
@@ -15,10 +16,12 @@ export class GitStreamingBatchResponseEndedError extends Error {
|
|
|
15
16
|
}
|
|
16
17
|
class BufferedReader {
|
|
17
18
|
#iterator;
|
|
19
|
+
#onProgress;
|
|
18
20
|
#buffer = Buffer.alloc(0);
|
|
19
21
|
#ended = false;
|
|
20
|
-
constructor(stream) {
|
|
22
|
+
constructor(stream, onProgress) {
|
|
21
23
|
this.#iterator = stream[Symbol.asyncIterator]();
|
|
24
|
+
this.#onProgress = onProgress;
|
|
22
25
|
}
|
|
23
26
|
async #fill() {
|
|
24
27
|
if (this.#ended) {
|
|
@@ -30,6 +33,7 @@ class BufferedReader {
|
|
|
30
33
|
throw new GitStreamingBatchResponseEndedError("git streaming batch response ended unexpectedly");
|
|
31
34
|
}
|
|
32
35
|
const chunk = Buffer.isBuffer(next.value) ? next.value : Buffer.from(next.value);
|
|
36
|
+
this.#onProgress();
|
|
33
37
|
this.#buffer = this.#buffer.length === 0 ? chunk : Buffer.concat([this.#buffer, chunk]);
|
|
34
38
|
}
|
|
35
39
|
async readLine(maxBytes) {
|
|
@@ -84,20 +88,36 @@ export class GitStreamingBatchSession {
|
|
|
84
88
|
#child;
|
|
85
89
|
#reader;
|
|
86
90
|
#stderr = [];
|
|
87
|
-
#
|
|
91
|
+
#timeoutMs;
|
|
88
92
|
#closed;
|
|
89
93
|
#maxHeaderBytes;
|
|
94
|
+
#signal;
|
|
95
|
+
#abortListener;
|
|
96
|
+
#timeout = null;
|
|
90
97
|
#state = { kind: "open" };
|
|
91
98
|
#timedOut = false;
|
|
92
99
|
#aborted = false;
|
|
93
100
|
#inputError = null;
|
|
94
101
|
#requestInFlight = false;
|
|
102
|
+
#requestDone = null;
|
|
103
|
+
#finishRequest = null;
|
|
104
|
+
#requestTimeoutOwner = null;
|
|
95
105
|
#closePromise = null;
|
|
96
|
-
|
|
106
|
+
#terminationPromise = null;
|
|
107
|
+
constructor(cwd, child, timeoutMs, maxHeaderBytes, signal) {
|
|
97
108
|
this.cwd = cwd;
|
|
98
109
|
this.#child = child;
|
|
99
|
-
this.#
|
|
110
|
+
this.#timeoutMs = timeoutMs;
|
|
111
|
+
this.#reader = new BufferedReader(this.#child.stdout, () => {
|
|
112
|
+
if (this.#requestTimeoutOwner)
|
|
113
|
+
this.#armTimeout(this.#requestTimeoutOwner);
|
|
114
|
+
});
|
|
100
115
|
this.#maxHeaderBytes = maxHeaderBytes;
|
|
116
|
+
this.#signal = signal;
|
|
117
|
+
this.#abortListener = () => this.abort();
|
|
118
|
+
signal?.addEventListener("abort", this.#abortListener, { once: true });
|
|
119
|
+
if (signal?.aborted)
|
|
120
|
+
this.abort();
|
|
101
121
|
this.#child.stderr.on("data", (chunk) => this.#stderr.push(chunk));
|
|
102
122
|
this.#child.stdin.on("error", (error) => {
|
|
103
123
|
this.#inputError = error;
|
|
@@ -110,10 +130,27 @@ export class GitStreamingBatchSession {
|
|
|
110
130
|
resolve({ code, signal });
|
|
111
131
|
});
|
|
112
132
|
});
|
|
113
|
-
|
|
133
|
+
}
|
|
134
|
+
#armTimeout(owner) {
|
|
135
|
+
this.#clearTimeout();
|
|
136
|
+
const handle = setTimeout(() => {
|
|
137
|
+
if (this.#timeout?.owner !== owner)
|
|
138
|
+
return;
|
|
139
|
+
this.#timeout = null;
|
|
114
140
|
this.#timedOut = true;
|
|
141
|
+
if (this.#state.kind === "open")
|
|
142
|
+
this.#state = { kind: "closing" };
|
|
115
143
|
this.#child.kill("SIGKILL");
|
|
116
|
-
}, timeoutMs);
|
|
144
|
+
}, this.#timeoutMs);
|
|
145
|
+
this.#timeout = { owner, handle };
|
|
146
|
+
}
|
|
147
|
+
#clearTimeout(owner) {
|
|
148
|
+
if (this.#timeout === null)
|
|
149
|
+
return;
|
|
150
|
+
if (owner !== undefined && this.#timeout.owner !== owner)
|
|
151
|
+
return;
|
|
152
|
+
clearTimeout(this.#timeout.handle);
|
|
153
|
+
this.#timeout = null;
|
|
117
154
|
}
|
|
118
155
|
/**
|
|
119
156
|
* Open the sole long-lived Git batch process for `cwd`.
|
|
@@ -125,11 +162,11 @@ export class GitStreamingBatchSession {
|
|
|
125
162
|
const maxHeaderBytes = options?.maxHeaderBytes ?? GIT_STREAMING_BATCH_MAX_HEADER_BYTES;
|
|
126
163
|
const child = spawn("git", [...args], {
|
|
127
164
|
cwd: normalizeFilesystemCoordinate(cwd),
|
|
128
|
-
env: gitProcessEnv(),
|
|
165
|
+
env: gitProcessEnv(options?.env),
|
|
129
166
|
shell: false,
|
|
130
167
|
stdio: ["pipe", "pipe", "pipe"],
|
|
131
168
|
});
|
|
132
|
-
return new GitStreamingBatchSession(cwd, child, timeoutMs, maxHeaderBytes);
|
|
169
|
+
return new GitStreamingBatchSession(cwd, child, timeoutMs, maxHeaderBytes, options?.signal);
|
|
133
170
|
}
|
|
134
171
|
/**
|
|
135
172
|
* Write one request line and read one framed response record.
|
|
@@ -142,7 +179,13 @@ export class GitStreamingBatchSession {
|
|
|
142
179
|
if (this.#requestInFlight) {
|
|
143
180
|
throw new Error("git streaming batch rejects concurrent requests");
|
|
144
181
|
}
|
|
182
|
+
const timeoutOwner = Symbol("git-streaming-batch-request");
|
|
145
183
|
this.#requestInFlight = true;
|
|
184
|
+
this.#requestTimeoutOwner = timeoutOwner;
|
|
185
|
+
this.#requestDone = new Promise((resolve) => {
|
|
186
|
+
this.#finishRequest = resolve;
|
|
187
|
+
});
|
|
188
|
+
this.#armTimeout(timeoutOwner);
|
|
146
189
|
try {
|
|
147
190
|
if (this.#inputError)
|
|
148
191
|
throw this.#inputError;
|
|
@@ -168,7 +211,8 @@ export class GitStreamingBatchSession {
|
|
|
168
211
|
catch (error) {
|
|
169
212
|
if (error instanceof GitStreamingBatchFramingError
|
|
170
213
|
|| error instanceof GitStreamingBatchResponseEndedError) {
|
|
171
|
-
this
|
|
214
|
+
if (!this.#timedOut)
|
|
215
|
+
this.abort();
|
|
172
216
|
}
|
|
173
217
|
throw error;
|
|
174
218
|
}
|
|
@@ -183,7 +227,7 @@ export class GitStreamingBatchSession {
|
|
|
183
227
|
const size = Number(sizeText);
|
|
184
228
|
if (unexpected.length > 0
|
|
185
229
|
|| !oid
|
|
186
|
-
||
|
|
230
|
+
|| !isFullGitObjectId(oid)
|
|
187
231
|
|| !type
|
|
188
232
|
|| !Number.isSafeInteger(size)
|
|
189
233
|
|| size < 0) {
|
|
@@ -221,7 +265,13 @@ export class GitStreamingBatchSession {
|
|
|
221
265
|
};
|
|
222
266
|
}
|
|
223
267
|
finally {
|
|
268
|
+
this.#clearTimeout(timeoutOwner);
|
|
269
|
+
this.#requestTimeoutOwner = null;
|
|
224
270
|
this.#requestInFlight = false;
|
|
271
|
+
const finishRequest = this.#finishRequest;
|
|
272
|
+
this.#finishRequest = null;
|
|
273
|
+
this.#requestDone = null;
|
|
274
|
+
finishRequest?.();
|
|
225
275
|
}
|
|
226
276
|
}
|
|
227
277
|
/**
|
|
@@ -232,11 +282,12 @@ export class GitStreamingBatchSession {
|
|
|
232
282
|
if (this.#aborted)
|
|
233
283
|
return;
|
|
234
284
|
this.#aborted = true;
|
|
285
|
+
this.#clearTimeout();
|
|
235
286
|
if (this.#state.kind === "open") {
|
|
236
287
|
this.#state = { kind: "closing" };
|
|
237
288
|
}
|
|
238
289
|
this.#child.stdin.destroy();
|
|
239
|
-
this.#child
|
|
290
|
+
this.#terminationPromise ??= terminateGitChild(this.#child, this.#closed);
|
|
240
291
|
}
|
|
241
292
|
/**
|
|
242
293
|
* End stdin, await process exit, and return structured close facts.
|
|
@@ -263,7 +314,15 @@ export class GitStreamingBatchSession {
|
|
|
263
314
|
return this.#closePromise;
|
|
264
315
|
}
|
|
265
316
|
async #closeOnce() {
|
|
317
|
+
const pendingRequest = this.#requestDone;
|
|
318
|
+
if (pendingRequest)
|
|
319
|
+
await pendingRequest;
|
|
320
|
+
if (this.#terminationPromise)
|
|
321
|
+
await this.#terminationPromise;
|
|
322
|
+
const timeoutOwner = Symbol("git-streaming-batch-close");
|
|
266
323
|
if (!this.#aborted && this.#child.stdin?.writable) {
|
|
324
|
+
if (!this.#timedOut)
|
|
325
|
+
this.#armTimeout(timeoutOwner);
|
|
267
326
|
this.#child.stdin.end();
|
|
268
327
|
}
|
|
269
328
|
let exit;
|
|
@@ -271,8 +330,9 @@ export class GitStreamingBatchSession {
|
|
|
271
330
|
exit = await this.#closed;
|
|
272
331
|
}
|
|
273
332
|
finally {
|
|
274
|
-
clearTimeout(
|
|
333
|
+
this.#clearTimeout(timeoutOwner);
|
|
275
334
|
}
|
|
335
|
+
this.#signal?.removeEventListener("abort", this.#abortListener);
|
|
276
336
|
if (exit.processError && !this.#aborted) {
|
|
277
337
|
const fact = {
|
|
278
338
|
code: null,
|