@zq-silk/yui 2.23.0 → 2.25.0
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 +9 -4
- package/dist/artifacts/artifactCapability.js +6 -6
- package/dist/artifacts/gitArtifactRef.js +10 -10
- package/dist/artifacts/jobArtifact.js +5 -5
- package/dist/artifacts/managedGit.js +15 -12
- package/dist/artifacts/taskArtifactRepository.js +35 -33
- package/dist/cli.js +9 -2
- package/dist/commands/configCommands.js +64 -1
- package/dist/commands/projectCommands.js +9 -9
- package/dist/commands/taskCompletionGate.js +1 -1
- package/dist/commands/taskIntegrationCommands.js +1 -1
- package/dist/commands/taskPublicationAdoptCommand.js +1 -1
- package/dist/commands/taskUpstreamCommands.js +1 -1
- package/dist/config/configCatalog.js +3 -0
- package/dist/config/gitConfig.js +20 -0
- package/dist/config/yuiConfig.js +1 -0
- package/dist/controlPlaneCli.js +4 -4
- package/dist/controller/jobArtifactControl.js +3 -3
- package/dist/controller/jobDefinitionControl.js +1 -1
- package/dist/integration/gitIntegrationService.js +1 -1
- package/dist/kernel/builtinCapabilities.js +3 -3
- package/dist/repository/gitCommand.js +153 -0
- package/dist/repository/gitCompatibility.js +3 -2
- package/dist/repository/gitWorkspace.js +579 -563
- package/dist/repository/gitWorktreeInventory.js +15 -19
- package/dist/repository/projectRefresh.js +1 -1
- package/dist/repository/taskBaseFreshness.js +1 -4
- package/dist/repository/taskWorkspacePreparer.js +1 -1
- package/dist/runtime/nativeExecutable.js +5 -2
- package/dist/runtime/platform.js +11 -0
- package/dist/storage/sqliteSchema.js +10 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/taskStore.js +7 -1
- package/dist/web/webConversation.js +2 -2
- package/dist/web/webMaterials.js +19 -19
- package/dist/web/webTaskSurface.js +3 -3
- package/dist/workspace/workItemChangeSetManager.js +3 -3
- package/docs/project-refresh.md +3 -2
- package/docs/project-refresh.zh-CN.md +2 -1
- package/docs/release-workflow.md +58 -2
- package/docs/release-workflow.zh-CN.md +45 -1
- package/docs/roles-and-configuration.md +34 -0
- package/docs/roles-and-configuration.zh-CN.md +27 -0
- package/docs/storage-baseline.md +5 -1
- package/docs/storage-baseline.zh-CN.md +3 -1
- package/docs/testing/verification-levels.md +7 -7
- package/docs/testing/verification-levels.zh-CN.md +6 -6
- package/i18n/README.zh-CN.md +8 -4
- package/package.json +1 -1
- package/dist/runtime/native/darwin-x64/claude-process-owner +0 -0
- package/dist/runtime/native/darwin-x64/process-identity +0 -0
|
@@ -20,6 +20,9 @@ export const CONFIG_DEFINITIONS = Object.freeze([
|
|
|
20
20
|
{ key: "resources-quarantine-ttl-hours", domain: "resources", property: "resourcesQuarantineTtlHours", label: "Resource quarantine TTL", summary: "Default observation window before purge, 1-720 hours (default: 24).", takesEffect: "The next resource GC command without an explicit TTL override." },
|
|
21
21
|
{ key: "tmux-bin", domain: "tools", property: "tmuxBin", label: "Tmux bin", summary: "Command or path to the tmux binary (default: tmux).", takesEffect: "The next CLI-owned tmux invocation; restart the Controller for Controller-owned launches." },
|
|
22
22
|
{ key: "tmux-history-limit", domain: "tools", property: "tmuxHistoryLimit", label: "Tmux history limit", summary: "History lines retained for newly created tmux sessions, 1000-1000000 (default: 100000).", takesEffect: "New tmux sessions; existing sessions report drift until recreated." },
|
|
23
|
+
{ key: "git-local-timeout-seconds", domain: "tools", property: "gitLocalTimeoutSeconds", label: "Git local total timeout", summary: "Total elapsed-time limit for each local Git command, 1-86400 seconds (default: 30). Independent of remote transport budgets.", takesEffect: "The next local Git command, including Controller-owned commands; in-flight commands keep their budgets." },
|
|
24
|
+
{ key: "git-remote-inactivity-timeout-seconds", domain: "tools", property: "gitRemoteInactivityTimeoutSeconds", label: "Git remote inactivity timeout", summary: "Maximum silence on stdout/stderr for clone, remote fetch and ls-remote, 1-86400 seconds (default: 120). Local Git uses its separate configurable total budget.", takesEffect: "The next remote Git command, including Controller-owned commands; in-flight commands keep their budgets." },
|
|
25
|
+
{ key: "git-remote-timeout-seconds", domain: "tools", property: "gitRemoteTimeoutSeconds", label: "Git remote total timeout", summary: "Total elapsed-time limit for each clone, remote fetch or ls-remote, 1-86400 seconds (default: 1800), even while progress continues.", takesEffect: "The next remote Git command, including Controller-owned commands; in-flight commands keep their budgets." },
|
|
23
26
|
{ key: "telemetry-enabled", domain: "tools", property: "telemetryEnabled", label: "Diagnostic telemetry", summary: "Enable the optional SQLite diagnostic telemetry projection (default: false); requires a SQLite Home.", takesEffect: "Standalone telemetry commands immediately; scheduler writes after Controller restart." },
|
|
24
27
|
{ key: "telemetry-terminal-keep", domain: "tools", property: "telemetryTerminalKeep", label: "Telemetry terminal keep", summary: "Positive-integer rows retained per terminal AgentRun (default: 200).", takesEffect: "Explicit retention immediately; automatic retention after the Controller restarts." },
|
|
25
28
|
{ key: "telemetry-run-cap", domain: "tools", property: "telemetryRunCap", label: "Telemetry AgentRun cap", summary: `Positive-integer maximum rows retained per AgentRun, at most ${MAX_RUN_CAP} (default: 50000).`, takesEffect: "Explicit retention immediately; automatic write-time capping after the Controller restarts." }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS = 30;
|
|
2
|
+
export const DEFAULT_GIT_REMOTE_INACTIVITY_TIMEOUT_SECONDS = 120;
|
|
3
|
+
export const DEFAULT_GIT_REMOTE_TIMEOUT_SECONDS = 1_800;
|
|
4
|
+
export function resolveGitLocalTimeoutSeconds(value) {
|
|
5
|
+
return timeoutSeconds(value, DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS, "gitLocalTimeoutSeconds");
|
|
6
|
+
}
|
|
7
|
+
export function resolveGitRemoteInactivityTimeoutSeconds(value) {
|
|
8
|
+
return timeoutSeconds(value, DEFAULT_GIT_REMOTE_INACTIVITY_TIMEOUT_SECONDS, "gitRemoteInactivityTimeoutSeconds");
|
|
9
|
+
}
|
|
10
|
+
export function resolveGitRemoteTimeoutSeconds(value) {
|
|
11
|
+
return timeoutSeconds(value, DEFAULT_GIT_REMOTE_TIMEOUT_SECONDS, "gitRemoteTimeoutSeconds");
|
|
12
|
+
}
|
|
13
|
+
function timeoutSeconds(value, fallback, label) {
|
|
14
|
+
if (value === undefined)
|
|
15
|
+
return fallback;
|
|
16
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > 86_400) {
|
|
17
|
+
throw new TypeError(`${label} must be an integer from 1 to 86400.`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
package/dist/config/yuiConfig.js
CHANGED
|
@@ -91,6 +91,7 @@ export const MAX_CONTROLLER_TASK_CONCURRENCY = 32;
|
|
|
91
91
|
export const DEFAULT_AGENT_LAUNCH_INACTIVITY_TIMEOUT_SECONDS = 300;
|
|
92
92
|
export const DEFAULT_DELIVERY_TIMEOUT_SECONDS = 120;
|
|
93
93
|
export const DEFAULT_TMUX_HISTORY_LIMIT = 100_000;
|
|
94
|
+
export { DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS, DEFAULT_GIT_REMOTE_INACTIVITY_TIMEOUT_SECONDS, DEFAULT_GIT_REMOTE_TIMEOUT_SECONDS, resolveGitLocalTimeoutSeconds, resolveGitRemoteInactivityTimeoutSeconds, resolveGitRemoteTimeoutSeconds } from "./gitConfig.js";
|
|
94
95
|
export function resolveRuntimeHealth(configured) {
|
|
95
96
|
if (configured === undefined || configured === null)
|
|
96
97
|
return DEFAULT_RUNTIME_HEALTH_POLICY;
|
package/dist/controlPlaneCli.js
CHANGED
|
@@ -1111,7 +1111,7 @@ export async function main() {
|
|
|
1111
1111
|
if (action === "list") {
|
|
1112
1112
|
if (resolved.length !== 4)
|
|
1113
1113
|
throw usageError(usage);
|
|
1114
|
-
data = await listArtifactsCapability(home, taskId);
|
|
1114
|
+
data = await listArtifactsCapability(home, taskId, () => store.getConfig());
|
|
1115
1115
|
}
|
|
1116
1116
|
else if (action === "read") {
|
|
1117
1117
|
const relativePath = resolved[4];
|
|
@@ -1120,7 +1120,7 @@ export async function main() {
|
|
|
1120
1120
|
throw usageError(usage);
|
|
1121
1121
|
data = await readArtifactCapability(home, taskId, {
|
|
1122
1122
|
relativePath, ...(commit === undefined ? {} : { commit })
|
|
1123
|
-
});
|
|
1123
|
+
}, () => store.getConfig());
|
|
1124
1124
|
}
|
|
1125
1125
|
else {
|
|
1126
1126
|
// save: a delivery-authoritative action; a managed Task caller must be the current Leader.
|
|
@@ -1156,7 +1156,7 @@ export async function main() {
|
|
|
1156
1156
|
relativePath, content, ...(encoding === undefined ? {} : { encoding }),
|
|
1157
1157
|
...(message === undefined ? {} : { message }),
|
|
1158
1158
|
...(expectedHead === undefined ? {} : { expectedHead })
|
|
1159
|
-
});
|
|
1159
|
+
}, () => store.getConfig());
|
|
1160
1160
|
}
|
|
1161
1161
|
emit(JSON.stringify(data, null, 2), false, data);
|
|
1162
1162
|
return;
|
|
@@ -2515,7 +2515,7 @@ async function deltaRecheckPreflightForTaskCommand(args, store, actualTaskReview
|
|
|
2515
2515
|
repositoryPaths,
|
|
2516
2516
|
previousRound: previous,
|
|
2517
2517
|
candidate: actualTaskReviewCandidate,
|
|
2518
|
-
git: new NodeGitWorkspace()
|
|
2518
|
+
git: new NodeGitWorkspace(() => store.getConfig())
|
|
2519
2519
|
});
|
|
2520
2520
|
if (assessment.kind === "ineligible") {
|
|
2521
2521
|
throw usageError(`Delta-recheck is technically unavailable: ${assessment.reason}`);
|
|
@@ -19,7 +19,7 @@ export async function controlJobArtifact(store, home, value, now) {
|
|
|
19
19
|
op: "run-get", scope: input.scope, caller: input.caller, id: input.id, runId: input.runId
|
|
20
20
|
}, now);
|
|
21
21
|
const run = current(); // Both definition and historical Assignment authorization.
|
|
22
|
-
const repo = openJobArtifactRepository(home, jobArtifactOwner(run), runReference(run));
|
|
22
|
+
const repo = openJobArtifactRepository(home, jobArtifactOwner(run), runReference(run), () => store.getConfig());
|
|
23
23
|
if (input.op === "artifact-list") {
|
|
24
24
|
const head = await repo.head();
|
|
25
25
|
const files = head === null ? [] : await repo.list(head);
|
|
@@ -45,7 +45,7 @@ export async function controlJobArtifact(store, home, value, now) {
|
|
|
45
45
|
if (!(run.artifacts ?? []).some(item => isDeepStrictEqual(item, ref))) {
|
|
46
46
|
throw new Error("Artifact reference is not registered for this exact Run.");
|
|
47
47
|
}
|
|
48
|
-
const content = await readJobArtifactReference(home, ref);
|
|
48
|
+
const content = await readJobArtifactReference(home, ref, () => store.getConfig());
|
|
49
49
|
current(); // A replaced Session during I/O must not receive the material.
|
|
50
50
|
return boundedJobArtifactPayload({ reference: ref, ...(input.op === "artifact-preview"
|
|
51
51
|
? previewArtifactContent(content, input.offset) : presentArtifactContent(content)) }, "Artifact response", MAX_JOB_ARTIFACT_RESPONSE_BYTES);
|
|
@@ -79,7 +79,7 @@ export async function controlJobArtifact(store, home, value, now) {
|
|
|
79
79
|
}
|
|
80
80
|
assertCallerAuthorized(store, input.caller, ref.owner.taskId);
|
|
81
81
|
}
|
|
82
|
-
await readJobArtifactReference(home, ref);
|
|
82
|
+
await readJobArtifactReference(home, ref, () => store.getConfig());
|
|
83
83
|
}
|
|
84
84
|
try {
|
|
85
85
|
return store.transaction(tx => {
|
|
@@ -303,7 +303,7 @@ export function authorizeGlobalJobStart(store, run) {
|
|
|
303
303
|
const definition = store.getJobDefinition({ kind: "global" }, run.definitionId);
|
|
304
304
|
if (definition?.builtin)
|
|
305
305
|
requireProjectSyncTarget(store, definition);
|
|
306
|
-
const project = requireRefreshProject({ listProjects: () => projects
|
|
306
|
+
const project = requireRefreshProject({ listProjects: () => projects }, run.projectId, true);
|
|
307
307
|
if (projectRefreshTargetDigest(project) !== projectRefreshDigest({
|
|
308
308
|
kind: "builtin", action: run.action, projectId: run.projectId, input: run.actionInput
|
|
309
309
|
}))
|
|
@@ -64,7 +64,7 @@ export class GitIntegrationService {
|
|
|
64
64
|
environment;
|
|
65
65
|
runtimeIsolation;
|
|
66
66
|
#resourceRegistrarValue;
|
|
67
|
-
constructor(home, store, git = new NodeGitWorkspace(), now = () => new Date(), environment = process.env, runtimeIsolation = defaultIntegrationRuntimeIsolation(home, store.getHomeIdentity().homeId), jobPort) {
|
|
67
|
+
constructor(home, store, git = new NodeGitWorkspace(() => store.getConfig()), now = () => new Date(), environment = process.env, runtimeIsolation = defaultIntegrationRuntimeIsolation(home, store.getHomeIdentity().homeId), jobPort) {
|
|
68
68
|
this.store = store;
|
|
69
69
|
this.git = git;
|
|
70
70
|
this.now = now;
|
|
@@ -324,14 +324,14 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
|
|
|
324
324
|
...(params.encoding === undefined ? {} : { encoding: params.encoding }),
|
|
325
325
|
...(params.message === undefined ? {} : { message: params.message }),
|
|
326
326
|
...(params.expectedHead === undefined ? {} : { expectedHead: params.expectedHead })
|
|
327
|
-
});
|
|
327
|
+
}, () => store.getConfig());
|
|
328
328
|
if (name === "artifact.read")
|
|
329
329
|
return readArtifactCapability(store.rootDirectory(), taskId, {
|
|
330
330
|
relativePath: params.relativePath,
|
|
331
331
|
...(params.commit === undefined ? {} : { commit: params.commit })
|
|
332
|
-
});
|
|
332
|
+
}, () => store.getConfig());
|
|
333
333
|
if (name === "artifact.list")
|
|
334
|
-
return listArtifactsCapability(store.rootDirectory(), taskId);
|
|
334
|
+
return listArtifactsCapability(store.rootDirectory(), taskId, () => store.getConfig());
|
|
335
335
|
if (name === "environment.prepare")
|
|
336
336
|
return resources.prepare(taskId, params.plan);
|
|
337
337
|
if (name === "environment.adopt")
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS } from "../config/gitConfig.js";
|
|
4
|
+
export const LOCAL_GIT_TIMEOUT_MS = DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS * 1_000;
|
|
5
|
+
/** Retain process facts independently of Git's last progress/error output. */
|
|
6
|
+
export class GitCommandError extends Error {
|
|
7
|
+
timeoutKind;
|
|
8
|
+
budget;
|
|
9
|
+
elapsedMs;
|
|
10
|
+
code;
|
|
11
|
+
exitCode;
|
|
12
|
+
signal;
|
|
13
|
+
stdout;
|
|
14
|
+
stderr;
|
|
15
|
+
stderrTruncated;
|
|
16
|
+
constructor(cause, timeoutKind, budget, elapsedMs, output) {
|
|
17
|
+
const failure = cause;
|
|
18
|
+
const code = failure?.code ?? output.exitCode;
|
|
19
|
+
const exitCode = output.exitCode;
|
|
20
|
+
const stderr = output.stderr.toString("utf8").trim();
|
|
21
|
+
const reasons = [
|
|
22
|
+
...(timeoutKind === undefined ? [] : [
|
|
23
|
+
`${timeoutKind === "inactivity" ? "no-progress timeout" : "total timeout"} after `
|
|
24
|
+
+ `${timeoutKind === "inactivity" ? budget.inactivityMs : budget.totalMs} ms`
|
|
25
|
+
]),
|
|
26
|
+
...(exitCode === null ? [] : [`exit code ${exitCode}`]),
|
|
27
|
+
...(output.signal === null ? [] : [`signal ${output.signal}`]),
|
|
28
|
+
...(typeof code === "string" ? [`${code}: ${failure?.message ?? "process error"}`] : [])
|
|
29
|
+
];
|
|
30
|
+
if (reasons.length === 0)
|
|
31
|
+
reasons.push(failure?.message ?? "unknown process failure");
|
|
32
|
+
super(`Git command failed (${reasons.join("; ")}; elapsed ${elapsedMs} ms)`
|
|
33
|
+
+ (stderr.length === 0 ? "." : `: ${output.stderrTruncated ? "[stderr tail; earlier output omitted] " : ""}${stderr}`), { cause });
|
|
34
|
+
this.timeoutKind = timeoutKind;
|
|
35
|
+
this.budget = budget;
|
|
36
|
+
this.elapsedMs = elapsedMs;
|
|
37
|
+
this.name = "GitCommandError";
|
|
38
|
+
this.code = code;
|
|
39
|
+
this.exitCode = exitCode;
|
|
40
|
+
this.signal = output.signal;
|
|
41
|
+
this.stdout = output.stdout;
|
|
42
|
+
this.stderr = stderr;
|
|
43
|
+
this.stderrTruncated = output.stderrTruncated;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One buffered invocation, never retried. Output refreshes only the inactivity
|
|
48
|
+
* clock; the total deadline never moves. At the hard deadline, kill the owned
|
|
49
|
+
* process group (including transport helpers) before reporting completion.
|
|
50
|
+
*/
|
|
51
|
+
export async function executeGitCommand(program, args, options = {}) {
|
|
52
|
+
const budget = options.budget ?? { totalMs: LOCAL_GIT_TIMEOUT_MS };
|
|
53
|
+
for (const value of [budget.totalMs, ...(budget.inactivityMs === undefined ? [] : [budget.inactivityMs])]) {
|
|
54
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 2_147_483_647) {
|
|
55
|
+
throw new TypeError("Git timeout budgets must be positive integer milliseconds within the timer range.");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const started = performance.now();
|
|
59
|
+
const child = spawn(program, args, {
|
|
60
|
+
...(options.environment === undefined ? {} : { env: options.environment }),
|
|
61
|
+
detached: process.platform !== "win32",
|
|
62
|
+
windowsHide: true
|
|
63
|
+
});
|
|
64
|
+
const maxBuffer = options.maxBuffer ?? 1024 * 1024;
|
|
65
|
+
const chunks = { stdout: [], stderr: [] };
|
|
66
|
+
const sizes = { stdout: 0, stderr: 0 };
|
|
67
|
+
let stderrTruncated = false;
|
|
68
|
+
let failure;
|
|
69
|
+
let timeoutKind;
|
|
70
|
+
let terminationError;
|
|
71
|
+
const terminate = () => {
|
|
72
|
+
try {
|
|
73
|
+
if (child.pid !== undefined && process.platform !== "win32") {
|
|
74
|
+
try {
|
|
75
|
+
process.kill(-child.pid, "SIGKILL");
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error.code !== "ESRCH")
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
child.kill("SIGKILL");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
terminationError = error;
|
|
88
|
+
child.kill("SIGKILL");
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const expire = (kind) => {
|
|
92
|
+
if (timeoutKind !== undefined || failure !== undefined)
|
|
93
|
+
return;
|
|
94
|
+
timeoutKind = kind;
|
|
95
|
+
terminate();
|
|
96
|
+
};
|
|
97
|
+
child.once("error", error => { failure ??= error; });
|
|
98
|
+
const completion = new Promise(settle => {
|
|
99
|
+
child.once("close", (exitCode, signal) => settle({
|
|
100
|
+
// A negative close code is an OS spawn error, not a process exit.
|
|
101
|
+
exitCode: exitCode !== null && exitCode >= 0 ? exitCode : null, signal
|
|
102
|
+
}));
|
|
103
|
+
});
|
|
104
|
+
const totalTimer = setTimeout(() => expire("total"), budget.totalMs);
|
|
105
|
+
const inactivityTimer = budget.inactivityMs === undefined
|
|
106
|
+
? undefined : setTimeout(() => expire("inactivity"), budget.inactivityMs);
|
|
107
|
+
const collect = (stream, chunk) => {
|
|
108
|
+
if (chunk.length === 0)
|
|
109
|
+
return;
|
|
110
|
+
if (timeoutKind === undefined && failure === undefined)
|
|
111
|
+
inactivityTimer?.refresh();
|
|
112
|
+
if (stream === "stderr" && options.stderrTail) {
|
|
113
|
+
const limit = Math.min(maxBuffer, 64 * 1024);
|
|
114
|
+
stderrTruncated ||= sizes.stderr + chunk.length > limit;
|
|
115
|
+
const tail = Buffer.concat([...chunks.stderr, chunk]);
|
|
116
|
+
chunks.stderr = [tail.subarray(Math.max(0, tail.length - limit))];
|
|
117
|
+
sizes.stderr = Math.min(limit, tail.length);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const remaining = maxBuffer - sizes[stream];
|
|
121
|
+
if (remaining > 0) {
|
|
122
|
+
chunks[stream].push(chunk.subarray(0, remaining));
|
|
123
|
+
sizes[stream] += Math.min(chunk.length, remaining);
|
|
124
|
+
}
|
|
125
|
+
if (chunk.length > remaining && failure === undefined) {
|
|
126
|
+
failure = Object.assign(new Error(`${stream} maxBuffer length exceeded (${maxBuffer} bytes).`), { code: "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" });
|
|
127
|
+
terminate();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
child.stdout.on("data", chunk => collect("stdout", chunk));
|
|
131
|
+
child.stderr.on("data", chunk => collect("stderr", chunk));
|
|
132
|
+
child.stdin.on("error", error => {
|
|
133
|
+
failure ??= error;
|
|
134
|
+
terminate();
|
|
135
|
+
});
|
|
136
|
+
try {
|
|
137
|
+
child.stdin.end(options.input);
|
|
138
|
+
const status = await completion;
|
|
139
|
+
const result = { stdout: Buffer.concat(chunks.stdout), stderr: Buffer.concat(chunks.stderr), stderrTruncated };
|
|
140
|
+
if (failure !== undefined || timeoutKind !== undefined || status.exitCode !== 0 || status.signal !== null) {
|
|
141
|
+
const error = new GitCommandError(failure, timeoutKind, budget, Math.round(performance.now() - started), { ...result, ...status });
|
|
142
|
+
if (terminationError !== undefined) {
|
|
143
|
+
throw new AggregateError([error, terminationError], `${error.message} Process-group termination failed; helper effects are unconfirmed.`, { cause: error });
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
clearTimeout(totalTimer);
|
|
151
|
+
clearTimeout(inactivityTimer);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { realpathSync, statSync } from "node:fs";
|
|
3
3
|
import { resolveExecutable } from "../external/pinnedCommandRunner.js";
|
|
4
|
+
import { DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS } from "../config/gitConfig.js";
|
|
4
5
|
// The fetch isolation boundary needs --no-write-fetch-head. Older Git cannot
|
|
5
6
|
// implement it by saving/restoring FETCH_HEAD without racing another writer.
|
|
6
7
|
export const MINIMUM_GIT_VERSION = "2.29.0";
|
|
@@ -17,7 +18,7 @@ export function gitVersionSupport(output) {
|
|
|
17
18
|
}
|
|
18
19
|
let cached;
|
|
19
20
|
/** Pin the executable checked, reusing only an unchanged on-disk identity. */
|
|
20
|
-
export function supportedGit() {
|
|
21
|
+
export function supportedGit(timeoutMs = DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS * 1_000) {
|
|
21
22
|
const selected = resolveExecutable("git", process.env.PATH);
|
|
22
23
|
if (selected === undefined)
|
|
23
24
|
throw new Error("Git executable not found in PATH; install a supported Git before retrying. No Git operation was started.");
|
|
@@ -27,7 +28,7 @@ export function supportedGit() {
|
|
|
27
28
|
if (cached?.identity === identity)
|
|
28
29
|
return cached;
|
|
29
30
|
const output = execFileSync(path, ["--version"], {
|
|
30
|
-
encoding: "utf8", timeout:
|
|
31
|
+
encoding: "utf8", timeout: timeoutMs, stdio: ["ignore", "pipe", "pipe"]
|
|
31
32
|
});
|
|
32
33
|
const support = gitVersionSupport(output);
|
|
33
34
|
if (!support.supported) {
|