@minhpnq1807/contextos 0.5.42 → 0.5.44

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.
@@ -1,7 +1,9 @@
1
1
  import { scheduleContext } from "./scheduler.js";
2
2
  import { appendJsonLine, writeJsonFile } from "./fs-utils.js";
3
+ import { maybeAutoWarmWorkspace } from "./auto-warm.js";
3
4
  import { callCtxScoreContext } from "./ctx-mcp-client.js";
4
5
  import { resolveHookCwd } from "./hook-io.js";
6
+ import { loadOutputConfig } from "./output-config.js";
5
7
  import { scoreContext as scoreContextDirect } from "./score-context.js";
6
8
  import path from "node:path";
7
9
 
@@ -14,7 +16,11 @@ export async function handlePromptPayload(
14
16
  started = Date.now(),
15
17
  injectContext = process.env.CONTEXTOS_INJECT !== "0",
16
18
  scoreContextClient = callCtxScoreContext,
17
- mcpDataDir
19
+ scoreContextDirectClient = scoreContextDirect,
20
+ autoWarmWorkspace = maybeAutoWarmWorkspace,
21
+ mcpDataDir,
22
+ outputConfig,
23
+ directFallbackTimeoutMs = Number(process.env.CONTEXTOS_DIRECT_FALLBACK_TIMEOUT_MS || 6000)
18
24
  } = {}
19
25
  ) {
20
26
  const prompt = payload.prompt || payload.message || payload.user_prompt || "";
@@ -31,21 +37,31 @@ export async function handlePromptPayload(
31
37
  maxFiles: 3
32
38
  }, {
33
39
  dataDir: mcpDataDir || dataDir,
34
- timeoutMs: Number(process.env.CONTEXTOS_MCP_BRIDGE_TIMEOUT_MS || 1000)
40
+ timeoutMs: Number(process.env.CONTEXTOS_MCP_BRIDGE_TIMEOUT_MS || 2000)
35
41
  });
36
42
  } catch (error) {
37
- scored = await scoreContextDirect({
38
- cwd,
39
- prompt,
40
- openFiles,
41
- maxFiles: 3,
42
- dataDir: mcpDataDir || dataDir
43
- });
44
- scored.telemetry = {
45
- ...(scored.telemetry || {}),
46
- bridgeStatus: "fallback",
47
- bridgeError: error?.message || String(error)
48
- };
43
+ try {
44
+ scored = await withTimeout(scoreContextDirectClient({
45
+ cwd,
46
+ prompt,
47
+ openFiles,
48
+ maxFiles: 3,
49
+ dataDir: mcpDataDir || dataDir,
50
+ embeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_EMBEDDING_TIMEOUT_MS || 500),
51
+ fileEmbeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_FILE_EMBEDDING_TIMEOUT_MS || 500)
52
+ }), directFallbackTimeoutMs, "direct fallback scoring");
53
+ scored.telemetry = {
54
+ ...(scored.telemetry || {}),
55
+ bridgeStatus: "fallback",
56
+ bridgeError: error?.message || String(error)
57
+ };
58
+ } catch (directError) {
59
+ scored = emptyScore({
60
+ bridgeStatus: "fallback-failed",
61
+ bridgeError: error?.message || String(error),
62
+ directFallbackError: directError?.message || String(directError)
63
+ });
64
+ }
49
65
  }
50
66
 
51
67
  if (scored.error) throw new Error(scored.error);
@@ -53,7 +69,16 @@ export async function handlePromptPayload(
53
69
  const relevantFiles = (scored.suggestedFiles || []).slice(0, 3);
54
70
  const suggestedSkills = (scored.suggestedSkills || []).slice(0, 3);
55
71
  const suggestedWorkflows = (scored.suggestedWorkflows || []).slice(0, 2);
56
- const scheduled = scheduleContext({ rules: scoredRules, relevantFiles, suggestedSkills, suggestedWorkflows });
72
+ const effectiveOutputConfig = outputConfig || loadOutputConfig();
73
+ const scheduled = scheduleContext({ rules: scoredRules, relevantFiles, suggestedSkills, suggestedWorkflows, outputConfig: effectiveOutputConfig });
74
+ const contextEmptyReason = emptyContextReason({ scheduled, outputConfig: effectiveOutputConfig, injectContext });
75
+ const autoWarm = autoWarmWorkspace({
76
+ cwd,
77
+ prompt,
78
+ dataDir,
79
+ reason: contextEmptyReason,
80
+ now: now.getTime()
81
+ });
57
82
 
58
83
  const runtime = {
59
84
  at: now.toISOString(),
@@ -72,7 +97,9 @@ export async function handlePromptPayload(
72
97
  rulesInjected: (scheduled.highRules?.length || 0) + (scheduled.midRules?.length || 0),
73
98
  filesSuggested: relevantFiles.length,
74
99
  skillsSuggested: suggestedSkills.length,
75
- workflowsSuggested: suggestedWorkflows.length
100
+ workflowsSuggested: suggestedWorkflows.length,
101
+ emptyContextReason: contextEmptyReason,
102
+ autoWarm
76
103
  },
77
104
  scheduled,
78
105
  injected: injectContext,
@@ -86,12 +113,60 @@ export async function handlePromptPayload(
86
113
  // Context injection is the critical path; diagnostics are best-effort.
87
114
  }
88
115
 
89
- return {
116
+ const additionalContext = injectContext ? scheduled.additionalContext : "";
117
+ const output = {
90
118
  continue: true,
91
- suppressOutput: true,
92
- hookSpecificOutput: {
119
+ suppressOutput: true
120
+ };
121
+ if (additionalContext) {
122
+ output.hookSpecificOutput = {
93
123
  hookEventName: "UserPromptSubmit",
94
- additionalContext: injectContext ? scheduled.additionalContext : ""
124
+ additionalContext
125
+ };
126
+ }
127
+ return output;
128
+ }
129
+
130
+ function emptyContextReason({ scheduled, outputConfig, injectContext }) {
131
+ if (!injectContext) return "injection-disabled";
132
+ if (scheduled.additionalContext) return null;
133
+ const sections = outputConfig?.sections || {};
134
+ const available = [];
135
+ if ((scheduled.highRules?.length || 0) || (scheduled.midRules?.length || 0)) available.push("rules");
136
+ if (scheduled.relevantFiles?.length) available.push("files");
137
+ if (scheduled.suggestedSkills?.length) available.push("skills");
138
+ if (scheduled.suggestedWorkflows?.length) available.push("workflows");
139
+ if (!available.length) return "no-context-candidates";
140
+ const enabled = available.filter((section) => sections[section] !== false);
141
+ return enabled.length ? "enabled-sections-empty-after-formatting" : `available-sections-disabled:${available.join(",")}`;
142
+ }
143
+
144
+ function emptyScore(telemetry = {}) {
145
+ return {
146
+ scoredRules: [],
147
+ suggestedFiles: [],
148
+ suggestedSkills: [],
149
+ suggestedWorkflows: [],
150
+ telemetry: {
151
+ elapsedMs: 0,
152
+ modelStatus: "skipped",
153
+ rulesParsed: 0,
154
+ rulesInjected: 0,
155
+ filesSuggested: 0,
156
+ skillsSuggested: 0,
157
+ workflowsSuggested: 0,
158
+ ...telemetry
95
159
  }
96
160
  };
97
161
  }
162
+
163
+ function withTimeout(promise, timeoutMs, label) {
164
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return promise;
165
+ let timer;
166
+ return Promise.race([
167
+ promise,
168
+ new Promise((_, reject) => {
169
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
170
+ })
171
+ ]).finally(() => clearTimeout(timer));
172
+ }
@@ -6,6 +6,7 @@ import { stdin as input, stdout as output } from "node:process";
6
6
  import { execFileSync, spawn } from "node:child_process";
7
7
 
8
8
  import { defaultDataRoot } from "./workspace-data.js";
9
+ import { formatTomlValue, readMcpServersFromToml } from "./toml-config.js";
9
10
 
10
11
  const DEFAULT_AGENTS = ["codex", "claude", "antigravity", "copilot"];
11
12
  const CTX_MCP_NAME = "ctx-mcp";
@@ -171,60 +172,12 @@ function hasTomlSection(content, sectionName) {
171
172
  return new RegExp(`^\\[${sectionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\]\\s*$`, "m").test(content);
172
173
  }
173
174
 
174
- function findMcpServerSections(content) {
175
- const lines = String(content || "").split(/\r?\n/);
176
- const sections = [];
177
- for (let index = 0; index < lines.length; index += 1) {
178
- const match = lines[index].match(/^\[mcp_servers\.([^\].]+)\]\s*$/);
179
- if (!match) continue;
180
- let end = lines.length;
181
- for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
182
- if (/^\[/.test(lines[cursor])) {
183
- end = cursor;
184
- break;
185
- }
186
- }
187
- sections.push({
188
- name: unquoteTomlKey(match[1]),
189
- body: lines.slice(index + 1, end)
190
- });
191
- }
192
- return sections;
193
- }
194
-
195
- function findStringValue(lines, key) {
196
- const line = lines.find((item) => new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(item));
197
- if (!line) return null;
198
- const match = line.match(/=\s*"((?:\\.|[^"\\])*)"/);
199
- return match ? unescapeTomlString(match[1]) : null;
200
- }
201
-
202
- function findArrayValue(lines, key) {
203
- const line = lines.find((item) => new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(item));
204
- if (!line) return [];
205
- const arrayMatch = line.match(/=\s*\[(.*)\]\s*$/);
206
- if (!arrayMatch) return [];
207
- const values = [];
208
- const pattern = /"((?:\\.|[^"\\])*)"/g;
209
- let match;
210
- while ((match = pattern.exec(arrayMatch[1]))) values.push(unescapeTomlString(match[1]));
211
- return values;
212
- }
213
-
214
175
  function tomlString(value) {
215
- return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
176
+ return formatTomlValue(value);
216
177
  }
217
178
 
218
179
  function tomlArray(values = []) {
219
- return `[${values.map(tomlString).join(", ")}]`;
220
- }
221
-
222
- function unescapeTomlString(value) {
223
- return String(value).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
224
- }
225
-
226
- function unquoteTomlKey(value) {
227
- return value.replace(/^"|"$/g, "");
180
+ return formatTomlValue(values);
228
181
  }
229
182
 
230
183
  function escapeRegExp(value) {
@@ -246,19 +199,14 @@ function unwrapContextOSProxy(command, args = []) {
246
199
  export function readCodexMcpServers({ configPath = codexConfigPath() } = {}) {
247
200
  if (!fs.existsSync(configPath)) return [];
248
201
  const content = fs.readFileSync(configPath, "utf8");
249
- const servers = [];
250
- for (const section of findMcpServerSections(content)) {
251
- const command = findStringValue(section.body, "command");
252
- if (!command) continue;
253
- const args = findArrayValue(section.body, "args");
202
+ return readMcpServersFromToml(content).map(({ name, command, args }) => {
254
203
  const unwrapped = unwrapContextOSProxy(command, args);
255
- servers.push({
256
- name: section.name,
204
+ return {
205
+ name,
257
206
  command: unwrapped.command,
258
207
  args: unwrapped.args
259
- });
260
- }
261
- return servers;
208
+ };
209
+ });
262
210
  }
263
211
 
264
212
  export function readProjectMcpJsonServers({ cwd = process.cwd(), configPath = path.join(cwd, ".mcp.json") } = {}) {
@@ -302,17 +250,7 @@ function mergeMcpServers(...groups) {
302
250
  function readRulerMcpServers({ tomlPath } = {}) {
303
251
  if (!tomlPath || !fs.existsSync(tomlPath)) return [];
304
252
  const content = fs.readFileSync(tomlPath, "utf8");
305
- const servers = [];
306
- for (const section of findMcpServerSections(content)) {
307
- const command = findStringValue(section.body, "command");
308
- if (!command) continue;
309
- servers.push({
310
- name: section.name,
311
- command,
312
- args: findArrayValue(section.body, "args")
313
- });
314
- }
315
- return servers;
253
+ return readMcpServersFromToml(content);
316
254
  }
317
255
 
318
256
  function readRulerMcpServer({ tomlPath, name } = {}) {
@@ -1,26 +1,36 @@
1
+ import path from "node:path";
2
+
3
+ import { loadOutputConfig } from "./output-config.js";
1
4
 
2
5
  const MAX_CONTEXT_CHARS = 4000;
3
6
 
4
- export function scheduleContext({ rules = [], relevantFiles = [], suggestedSkills = [], suggestedWorkflows = [], maxChars = MAX_CONTEXT_CHARS } = {}) {
7
+ export function scheduleContext({
8
+ rules = [],
9
+ relevantFiles = [],
10
+ suggestedSkills = [],
11
+ suggestedWorkflows = [],
12
+ maxChars = MAX_CONTEXT_CHARS,
13
+ outputConfig = loadOutputConfig()
14
+ } = {}) {
5
15
  const orderedRules = [...rules].sort(compareRulesForContext);
6
16
  const high = orderedRules.filter((rule) => rule.score >= 0.5);
7
17
  const mid = orderedRules.filter((rule) => rule.score >= 0.1 && rule.score < 0.5);
8
18
  const dropped = orderedRules.filter((rule) => rule.score < 0.1);
9
19
 
10
20
  const sections = [];
11
- if (high.length) {
21
+ if (outputConfig.sections.rules && high.length) {
12
22
  sections.push(section("Critical ContextOS rules", high.slice(0, 5).map(formatRule)));
13
23
  }
14
- if (relevantFiles.length) {
15
- sections.push(section("Suggested files to check", relevantFiles.map((file) => `- ${file.path}`)));
24
+ if (outputConfig.sections.files && relevantFiles.length) {
25
+ sections.push(section("Suggested files to check", relevantFiles.map(formatFile)));
16
26
  }
17
- if (suggestedSkills.length) {
18
- sections.push(section("Skills to activate for this task", suggestedSkills.map(formatSkill)));
27
+ if (outputConfig.sections.skills && suggestedSkills.length) {
28
+ sections.push(inlineSection("Skills to activate for this task", suggestedSkills.map(formatSkill)));
19
29
  }
20
- if (suggestedWorkflows.length) {
30
+ if (outputConfig.sections.workflows && suggestedWorkflows.length) {
21
31
  sections.push(section("Suggested workflow for this task", suggestedWorkflows.map(formatWorkflow)));
22
32
  }
23
- if (mid.length) {
33
+ if (outputConfig.sections.rules && mid.length) {
24
34
  sections.push(section("Additional relevant rules", mid.slice(0, 5).map(formatRule)));
25
35
  }
26
36
 
@@ -51,8 +61,15 @@ function rulePriority(rule) {
51
61
  }
52
62
 
53
63
  function section(title, lines) {
54
- if (!lines.length) return "";
55
- return `## ${title}\n${lines.join("\n")}`;
64
+ const uniqueLines = [...new Set(lines)];
65
+ if (!uniqueLines.length) return "";
66
+ return `## ${title}\n${uniqueLines.join("\n")}`;
67
+ }
68
+
69
+ function inlineSection(title, values) {
70
+ const uniqueValues = [...new Set(values)];
71
+ if (!uniqueValues.length) return "";
72
+ return `## ${title}: ${uniqueValues.join(", ")}`;
56
73
  }
57
74
 
58
75
 
@@ -60,23 +77,18 @@ function formatRule(rule) {
60
77
  return `- ${rule.content}`;
61
78
  }
62
79
 
80
+ function formatFile(file) {
81
+ return `- ${path.basename(file.path)}`;
82
+ }
83
+
63
84
  function formatSkill(skill) {
64
- const desc = skill.description
65
- ? `: ${truncate(skill.description, 80)}`
66
- : "";
67
- return `- ${skill.name}${desc}`;
85
+ return skill.name;
68
86
  }
69
87
 
70
88
  function formatWorkflow(workflow) {
71
89
  const name = workflow.title || workflow.name;
72
- const hint = workflow.hint ? `: ${workflow.hint}` : "";
73
90
  const chain = workflow.chain?.length ? `\n chain: ${workflow.chain.join(" -> ")}` : "";
74
- return `- ${name}${hint}${chain}`;
75
- }
76
-
77
- function truncate(text, maxLen) {
78
- if (text.length <= maxLen) return text;
79
- return `${text.slice(0, maxLen - 1)}…`;
91
+ return `- ${name}${chain}`;
80
92
  }
81
93
 
82
94
  function trimToLimit(value, maxChars) {
@@ -17,46 +17,74 @@ export async function scoreContext({
17
17
  skills = null,
18
18
  workflows = null,
19
19
  embeddingTimeoutMs = 5000,
20
- fileEmbeddingTimeoutMs = Number(process.env.CONTEXTOS_FILE_EMBEDDING_TIMEOUT_MS || 80)
20
+ fileEmbeddingTimeoutMs = Number(process.env.CONTEXTOS_FILE_EMBEDDING_TIMEOUT_MS || 1000)
21
21
  } = {}) {
22
22
  const started = Date.now();
23
- const merged = readAgentsChain({ cwd });
24
- const rawRules = parseRules(merged.content);
25
- const parsedRules = filterActionableRules(rawRules);
26
- const baseScoredRules = scoreRules(parsedRules, prompt, openFiles);
27
- const embedding = await enhanceRuleScoresWithEmbeddings(baseScoredRules, prompt, {
28
- dataDir,
29
- sources: merged.sources,
30
- timeoutMs: embeddingTimeoutMs,
31
- allowRemote: false
23
+ const ruleInputsPromise = Promise.resolve().then(() => {
24
+ const merged = readAgentsChain({ cwd });
25
+ const rawRules = parseRules(merged.content);
26
+ const parsedRules = filterActionableRules(rawRules);
27
+ return {
28
+ merged,
29
+ rawRules,
30
+ parsedRules,
31
+ baseScoredRules: scoreRules(parsedRules, prompt, openFiles)
32
+ };
32
33
  });
33
- const scoredRules = embedding.rules;
34
- const suggestedFiles = await findRelevantFiles({
35
- cwd,
36
- task: prompt,
37
- rules: scoredRules,
38
- dataDir,
39
- limit: maxFiles,
40
- fileEmbeddingTimeoutMs,
41
- fileEmbeddingOptions: {
34
+
35
+ const rulesPromise = ruleInputsPromise.then(({ merged, baseScoredRules }) => {
36
+ return enhanceRuleScoresWithEmbeddings(baseScoredRules, prompt, {
37
+ dataDir,
38
+ sources: merged.sources,
39
+ timeoutMs: embeddingTimeoutMs,
42
40
  allowRemote: false
43
- }
41
+ });
44
42
  });
45
- const skillCatalog = Array.isArray(skills) ? skills : scanSkills({ cwd });
46
- const suggestedSkills = await suggestSkills({
47
- prompt,
48
- skills: skillCatalog,
49
- dataDir,
50
- limit: maxSkills
43
+
44
+ const filesPromise = ruleInputsPromise.then(({ baseScoredRules }) => {
45
+ return findRelevantFiles({
46
+ cwd,
47
+ task: prompt,
48
+ rules: baseScoredRules,
49
+ dataDir,
50
+ limit: maxFiles,
51
+ fileEmbeddingTimeoutMs,
52
+ fileEmbeddingOptions: {
53
+ allowRemote: false
54
+ }
55
+ });
51
56
  });
52
- const workflowCatalog = Array.isArray(workflows) ? workflows : scanWorkflows({ cwd });
53
- const suggestedWorkflows = await suggestWorkflows({
54
- prompt,
55
- workflows: workflowCatalog,
56
- dataDir,
57
- limit: maxWorkflows
57
+
58
+ const skillsPromise = Promise.resolve().then(async () => {
59
+ const catalog = Array.isArray(skills) ? skills : scanSkills({ cwd });
60
+ return {
61
+ catalog,
62
+ suggestions: await suggestSkills({ cwd, prompt, skills: catalog, dataDir, limit: maxSkills })
63
+ };
64
+ });
65
+
66
+ const workflowsPromise = Promise.resolve().then(async () => {
67
+ const catalog = Array.isArray(workflows) ? workflows : scanWorkflows({ cwd });
68
+ return {
69
+ catalog,
70
+ suggestions: await suggestWorkflows({ prompt, workflows: catalog, dataDir, limit: maxWorkflows })
71
+ };
58
72
  });
59
73
 
74
+ const [ruleInputs, embedding, suggestedFiles, skillResult, workflowResult] = await Promise.all([
75
+ ruleInputsPromise,
76
+ rulesPromise,
77
+ filesPromise,
78
+ skillsPromise,
79
+ workflowsPromise
80
+ ]);
81
+ const { merged, rawRules, parsedRules } = ruleInputs;
82
+ const scoredRules = embedding.rules;
83
+ const skillCatalog = skillResult.catalog;
84
+ const suggestedSkills = skillResult.suggestions;
85
+ const workflowCatalog = workflowResult.catalog;
86
+ const suggestedWorkflows = workflowResult.suggestions;
87
+
60
88
  return {
61
89
  cwd,
62
90
  prompt,
@@ -36,13 +36,16 @@ export function setupSummaryLines({
36
36
  cwd = process.cwd(),
37
37
  agents = DEFAULT_AGENTS,
38
38
  syncRules = true,
39
- syncSkills = true
39
+ syncSkills = true,
40
+ promptSections = null
40
41
  } = {}) {
41
- return [
42
+ const lines = [
42
43
  `Installation directory: ${cwd}`,
43
44
  `Agents: ${agents.join(", ") || "(none)"}`,
44
45
  `Prompt context injection: always enabled`,
45
46
  `Ruler rule/MCP sync: ${syncRules ? "enabled" : "skipped"}`,
46
47
  `skillshare skill sync: ${syncSkills ? "enabled" : "skipped"}`
47
48
  ];
49
+ if (promptSections !== null) lines.push(`Prompt sections shown: ${promptSections}`);
50
+ return lines;
48
51
  }
@@ -0,0 +1,88 @@
1
+ import fs from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+
4
+ export function shellInvocation(command, { platform = process.platform, env = process.env } = {}) {
5
+ if (platform === "win32") {
6
+ return {
7
+ command: env.ComSpec || env.COMSPEC || "cmd.exe",
8
+ args: ["/d", "/s", "/c", command]
9
+ };
10
+ }
11
+ return {
12
+ command: fs.existsSync("/bin/sh") ? "/bin/sh" : "sh",
13
+ args: ["-c", command]
14
+ };
15
+ }
16
+
17
+ export function runPrefixedCommand(commandText, {
18
+ spawnFn = spawn,
19
+ stdout = process.stdout,
20
+ stderr = process.stderr,
21
+ stdin = "inherit",
22
+ platform = process.platform,
23
+ env = process.env,
24
+ prefix = "\x1B[2m│\x1B[0m "
25
+ } = {}) {
26
+ const shell = shellInvocation(commandText, { platform, env });
27
+ return new Promise((resolve, reject) => {
28
+ let settled = false;
29
+ const fail = (error) => {
30
+ if (settled) return;
31
+ settled = true;
32
+ reject(error);
33
+ };
34
+
35
+ let child;
36
+ try {
37
+ child = spawnFn(shell.command, shell.args, {
38
+ stdio: [stdin, "pipe", "pipe"],
39
+ windowsHide: true
40
+ });
41
+ } catch (error) {
42
+ fail(error);
43
+ return;
44
+ }
45
+
46
+ pipePrefixed(child.stdout, stdout, prefix);
47
+ pipePrefixed(child.stderr, stderr, prefix);
48
+
49
+ child.on("error", (error) => {
50
+ if (error?.code === "ENOENT") {
51
+ fail(new Error([
52
+ `Unable to start shell '${shell.command}' for installer command.`,
53
+ `Original command: ${commandText}`,
54
+ platform === "win32"
55
+ ? "Fix: ensure cmd.exe is available through ComSpec/COMSPEC, or run ContextOS from a normal Command Prompt, PowerShell, or Windows Terminal session."
56
+ : "Fix: ensure /bin/sh exists, or install a POSIX shell before running ContextOS installers."
57
+ ].join("\n")));
58
+ return;
59
+ }
60
+ fail(error);
61
+ });
62
+
63
+ child.on("close", (code) => {
64
+ if (settled) return;
65
+ settled = true;
66
+ if (code === 0) resolve();
67
+ else reject(new Error(`Installer command exited with code ${code}: ${commandText}`));
68
+ });
69
+ });
70
+ }
71
+
72
+ function pipePrefixed(stream, target, prefix) {
73
+ if (!stream) return;
74
+ let needPrefix = true;
75
+ stream.on("data", (buf) => {
76
+ const str = buf.toString();
77
+ let out = "";
78
+ for (const ch of str) {
79
+ if (needPrefix) {
80
+ out += prefix;
81
+ needPrefix = false;
82
+ }
83
+ out += ch;
84
+ if (ch === "\n") needPrefix = true;
85
+ }
86
+ target.write(out);
87
+ });
88
+ }