okf-catalog 0.1.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.
Files changed (107) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +10 -0
  4. package/README.md +87 -0
  5. package/dist/bundle/cat-file.js +94 -0
  6. package/dist/bundle/cat-file.js.map +1 -0
  7. package/dist/bundle/contract.js +60 -0
  8. package/dist/bundle/contract.js.map +1 -0
  9. package/dist/bundle/frontmatter.js +106 -0
  10. package/dist/bundle/frontmatter.js.map +1 -0
  11. package/dist/bundle/git-tree.js +109 -0
  12. package/dist/bundle/git-tree.js.map +1 -0
  13. package/dist/bundle/index-file.js +90 -0
  14. package/dist/bundle/index-file.js.map +1 -0
  15. package/dist/bundle/links.js +57 -0
  16. package/dist/bundle/links.js.map +1 -0
  17. package/dist/bundle/load.js +363 -0
  18. package/dist/bundle/load.js.map +1 -0
  19. package/dist/bundle/manifest.js +95 -0
  20. package/dist/bundle/manifest.js.map +1 -0
  21. package/dist/bundle/markdown.js +210 -0
  22. package/dist/bundle/markdown.js.map +1 -0
  23. package/dist/bundle/model.js +7 -0
  24. package/dist/bundle/model.js.map +1 -0
  25. package/dist/bundle/page.js +374 -0
  26. package/dist/bundle/page.js.map +1 -0
  27. package/dist/bundle/paths.js +36 -0
  28. package/dist/bundle/paths.js.map +1 -0
  29. package/dist/bundle/reserved.js +56 -0
  30. package/dist/bundle/reserved.js.map +1 -0
  31. package/dist/bundle/timestamp.js +49 -0
  32. package/dist/bundle/timestamp.js.map +1 -0
  33. package/dist/catalog/model.js +32 -0
  34. package/dist/catalog/model.js.map +1 -0
  35. package/dist/catalog/nearest.js +56 -0
  36. package/dist/catalog/nearest.js.map +1 -0
  37. package/dist/catalog/outputs.js +438 -0
  38. package/dist/catalog/outputs.js.map +1 -0
  39. package/dist/catalog/provenance.js +37 -0
  40. package/dist/catalog/provenance.js.map +1 -0
  41. package/dist/catalog/runtime.js +2 -0
  42. package/dist/catalog/runtime.js.map +1 -0
  43. package/dist/catalog/text.js +140 -0
  44. package/dist/catalog/text.js.map +1 -0
  45. package/dist/cli.js +81 -0
  46. package/dist/cli.js.map +1 -0
  47. package/dist/commands/check.js +155 -0
  48. package/dist/commands/check.js.map +1 -0
  49. package/dist/commands/pack.js +182 -0
  50. package/dist/commands/pack.js.map +1 -0
  51. package/dist/commands/serve.js +452 -0
  52. package/dist/commands/serve.js.map +1 -0
  53. package/dist/config/company-config.js +272 -0
  54. package/dist/config/company-config.js.map +1 -0
  55. package/dist/derive/derived-document.js +42 -0
  56. package/dist/derive/derived-document.js.map +1 -0
  57. package/dist/engine/qmd-render.js +54 -0
  58. package/dist/engine/qmd-render.js.map +1 -0
  59. package/dist/engine/qmd.js +241 -0
  60. package/dist/engine/qmd.js.map +1 -0
  61. package/dist/fs/cache-dir.js +140 -0
  62. package/dist/fs/cache-dir.js.map +1 -0
  63. package/dist/fs/company-lock.js +162 -0
  64. package/dist/fs/company-lock.js.map +1 -0
  65. package/dist/fs/walk.js +168 -0
  66. package/dist/fs/walk.js.map +1 -0
  67. package/dist/log.js +65 -0
  68. package/dist/log.js.map +1 -0
  69. package/dist/mcp/server.js +30 -0
  70. package/dist/mcp/server.js.map +1 -0
  71. package/dist/mcp/stdio.js +45 -0
  72. package/dist/mcp/stdio.js.map +1 -0
  73. package/dist/mcp/tools.js +249 -0
  74. package/dist/mcp/tools.js.map +1 -0
  75. package/dist/report/report.js +43 -0
  76. package/dist/report/report.js.map +1 -0
  77. package/dist/search/engine.js +2 -0
  78. package/dist/search/engine.js.map +1 -0
  79. package/dist/search/query.js +91 -0
  80. package/dist/search/query.js.map +1 -0
  81. package/dist/search/search.js +224 -0
  82. package/dist/search/search.js.map +1 -0
  83. package/dist/search/snippet.js +95 -0
  84. package/dist/search/snippet.js.map +1 -0
  85. package/dist/serve/poller.js +119 -0
  86. package/dist/serve/poller.js.map +1 -0
  87. package/dist/serve/runtime.js +390 -0
  88. package/dist/serve/runtime.js.map +1 -0
  89. package/dist/source/git-runner.js +232 -0
  90. package/dist/source/git-runner.js.map +1 -0
  91. package/dist/source/git.js +457 -0
  92. package/dist/source/git.js.map +1 -0
  93. package/dist/source/local.js +20 -0
  94. package/dist/source/local.js.map +1 -0
  95. package/dist/source/source.js +2 -0
  96. package/dist/source/source.js.map +1 -0
  97. package/npm-shrinkwrap.json +5482 -0
  98. package/package.json +80 -0
  99. package/plugin/claude-code/.claude-plugin/plugin.json +15 -0
  100. package/plugin/claude-code/.mcp.json +10 -0
  101. package/plugin/claude-code/skills/okf-catalog/SKILL.md +15 -0
  102. package/recipes/publish/README.md +32 -0
  103. package/recipes/publish/checkers.lock +435 -0
  104. package/recipes/publish/checkers.txt +5 -0
  105. package/recipes/publish/pack.sh +60 -0
  106. package/recipes/publish/publish.yml +88 -0
  107. package/recipes/publish/push.sh +63 -0
@@ -0,0 +1,232 @@
1
+ import { spawn } from "node:child_process";
2
+ import { escapeControls } from "../catalog/text.js";
3
+ import { LOG_VALUE_CAP } from "../log.js";
4
+ /** Variables git may inherit from the person's session; everything else is withheld (D47). */
5
+ const KEEP = [
6
+ "HOME",
7
+ "PATH",
8
+ "USER",
9
+ "LOGNAME",
10
+ "LANG",
11
+ "LC_ALL",
12
+ "TMPDIR",
13
+ "XDG_CONFIG_HOME",
14
+ "XDG_RUNTIME_DIR",
15
+ "DBUS_SESSION_BUS_ADDRESS",
16
+ "SSH_AUTH_SOCK",
17
+ "SSH_AGENT_PID",
18
+ "GIT_SSH",
19
+ "GIT_SSH_COMMAND",
20
+ "http_proxy",
21
+ "https_proxy",
22
+ "HTTP_PROXY",
23
+ "HTTPS_PROXY",
24
+ "ALL_PROXY",
25
+ "NO_PROXY",
26
+ "all_proxy",
27
+ "no_proxy",
28
+ "SSL_CERT_FILE",
29
+ "SSL_CERT_DIR",
30
+ "CURL_CA_BUNDLE",
31
+ "GIT_SSL_CAINFO",
32
+ "GIT_SSL_CAPATH",
33
+ ];
34
+ /** Settings every command carries: no hooks, no monitor, no submodules, no maintenance, no prompts, no credentials in URLs, objects checked. */
35
+ export const GIT_FIXED_CONFIG = [
36
+ "core.hooksPath=/dev/null",
37
+ "core.fsmonitor=false",
38
+ "submodule.recurse=false",
39
+ "maintenance.auto=false",
40
+ "gc.auto=0",
41
+ "protocol.allow=never",
42
+ "credential.interactive=false",
43
+ "transfer.credentialsInUrl=die",
44
+ "fetch.fsckObjects=true",
45
+ ];
46
+ const STDERR_RAW_CAP = 64 * 1024;
47
+ const DEFAULT_STDOUT_CAP = 16 * 1024 * 1024;
48
+ const TERM_GRACE_MS = 1000;
49
+ /** The environment git runs with: the keep list, copied when set, and the fixed settings. */
50
+ export function gitEnvironment(source, allowProtocols, cacheRoot) {
51
+ const env = {};
52
+ for (const name of KEEP) {
53
+ const value = source[name];
54
+ if (value !== undefined)
55
+ env[name] = value;
56
+ }
57
+ env.GIT_TERMINAL_PROMPT = "0";
58
+ env.GIT_ALLOW_PROTOCOL = allowProtocols;
59
+ env.GIT_ATTR_NOSYSTEM = "1";
60
+ env.GIT_CEILING_DIRECTORIES = cacheRoot;
61
+ return env;
62
+ }
63
+ /** URL userinfo replaced by `***`; the scp-like `user@host:path` form carries no password and stays. */
64
+ export const redactCredentials = (text) => text.replace(/\b([a-z][a-z0-9+.-]*):\/\/[^\s/@]+@/gi, "$1://***@");
65
+ export class GitError extends Error {
66
+ code;
67
+ stderr;
68
+ timedOut;
69
+ aborted;
70
+ overflow;
71
+ constructor(message, details = {}) {
72
+ super(message);
73
+ this.name = "GitError";
74
+ this.code = details.code ?? null;
75
+ this.stderr = details.stderr ?? "";
76
+ this.timedOut = details.timedOut ?? false;
77
+ this.aborted = details.aborted ?? false;
78
+ this.overflow = details.overflow ?? false;
79
+ }
80
+ }
81
+ /** Captured stderr as it may be logged or shown: redacted, escaped, cut. */
82
+ const presentStderr = (raw) => {
83
+ const text = Buffer.concat(raw).toString("utf8");
84
+ const safe = escapeControls(redactCredentials(text));
85
+ return safe.length <= LOG_VALUE_CAP ? safe : `${safe.slice(0, LOG_VALUE_CAP)}…`;
86
+ };
87
+ const killGroup = (child, signal) => {
88
+ if (child.pid === undefined)
89
+ return;
90
+ try {
91
+ process.kill(-child.pid, signal);
92
+ }
93
+ catch {
94
+ try {
95
+ child.kill(signal);
96
+ }
97
+ catch {
98
+ // already gone
99
+ }
100
+ }
101
+ };
102
+ /**
103
+ * Runs git as a transport, nothing more: a fixed binary, a built environment, the fixed settings, an explicit
104
+ * repository, its own process group (so a timeout or a shutdown ends ssh and every other child with it),
105
+ * streamed and capped output, and errors that carry no credential.
106
+ */
107
+ export function createGitRunner(options) {
108
+ const env = gitEnvironment(options.env, options.allowProtocols, options.cacheRoot);
109
+ const live = new Set();
110
+ let closed = false;
111
+ const configArgs = (extra = []) => [...GIT_FIXED_CONFIG, ...extra].flatMap((pair) => ["-c", pair]);
112
+ function run(args, run) {
113
+ return new Promise((resolve, reject) => {
114
+ if (closed) {
115
+ reject(new GitError("git was aborted by shutdown", { aborted: true }));
116
+ return;
117
+ }
118
+ const argv = [
119
+ ...configArgs(run.extraConfig),
120
+ ...(run.gitDir === undefined ? [] : [`--git-dir=${run.gitDir}`]),
121
+ ...args,
122
+ ];
123
+ const child = spawn(options.binary, argv, {
124
+ cwd: run.cwd,
125
+ env,
126
+ detached: true,
127
+ stdio: [run.input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
128
+ });
129
+ live.add(child);
130
+ const out = [];
131
+ const err = [];
132
+ let outBytes = 0;
133
+ let errBytes = 0;
134
+ let settled = false;
135
+ let stopped = false;
136
+ let timedOut = false;
137
+ let aborted = false;
138
+ let overflow = false;
139
+ let killTimer;
140
+ const cap = run.maxStdoutBytes ?? DEFAULT_STDOUT_CAP;
141
+ const end = () => {
142
+ if (!settled)
143
+ return;
144
+ clearTimeout(timer);
145
+ // The SIGKILL after the grace is kept even once git itself has exited: a group member that ignored
146
+ // SIGTERM (a stuck ssh) still gets it (bite 5 review BR10).
147
+ live.delete(child);
148
+ };
149
+ const terminate = () => {
150
+ killGroup(child, "SIGTERM");
151
+ killTimer = setTimeout(() => killGroup(child, "SIGKILL"), TERM_GRACE_MS);
152
+ killTimer.unref();
153
+ };
154
+ const timer = setTimeout(() => {
155
+ timedOut = true;
156
+ terminate();
157
+ }, run.timeoutMs);
158
+ child.okfAbort = () => {
159
+ aborted = true;
160
+ terminate();
161
+ };
162
+ child.stdout?.on("data", (chunk) => {
163
+ if (stopped || overflow)
164
+ return;
165
+ if (run.onStdout !== undefined) {
166
+ if (run.onStdout(chunk) === "stop") {
167
+ stopped = true;
168
+ terminate();
169
+ }
170
+ return;
171
+ }
172
+ outBytes += chunk.length;
173
+ if (outBytes > cap) {
174
+ overflow = true;
175
+ terminate();
176
+ return;
177
+ }
178
+ out.push(chunk);
179
+ });
180
+ child.stderr?.on("data", (chunk) => {
181
+ if (errBytes >= STDERR_RAW_CAP)
182
+ return;
183
+ errBytes += chunk.length;
184
+ err.push(chunk);
185
+ });
186
+ child.on("error", (error) => {
187
+ settled = true;
188
+ end();
189
+ reject(new GitError(`git could not be run: ${redactCredentials(error.message)}`));
190
+ });
191
+ child.on("close", (code) => {
192
+ settled = true;
193
+ end();
194
+ const stderr = presentStderr(err);
195
+ if (aborted)
196
+ reject(new GitError("git was aborted by shutdown", { stderr, aborted: true }));
197
+ else if (timedOut)
198
+ reject(new GitError(`git did not finish within ${run.timeoutMs} ms`, {
199
+ stderr,
200
+ timedOut: true,
201
+ }));
202
+ else if (overflow)
203
+ reject(new GitError(`git printed more than ${cap} bytes`, { stderr, overflow: true }));
204
+ else if (stopped)
205
+ resolve({ stdout: Buffer.alloc(0), stderr, code, stopped: true });
206
+ else if (code !== 0)
207
+ reject(new GitError(`git exited with ${code}: ${stderr}`, { code, stderr }));
208
+ else
209
+ resolve({ stdout: Buffer.concat(out), stderr, code, stopped: false });
210
+ });
211
+ // A git that exits without reading its input makes the pending write fail with EPIPE; that is not an event
212
+ // this process should die on (bite 5 review BR3).
213
+ child.stdin?.on("error", () => undefined);
214
+ if (run.input !== undefined)
215
+ child.stdin?.end(run.input);
216
+ });
217
+ }
218
+ return {
219
+ run,
220
+ abort: () => {
221
+ // Once aborted, no command starts again: a shutdown must not race a tick into a fresh fetch.
222
+ closed = true;
223
+ for (const child of live)
224
+ child.okfAbort?.();
225
+ },
226
+ get running() {
227
+ return live.size;
228
+ },
229
+ env,
230
+ };
231
+ }
232
+ //# sourceMappingURL=git-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-runner.js","sourceRoot":"","sources":["../../src/source/git-runner.ts"],"names":[],"mappings":"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","sourcesContent":["import { type ChildProcess, spawn } from \"node:child_process\";\nimport { escapeControls } from \"../catalog/text.js\";\nimport { LOG_VALUE_CAP } from \"../log.js\";\n\n/** Variables git may inherit from the person's session; everything else is withheld (D47). */\nconst KEEP = [\n \"HOME\",\n \"PATH\",\n \"USER\",\n \"LOGNAME\",\n \"LANG\",\n \"LC_ALL\",\n \"TMPDIR\",\n \"XDG_CONFIG_HOME\",\n \"XDG_RUNTIME_DIR\",\n \"DBUS_SESSION_BUS_ADDRESS\",\n \"SSH_AUTH_SOCK\",\n \"SSH_AGENT_PID\",\n \"GIT_SSH\",\n \"GIT_SSH_COMMAND\",\n \"http_proxy\",\n \"https_proxy\",\n \"HTTP_PROXY\",\n \"HTTPS_PROXY\",\n \"ALL_PROXY\",\n \"NO_PROXY\",\n \"all_proxy\",\n \"no_proxy\",\n \"SSL_CERT_FILE\",\n \"SSL_CERT_DIR\",\n \"CURL_CA_BUNDLE\",\n \"GIT_SSL_CAINFO\",\n \"GIT_SSL_CAPATH\",\n] as const;\n\n/** Settings every command carries: no hooks, no monitor, no submodules, no maintenance, no prompts, no credentials in URLs, objects checked. */\nexport const GIT_FIXED_CONFIG: readonly string[] = [\n \"core.hooksPath=/dev/null\",\n \"core.fsmonitor=false\",\n \"submodule.recurse=false\",\n \"maintenance.auto=false\",\n \"gc.auto=0\",\n \"protocol.allow=never\",\n \"credential.interactive=false\",\n \"transfer.credentialsInUrl=die\",\n \"fetch.fsckObjects=true\",\n];\n\nconst STDERR_RAW_CAP = 64 * 1024;\nconst DEFAULT_STDOUT_CAP = 16 * 1024 * 1024;\nconst TERM_GRACE_MS = 1000;\n\n/** The environment git runs with: the keep list, copied when set, and the fixed settings. */\nexport function gitEnvironment(\n source: NodeJS.ProcessEnv,\n allowProtocols: string,\n cacheRoot: string,\n): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = {};\n for (const name of KEEP) {\n const value = source[name];\n if (value !== undefined) env[name] = value;\n }\n env.GIT_TERMINAL_PROMPT = \"0\";\n env.GIT_ALLOW_PROTOCOL = allowProtocols;\n env.GIT_ATTR_NOSYSTEM = \"1\";\n env.GIT_CEILING_DIRECTORIES = cacheRoot;\n return env;\n}\n\n/** URL userinfo replaced by `***`; the scp-like `user@host:path` form carries no password and stays. */\nexport const redactCredentials = (text: string): string =>\n text.replace(/\\b([a-z][a-z0-9+.-]*):\\/\\/[^\\s/@]+@/gi, \"$1://***@\");\n\nexport class GitError extends Error {\n readonly code: number | null;\n readonly stderr: string;\n readonly timedOut: boolean;\n readonly aborted: boolean;\n readonly overflow: boolean;\n constructor(\n message: string,\n details: {\n code?: number | null;\n stderr?: string;\n timedOut?: boolean;\n aborted?: boolean;\n overflow?: boolean;\n } = {},\n ) {\n super(message);\n this.name = \"GitError\";\n this.code = details.code ?? null;\n this.stderr = details.stderr ?? \"\";\n this.timedOut = details.timedOut ?? false;\n this.aborted = details.aborted ?? false;\n this.overflow = details.overflow ?? false;\n }\n}\n\nexport interface RunOptions {\n cwd: string;\n /** Passed as `--git-dir=` once the clone exists, so git never searches for a repository. */\n gitDir?: string;\n timeoutMs: number;\n /** Written to the command's stdin, then closed. */\n input?: string;\n /** Receives stdout as it arrives; answering `stop` ends the command, and `stdout` is then empty. */\n onStdout?: (chunk: Buffer) => \"continue\" | \"stop\";\n /** The most stdout kept when no consumer streams it. */\n maxStdoutBytes?: number;\n /** Extra `-c key=value` settings for this call only. */\n extraConfig?: readonly string[];\n}\n\nexport interface RunResult {\n stdout: Buffer;\n stderr: string;\n code: number | null;\n /** The consumer stopped the command before it finished. */\n stopped: boolean;\n}\n\nexport interface GitRunner {\n run(args: readonly string[], options: RunOptions): Promise<RunResult>;\n /** Kills every running command, with its whole process group, and refuses every command after it. */\n abort(): void;\n readonly running: number;\n /** The environment every command runs with, for callers that must know what git will see. */\n readonly env: NodeJS.ProcessEnv;\n}\n\nexport interface RunnerOptions {\n /** An absolute path to git; never read from the environment. */\n binary: string;\n /** The `GIT_ALLOW_PROTOCOL` value: `https:ssh` in production, with `file` added by tests. */\n allowProtocols: string;\n cacheRoot: string;\n env: NodeJS.ProcessEnv;\n}\n\n/** Captured stderr as it may be logged or shown: redacted, escaped, cut. */\nconst presentStderr = (raw: Buffer[]): string => {\n const text = Buffer.concat(raw).toString(\"utf8\");\n const safe = escapeControls(redactCredentials(text));\n return safe.length <= LOG_VALUE_CAP ? safe : `${safe.slice(0, LOG_VALUE_CAP)}…`;\n};\n\nconst killGroup = (child: ChildProcess, signal: NodeJS.Signals): void => {\n if (child.pid === undefined) return;\n try {\n process.kill(-child.pid, signal);\n } catch {\n try {\n child.kill(signal);\n } catch {\n // already gone\n }\n }\n};\n\n/**\n * Runs git as a transport, nothing more: a fixed binary, a built environment, the fixed settings, an explicit\n * repository, its own process group (so a timeout or a shutdown ends ssh and every other child with it),\n * streamed and capped output, and errors that carry no credential.\n */\nexport function createGitRunner(options: RunnerOptions): GitRunner {\n const env = gitEnvironment(options.env, options.allowProtocols, options.cacheRoot);\n const live = new Set<ChildProcess>();\n let closed = false;\n const configArgs = (extra: readonly string[] = []): string[] =>\n [...GIT_FIXED_CONFIG, ...extra].flatMap((pair) => [\"-c\", pair]);\n\n function run(args: readonly string[], run: RunOptions): Promise<RunResult> {\n return new Promise<RunResult>((resolve, reject) => {\n if (closed) {\n reject(new GitError(\"git was aborted by shutdown\", { aborted: true }));\n return;\n }\n const argv = [\n ...configArgs(run.extraConfig),\n ...(run.gitDir === undefined ? [] : [`--git-dir=${run.gitDir}`]),\n ...args,\n ];\n const child = spawn(options.binary, argv, {\n cwd: run.cwd,\n env,\n detached: true,\n stdio: [run.input === undefined ? \"ignore\" : \"pipe\", \"pipe\", \"pipe\"],\n });\n live.add(child);\n const out: Buffer[] = [];\n const err: Buffer[] = [];\n let outBytes = 0;\n let errBytes = 0;\n let settled = false;\n let stopped = false;\n let timedOut = false;\n let aborted = false;\n let overflow = false;\n let killTimer: NodeJS.Timeout | undefined;\n const cap = run.maxStdoutBytes ?? DEFAULT_STDOUT_CAP;\n const end = (): void => {\n if (!settled) return;\n clearTimeout(timer);\n // The SIGKILL after the grace is kept even once git itself has exited: a group member that ignored\n // SIGTERM (a stuck ssh) still gets it (bite 5 review BR10).\n live.delete(child);\n };\n const terminate = (): void => {\n killGroup(child, \"SIGTERM\");\n killTimer = setTimeout(() => killGroup(child, \"SIGKILL\"), TERM_GRACE_MS);\n killTimer.unref();\n };\n const timer = setTimeout(() => {\n timedOut = true;\n terminate();\n }, run.timeoutMs);\n (child as ChildProcess & { okfAbort?: () => void }).okfAbort = () => {\n aborted = true;\n terminate();\n };\n child.stdout?.on(\"data\", (chunk: Buffer) => {\n if (stopped || overflow) return;\n if (run.onStdout !== undefined) {\n if (run.onStdout(chunk) === \"stop\") {\n stopped = true;\n terminate();\n }\n return;\n }\n outBytes += chunk.length;\n if (outBytes > cap) {\n overflow = true;\n terminate();\n return;\n }\n out.push(chunk);\n });\n child.stderr?.on(\"data\", (chunk: Buffer) => {\n if (errBytes >= STDERR_RAW_CAP) return;\n errBytes += chunk.length;\n err.push(chunk);\n });\n child.on(\"error\", (error) => {\n settled = true;\n end();\n reject(new GitError(`git could not be run: ${redactCredentials(error.message)}`));\n });\n child.on(\"close\", (code) => {\n settled = true;\n end();\n const stderr = presentStderr(err);\n if (aborted) reject(new GitError(\"git was aborted by shutdown\", { stderr, aborted: true }));\n else if (timedOut)\n reject(\n new GitError(`git did not finish within ${run.timeoutMs} ms`, {\n stderr,\n timedOut: true,\n }),\n );\n else if (overflow)\n reject(new GitError(`git printed more than ${cap} bytes`, { stderr, overflow: true }));\n else if (stopped) resolve({ stdout: Buffer.alloc(0), stderr, code, stopped: true });\n else if (code !== 0)\n reject(new GitError(`git exited with ${code}: ${stderr}`, { code, stderr }));\n else resolve({ stdout: Buffer.concat(out), stderr, code, stopped: false });\n });\n // A git that exits without reading its input makes the pending write fail with EPIPE; that is not an event\n // this process should die on (bite 5 review BR3).\n child.stdin?.on(\"error\", () => undefined);\n if (run.input !== undefined) child.stdin?.end(run.input);\n });\n }\n\n return {\n run,\n abort: () => {\n // Once aborted, no command starts again: a shutdown must not race a tick into a fresh fetch.\n closed = true;\n for (const child of live) (child as ChildProcess & { okfAbort?: () => void }).okfAbort?.();\n },\n get running() {\n return live.size;\n },\n env,\n };\n}\n"]}